mirror of
https://github.com/hoernschen/dendrite.git
synced 2025-07-30 04:52:46 +00:00
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
This commit is contained in:
parent
c632867135
commit
acc8e80a51
23 changed files with 402 additions and 8 deletions
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue