aboutsummaryrefslogtreecommitdiff
path: root/userapi
diff options
context:
space:
mode:
authorNeil Alexander <neilalexander@users.noreply.github.com>2020-07-28 10:53:17 +0100
committerGitHub <noreply@github.com>2020-07-28 10:53:17 +0100
commitacc8e80a51515c953c6710cb24f36fd9d1f7aeb1 (patch)
treeb33ecfd8e4d7ed3ac7aa66226f8fd80c1cdcf649 /userapi
parentc63286713570e1274759db971b15405665fa391a (diff)
User directory (#1225)
* User directory * Fix syncapi unit test * Make user directory only show remote users you know about from your joined rooms * Update sytest-whitelist * Review comments
Diffstat (limited to 'userapi')
-rw-r--r--userapi/api/api.go16
-rw-r--r--userapi/internal/api.go9
-rw-r--r--userapi/inthttp/client.go19
-rw-r--r--userapi/inthttp/server.go13
-rw-r--r--userapi/storage/accounts/interface.go1
-rw-r--r--userapi/storage/accounts/postgres/profile_table.go31
-rw-r--r--userapi/storage/accounts/postgres/storage.go7
-rw-r--r--userapi/storage/accounts/sqlite3/profile_table.go31
-rw-r--r--userapi/storage/accounts/sqlite3/storage.go7
9 files changed, 129 insertions, 5 deletions
diff --git a/userapi/api/api.go b/userapi/api/api.go
index bd0773f8..5791403f 100644
--- a/userapi/api/api.go
+++ b/userapi/api/api.go
@@ -18,6 +18,7 @@ import (
"context"
"encoding/json"
+ "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/gomatrixserverlib"
)
@@ -31,6 +32,7 @@ type UserInternalAPI interface {
QueryDevices(ctx context.Context, req *QueryDevicesRequest, res *QueryDevicesResponse) error
QueryAccountData(ctx context.Context, req *QueryAccountDataRequest, res *QueryAccountDataResponse) error
QueryDeviceInfos(ctx context.Context, req *QueryDeviceInfosRequest, res *QueryDeviceInfosResponse) error
+ QuerySearchProfiles(ctx context.Context, req *QuerySearchProfilesRequest, res *QuerySearchProfilesResponse) error
}
// InputAccountDataRequest is the request for InputAccountData
@@ -112,6 +114,20 @@ type QueryProfileResponse struct {
AvatarURL string
}
+// QuerySearchProfilesRequest is the request for QueryProfile
+type QuerySearchProfilesRequest struct {
+ // The search string to match
+ SearchString string
+ // How many results to return
+ Limit int
+}
+
+// QuerySearchProfilesResponse is the response for QuerySearchProfilesRequest
+type QuerySearchProfilesResponse struct {
+ // Profiles matching the search
+ Profiles []authtypes.Profile
+}
+
// PerformAccountCreationRequest is the request for PerformAccountCreation
type PerformAccountCreationRequest struct {
AccountType AccountType // Required: whether this is a guest or user account
diff --git a/userapi/internal/api.go b/userapi/internal/api.go
index 2de8f960..5b154196 100644
--- a/userapi/internal/api.go
+++ b/userapi/internal/api.go
@@ -125,6 +125,15 @@ func (a *UserInternalAPI) QueryProfile(ctx context.Context, req *api.QueryProfil
return nil
}
+func (a *UserInternalAPI) QuerySearchProfiles(ctx context.Context, req *api.QuerySearchProfilesRequest, res *api.QuerySearchProfilesResponse) error {
+ profiles, err := a.AccountDB.SearchProfiles(ctx, req.SearchString, req.Limit)
+ if err != nil {
+ return err
+ }
+ res.Profiles = profiles
+ return nil
+}
+
func (a *UserInternalAPI) QueryDeviceInfos(ctx context.Context, req *api.QueryDeviceInfosRequest, res *api.QueryDeviceInfosResponse) error {
devices, err := a.DeviceDB.GetDevicesByID(ctx, req.DeviceIDs)
if err != nil {
diff --git a/userapi/inthttp/client.go b/userapi/inthttp/client.go
index b2b42823..3e1ac066 100644
--- a/userapi/inthttp/client.go
+++ b/userapi/inthttp/client.go
@@ -31,11 +31,12 @@ const (
PerformDeviceCreationPath = "/userapi/performDeviceCreation"
PerformAccountCreationPath = "/userapi/performAccountCreation"
- QueryProfilePath = "/userapi/queryProfile"
- QueryAccessTokenPath = "/userapi/queryAccessToken"
- QueryDevicesPath = "/userapi/queryDevices"
- QueryAccountDataPath = "/userapi/queryAccountData"
- QueryDeviceInfosPath = "/userapi/queryDeviceInfos"
+ QueryProfilePath = "/userapi/queryProfile"
+ QueryAccessTokenPath = "/userapi/queryAccessToken"
+ QueryDevicesPath = "/userapi/queryDevices"
+ QueryAccountDataPath = "/userapi/queryAccountData"
+ QueryDeviceInfosPath = "/userapi/queryDeviceInfos"
+ QuerySearchProfilesPath = "/userapi/querySearchProfiles"
)
// NewUserAPIClient creates a UserInternalAPI implemented by talking to a HTTP POST API.
@@ -141,3 +142,11 @@ func (h *httpUserInternalAPI) QueryAccountData(ctx context.Context, req *api.Que
apiURL := h.apiURL + QueryAccountDataPath
return httputil.PostJSON(ctx, span, h.httpClient, apiURL, req, res)
}
+
+func (h *httpUserInternalAPI) QuerySearchProfiles(ctx context.Context, req *api.QuerySearchProfilesRequest, res *api.QuerySearchProfilesResponse) error {
+ span, ctx := opentracing.StartSpanFromContext(ctx, "QuerySearchProfiles")
+ defer span.Finish()
+
+ apiURL := h.apiURL + QuerySearchProfilesPath
+ return httputil.PostJSON(ctx, span, h.httpClient, apiURL, req, res)
+}
diff --git a/userapi/inthttp/server.go b/userapi/inthttp/server.go
index d8e151ad..d29f4d44 100644
--- a/userapi/inthttp/server.go
+++ b/userapi/inthttp/server.go
@@ -117,4 +117,17 @@ func AddRoutes(internalAPIMux *mux.Router, s api.UserInternalAPI) {
return util.JSONResponse{Code: http.StatusOK, JSON: &response}
}),
)
+ internalAPIMux.Handle(QueryDeviceInfosPath,
+ httputil.MakeInternalAPI("querySearchProfiles", func(req *http.Request) util.JSONResponse {
+ request := api.QuerySearchProfilesRequest{}
+ response := api.QuerySearchProfilesResponse{}
+ if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
+ return util.MessageResponse(http.StatusBadRequest, err.Error())
+ }
+ if err := s.QuerySearchProfiles(req.Context(), &request, &response); err != nil {
+ return util.ErrorResponse(err)
+ }
+ return util.JSONResponse{Code: http.StatusOK, JSON: &response}
+ }),
+ )
}
diff --git a/userapi/storage/accounts/interface.go b/userapi/storage/accounts/interface.go
index 6f6caf11..86b91e60 100644
--- a/userapi/storage/accounts/interface.go
+++ b/userapi/storage/accounts/interface.go
@@ -49,6 +49,7 @@ type Database interface {
GetThreePIDsForLocalpart(ctx context.Context, localpart string) (threepids []authtypes.ThreePID, err error)
CheckAccountAvailability(ctx context.Context, localpart string) (bool, error)
GetAccountByLocalpart(ctx context.Context, localpart string) (*api.Account, error)
+ SearchProfiles(ctx context.Context, searchString string, limit int) ([]authtypes.Profile, error)
}
// Err3PIDInUse is the error returned when trying to save an association involving
diff --git a/userapi/storage/accounts/postgres/profile_table.go b/userapi/storage/accounts/postgres/profile_table.go
index d2cbeb8e..14b12c35 100644
--- a/userapi/storage/accounts/postgres/profile_table.go
+++ b/userapi/storage/accounts/postgres/profile_table.go
@@ -17,8 +17,10 @@ package postgres
import (
"context"
"database/sql"
+ "fmt"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
+ "github.com/matrix-org/dendrite/internal"
)
const profilesSchema = `
@@ -45,11 +47,15 @@ const setAvatarURLSQL = "" +
const setDisplayNameSQL = "" +
"UPDATE account_profiles SET display_name = $1 WHERE localpart = $2"
+const selectProfilesBySearchSQL = "" +
+ "SELECT localpart, display_name, avatar_url FROM account_profiles WHERE localpart LIKE $1 OR display_name LIKE $1 LIMIT $2"
+
type profilesStatements struct {
insertProfileStmt *sql.Stmt
selectProfileByLocalpartStmt *sql.Stmt
setAvatarURLStmt *sql.Stmt
setDisplayNameStmt *sql.Stmt
+ selectProfilesBySearchStmt *sql.Stmt
}
func (s *profilesStatements) prepare(db *sql.DB) (err error) {
@@ -69,6 +75,9 @@ func (s *profilesStatements) prepare(db *sql.DB) (err error) {
if s.setDisplayNameStmt, err = db.Prepare(setDisplayNameSQL); err != nil {
return
}
+ if s.selectProfilesBySearchStmt, err = db.Prepare(selectProfilesBySearchSQL); err != nil {
+ return
+ }
return
}
@@ -105,3 +114,25 @@ func (s *profilesStatements) setDisplayName(
_, err = s.setDisplayNameStmt.ExecContext(ctx, displayName, localpart)
return
}
+
+func (s *profilesStatements) selectProfilesBySearch(
+ ctx context.Context, searchString string, limit int,
+) ([]authtypes.Profile, error) {
+ var profiles []authtypes.Profile
+ // The fmt.Sprintf directive below is building a parameter for the
+ // "LIKE" condition in the SQL query. %% escapes the % char, so the
+ // statement in the end will look like "LIKE %searchString%".
+ rows, err := s.selectProfilesBySearchStmt.QueryContext(ctx, fmt.Sprintf("%%%s%%", searchString), limit)
+ if err != nil {
+ return nil, err
+ }
+ defer internal.CloseAndLogIfError(ctx, rows, "selectProfilesBySearch: rows.close() failed")
+ for rows.Next() {
+ var profile authtypes.Profile
+ if err := rows.Scan(&profile.Localpart, &profile.DisplayName, &profile.AvatarURL); err != nil {
+ return nil, err
+ }
+ profiles = append(profiles, profile)
+ }
+ return profiles, nil
+}
diff --git a/userapi/storage/accounts/postgres/storage.go b/userapi/storage/accounts/postgres/storage.go
index c76b92f1..f56fb6d8 100644
--- a/userapi/storage/accounts/postgres/storage.go
+++ b/userapi/storage/accounts/postgres/storage.go
@@ -298,3 +298,10 @@ func (d *Database) GetAccountByLocalpart(ctx context.Context, localpart string,
) (*api.Account, error) {
return d.accounts.selectAccountByLocalpart(ctx, localpart)
}
+
+// SearchProfiles returns all profiles where the provided localpart or display name
+// match any part of the profiles in the database.
+func (d *Database) SearchProfiles(ctx context.Context, searchString string, limit int,
+) ([]authtypes.Profile, error) {
+ return d.profiles.selectProfilesBySearch(ctx, searchString, limit)
+}
diff --git a/userapi/storage/accounts/sqlite3/profile_table.go b/userapi/storage/accounts/sqlite3/profile_table.go
index 68cea516..d4c404ca 100644
--- a/userapi/storage/accounts/sqlite3/profile_table.go
+++ b/userapi/storage/accounts/sqlite3/profile_table.go
@@ -17,8 +17,10 @@ package sqlite3
import (
"context"
"database/sql"
+ "fmt"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
+ "github.com/matrix-org/dendrite/internal"
"github.com/matrix-org/dendrite/internal/sqlutil"
)
@@ -46,6 +48,9 @@ const setAvatarURLSQL = "" +
const setDisplayNameSQL = "" +
"UPDATE account_profiles SET display_name = $1 WHERE localpart = $2"
+const selectProfilesBySearchSQL = "" +
+ "SELECT localpart, display_name, avatar_url FROM account_profiles WHERE localpart LIKE $1 OR display_name LIKE $1 LIMIT $2"
+
type profilesStatements struct {
db *sql.DB
writer *sqlutil.TransactionWriter
@@ -53,6 +58,7 @@ type profilesStatements struct {
selectProfileByLocalpartStmt *sql.Stmt
setAvatarURLStmt *sql.Stmt
setDisplayNameStmt *sql.Stmt
+ selectProfilesBySearchStmt *sql.Stmt
}
func (s *profilesStatements) prepare(db *sql.DB) (err error) {
@@ -74,6 +80,9 @@ func (s *profilesStatements) prepare(db *sql.DB) (err error) {
if s.setDisplayNameStmt, err = db.Prepare(setDisplayNameSQL); err != nil {
return
}
+ if s.selectProfilesBySearchStmt, err = db.Prepare(selectProfilesBySearchSQL); err != nil {
+ return
+ }
return
}
@@ -112,3 +121,25 @@ func (s *profilesStatements) setDisplayName(
_, err = s.setDisplayNameStmt.ExecContext(ctx, displayName, localpart)
return
}
+
+func (s *profilesStatements) selectProfilesBySearch(
+ ctx context.Context, searchString string, limit int,
+) ([]authtypes.Profile, error) {
+ var profiles []authtypes.Profile
+ // The fmt.Sprintf directive below is building a parameter for the
+ // "LIKE" condition in the SQL query. %% escapes the % char, so the
+ // statement in the end will look like "LIKE %searchString%".
+ rows, err := s.selectProfilesBySearchStmt.QueryContext(ctx, fmt.Sprintf("%%%s%%", searchString), limit)
+ if err != nil {
+ return nil, err
+ }
+ defer internal.CloseAndLogIfError(ctx, rows, "selectProfilesBySearch: rows.close() failed")
+ for rows.Next() {
+ var profile authtypes.Profile
+ if err := rows.Scan(&profile.Localpart, &profile.DisplayName, &profile.AvatarURL); err != nil {
+ return nil, err
+ }
+ profiles = append(profiles, profile)
+ }
+ return profiles, nil
+}
diff --git a/userapi/storage/accounts/sqlite3/storage.go b/userapi/storage/accounts/sqlite3/storage.go
index 2d09090f..72239014 100644
--- a/userapi/storage/accounts/sqlite3/storage.go
+++ b/userapi/storage/accounts/sqlite3/storage.go
@@ -343,3 +343,10 @@ func (d *Database) GetAccountByLocalpart(ctx context.Context, localpart string,
) (*api.Account, error) {
return d.accounts.selectAccountByLocalpart(ctx, localpart)
}
+
+// SearchProfiles returns all profiles where the provided localpart or display name
+// match any part of the profiles in the database.
+func (d *Database) SearchProfiles(ctx context.Context, searchString string, limit int,
+) ([]authtypes.Profile, error) {
+ return d.profiles.selectProfilesBySearch(ctx, searchString, limit)
+}