mirror of
https://github.com/matrix-org/dendrite.git
synced 2025-12-16 11:23:11 -06:00
Implement user directory for local users #649
Signed-off-by: Anton Stuetz <opensource@ti-zero.com>
This commit is contained in:
parent
145921f207
commit
9441f0863e
|
|
@ -18,6 +18,12 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/userutil"
|
||||||
|
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
|
||||||
|
searchtypes "github.com/matrix-org/dendrite/userdirectoryapi/types"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -39,6 +45,9 @@ const insertProfileSQL = "" +
|
||||||
const selectProfileByLocalpartSQL = "" +
|
const selectProfileByLocalpartSQL = "" +
|
||||||
"SELECT localpart, display_name, avatar_url FROM account_profiles WHERE localpart = $1"
|
"SELECT localpart, display_name, avatar_url FROM account_profiles WHERE localpart = $1"
|
||||||
|
|
||||||
|
const selectSearchTermLikeLocalpartOrDisplayName = "" +
|
||||||
|
"SELECT localpart, display_name, avatar_url FROM account_profiles WHERE localpart LIKE $1 OR display_name LIKE $1 LIMIT $2"
|
||||||
|
|
||||||
const setAvatarURLSQL = "" +
|
const setAvatarURLSQL = "" +
|
||||||
"UPDATE account_profiles SET avatar_url = $1 WHERE localpart = $2"
|
"UPDATE account_profiles SET avatar_url = $1 WHERE localpart = $2"
|
||||||
|
|
||||||
|
|
@ -46,10 +55,11 @@ const setDisplayNameSQL = "" +
|
||||||
"UPDATE account_profiles SET display_name = $1 WHERE localpart = $2"
|
"UPDATE account_profiles SET display_name = $1 WHERE localpart = $2"
|
||||||
|
|
||||||
type profilesStatements struct {
|
type profilesStatements struct {
|
||||||
insertProfileStmt *sql.Stmt
|
insertProfileStmt *sql.Stmt
|
||||||
selectProfileByLocalpartStmt *sql.Stmt
|
selectProfileByLocalpartStmt *sql.Stmt
|
||||||
setAvatarURLStmt *sql.Stmt
|
selectSearchTermLikeLocalpartOrDisplayNameStmt *sql.Stmt
|
||||||
setDisplayNameStmt *sql.Stmt
|
setAvatarURLStmt *sql.Stmt
|
||||||
|
setDisplayNameStmt *sql.Stmt
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *profilesStatements) prepare(db *sql.DB) (err error) {
|
func (s *profilesStatements) prepare(db *sql.DB) (err error) {
|
||||||
|
|
@ -69,6 +79,9 @@ func (s *profilesStatements) prepare(db *sql.DB) (err error) {
|
||||||
if s.setDisplayNameStmt, err = db.Prepare(setDisplayNameSQL); err != nil {
|
if s.setDisplayNameStmt, err = db.Prepare(setDisplayNameSQL); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if s.selectSearchTermLikeLocalpartOrDisplayNameStmt, err = db.Prepare(selectSearchTermLikeLocalpartOrDisplayName); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,6 +105,37 @@ func (s *profilesStatements) selectProfileByLocalpart(
|
||||||
return &profile, nil
|
return &profile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *profilesStatements) selectSearchTermLikeLocalpartOrDisplayName(
|
||||||
|
ctx context.Context, searchTerm string, limit int8, serverName gomatrixserverlib.ServerName,
|
||||||
|
) (*[]searchtypes.SearchResult, bool, error) {
|
||||||
|
//increase limit by one to find out if the query was limited
|
||||||
|
rows, err := s.selectSearchTermLikeLocalpartOrDisplayNameStmt.QueryContext(ctx, searchTerm, limit+1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
searchResults := []searchtypes.SearchResult{}
|
||||||
|
counter := int8(1)
|
||||||
|
limited := false
|
||||||
|
for rows.Next() {
|
||||||
|
if counter > limit {
|
||||||
|
limited = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
var r searchtypes.SearchResult
|
||||||
|
err = rows.Scan(
|
||||||
|
&r.UserId, &r.DisplayName, &r.AvatarUrl,
|
||||||
|
)
|
||||||
|
r.UserId = userutil.MakeUserID(r.UserId, serverName)
|
||||||
|
if err != nil {
|
||||||
|
return &searchResults, false, err
|
||||||
|
}
|
||||||
|
searchResults = append(searchResults, r)
|
||||||
|
counter++
|
||||||
|
}
|
||||||
|
|
||||||
|
return &searchResults, limited, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *profilesStatements) setAvatarURL(
|
func (s *profilesStatements) setAvatarURL(
|
||||||
ctx context.Context, localpart string, avatarURL string,
|
ctx context.Context, localpart string, avatarURL string,
|
||||||
) (err error) {
|
) (err error) {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
searchtypes "github.com/matrix-org/dendrite/userdirectoryapi/types"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/dendrite/common"
|
"github.com/matrix-org/dendrite/common"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
|
@ -28,6 +30,69 @@ import (
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type AccountDatabase interface {
|
||||||
|
GetAccountByPassword(
|
||||||
|
ctx context.Context, localpart, plaintextPassword string,
|
||||||
|
) (*authtypes.Account, error)
|
||||||
|
GetProfileByLocalpart(
|
||||||
|
ctx context.Context, localpart string,
|
||||||
|
) (*authtypes.Profile, error)
|
||||||
|
SetAvatarURL(
|
||||||
|
ctx context.Context, localpart string, avatarURL string,
|
||||||
|
) error
|
||||||
|
SetDisplayName(
|
||||||
|
ctx context.Context, localpart string, displayName string,
|
||||||
|
) error
|
||||||
|
CreateAccount(
|
||||||
|
ctx context.Context, localpart, plaintextPassword, appserviceID string,
|
||||||
|
) (*authtypes.Account, error)
|
||||||
|
UpdateMemberships(
|
||||||
|
ctx context.Context, eventsToAdd []gomatrixserverlib.Event, idsToRemove []string,
|
||||||
|
) error
|
||||||
|
GetMembershipInRoomByLocalpart(
|
||||||
|
ctx context.Context, localpart, roomID string,
|
||||||
|
) (authtypes.Membership, error)
|
||||||
|
GetMembershipsByLocalpart(
|
||||||
|
ctx context.Context, localpart string,
|
||||||
|
) (memberships []authtypes.Membership, err error)
|
||||||
|
SaveAccountData(
|
||||||
|
ctx context.Context, localpart, roomID, dataType, content string,
|
||||||
|
) error
|
||||||
|
GetAccountData(ctx context.Context, localpart string) (
|
||||||
|
global []gomatrixserverlib.ClientEvent,
|
||||||
|
rooms map[string][]gomatrixserverlib.ClientEvent,
|
||||||
|
err error,
|
||||||
|
)
|
||||||
|
GetAccountDataByType(
|
||||||
|
ctx context.Context, localpart, roomID, dataType string,
|
||||||
|
) (data *gomatrixserverlib.ClientEvent, err error)
|
||||||
|
GetNewNumericLocalpart(
|
||||||
|
ctx context.Context,
|
||||||
|
) (int64, error)
|
||||||
|
SaveThreePIDAssociation(
|
||||||
|
ctx context.Context, threepid, localpart, medium string,
|
||||||
|
) (err error)
|
||||||
|
RemoveThreePIDAssociation(
|
||||||
|
ctx context.Context, threepid string, medium string,
|
||||||
|
) (err error)
|
||||||
|
GetLocalpartForThreePID(
|
||||||
|
ctx context.Context, threepid string, medium string,
|
||||||
|
) (localpart string, err error)
|
||||||
|
GetThreePIDsForLocalpart(
|
||||||
|
ctx context.Context, localpart string,
|
||||||
|
) (threepids []authtypes.ThreePID, err error)
|
||||||
|
GetFilter(
|
||||||
|
ctx context.Context, localpart string, filterID string,
|
||||||
|
) (*gomatrixserverlib.Filter, error)
|
||||||
|
PutFilter(
|
||||||
|
ctx context.Context, localpart string, filter *gomatrixserverlib.Filter,
|
||||||
|
) (string, error)
|
||||||
|
CheckAccountAvailability(ctx context.Context, localpart string) (bool, error)
|
||||||
|
GetAccountByLocalpart(ctx context.Context, localpart string,
|
||||||
|
) (*authtypes.Account, error)
|
||||||
|
SearchUserIdAndDisplayName(ctx context.Context, searchTerm string, limit int8) (*[]searchtypes.SearchResult, bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
// Database represents an account database
|
// Database represents an account database
|
||||||
type Database struct {
|
type Database struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
|
|
@ -379,3 +444,7 @@ func (d *Database) GetAccountByLocalpart(ctx context.Context, localpart string,
|
||||||
) (*authtypes.Account, error) {
|
) (*authtypes.Account, error) {
|
||||||
return d.accounts.selectAccountByLocalpart(ctx, localpart)
|
return d.accounts.selectAccountByLocalpart(ctx, localpart)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Database) SearchUserIdAndDisplayName(ctx context.Context, searchTerm string, limit int8) (*[]searchtypes.SearchResult, bool, error) {
|
||||||
|
return d.profiles.selectSearchTermLikeLocalpartOrDisplayName(ctx, searchTerm, limit, d.serverName)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -517,7 +517,6 @@ func handleRegistrationFlow(
|
||||||
deviceDB *devices.Database,
|
deviceDB *devices.Database,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
// TODO: Shared secret registration (create new user scripts)
|
// TODO: Shared secret registration (create new user scripts)
|
||||||
// TODO: Enable registration config flag
|
|
||||||
// TODO: Guest account upgrading
|
// TODO: Guest account upgrading
|
||||||
|
|
||||||
// TODO: Handle loading of previous session parameters from database.
|
// TODO: Handle loading of previous session parameters from database.
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ import (
|
||||||
"flag"
|
"flag"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/userdirectoryapi"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/appservice"
|
"github.com/matrix-org/dendrite/appservice"
|
||||||
"github.com/matrix-org/dendrite/clientapi"
|
"github.com/matrix-org/dendrite/clientapi"
|
||||||
"github.com/matrix-org/dendrite/common"
|
"github.com/matrix-org/dendrite/common"
|
||||||
|
|
@ -71,6 +73,7 @@ func main() {
|
||||||
mediaapi.SetupMediaAPIComponent(base, deviceDB)
|
mediaapi.SetupMediaAPIComponent(base, deviceDB)
|
||||||
publicroomsapi.SetupPublicRoomsAPIComponent(base, deviceDB)
|
publicroomsapi.SetupPublicRoomsAPIComponent(base, deviceDB)
|
||||||
syncapi.SetupSyncAPIComponent(base, deviceDB, accountDB, query)
|
syncapi.SetupSyncAPIComponent(base, deviceDB, accountDB, query)
|
||||||
|
userdirectoryapi.SetupUserDirectoryApi(base, accountDB, deviceDB)
|
||||||
|
|
||||||
httpHandler := common.WrapHandlerInCORS(base.APIMux)
|
httpHandler := common.WrapHandlerInCORS(base.APIMux)
|
||||||
|
|
||||||
|
|
|
||||||
34
cmd/dendrite-userdirectory-api-server/main.go
Normal file
34
cmd/dendrite-userdirectory-api-server/main.go
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
// Copyright 2017 Vector Creations Ltd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/matrix-org/dendrite/common/basecomponent"
|
||||||
|
"github.com/matrix-org/dendrite/userdirectoryapi"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := basecomponent.ParseFlags()
|
||||||
|
base := basecomponent.NewBaseDendrite(cfg, "PublicRoomsAPI")
|
||||||
|
defer base.Close() // nolint: errcheck
|
||||||
|
|
||||||
|
deviceDB := base.CreateDeviceDB()
|
||||||
|
accountDB := base.CreateAccountsDB()
|
||||||
|
|
||||||
|
userdirectoryapi.SetupUserDirectoryApi(base, accountDB, deviceDB)
|
||||||
|
|
||||||
|
base.SetupAndServeHTTP(string(base.Cfg.Bind.UserDirectoryAPI), string(base.Cfg.Listen.UserDirectoryAPI))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -30,7 +30,7 @@ import (
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/crypto/ed25519"
|
"golang.org/x/crypto/ed25519"
|
||||||
yaml "gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
|
|
||||||
jaegerconfig "github.com/uber/jaeger-client-go/config"
|
jaegerconfig "github.com/uber/jaeger-client-go/config"
|
||||||
jaegermetrics "github.com/uber/jaeger-lib/metrics"
|
jaegermetrics "github.com/uber/jaeger-lib/metrics"
|
||||||
|
|
@ -206,6 +206,7 @@ type Dendrite struct {
|
||||||
RoomServer Address `yaml:"room_server"`
|
RoomServer Address `yaml:"room_server"`
|
||||||
FederationSender Address `yaml:"federation_sender"`
|
FederationSender Address `yaml:"federation_sender"`
|
||||||
PublicRoomsAPI Address `yaml:"public_rooms_api"`
|
PublicRoomsAPI Address `yaml:"public_rooms_api"`
|
||||||
|
UserDirectoryAPI Address `yaml:"user_directory_api"`
|
||||||
TypingServer Address `yaml:"typing_server"`
|
TypingServer Address `yaml:"typing_server"`
|
||||||
} `yaml:"bind"`
|
} `yaml:"bind"`
|
||||||
|
|
||||||
|
|
@ -219,6 +220,7 @@ type Dendrite struct {
|
||||||
RoomServer Address `yaml:"room_server"`
|
RoomServer Address `yaml:"room_server"`
|
||||||
FederationSender Address `yaml:"federation_sender"`
|
FederationSender Address `yaml:"federation_sender"`
|
||||||
PublicRoomsAPI Address `yaml:"public_rooms_api"`
|
PublicRoomsAPI Address `yaml:"public_rooms_api"`
|
||||||
|
UserDirectoryAPI Address `yaml:"user_directory_api"`
|
||||||
TypingServer Address `yaml:"typing_server"`
|
TypingServer Address `yaml:"typing_server"`
|
||||||
} `yaml:"listen"`
|
} `yaml:"listen"`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ listen:
|
||||||
media_api: "localhost:7774"
|
media_api: "localhost:7774"
|
||||||
appservice_api: "localhost:7777"
|
appservice_api: "localhost:7777"
|
||||||
typing_server: "localhost:7778"
|
typing_server: "localhost:7778"
|
||||||
|
user_directory_api: "localhost:7779"
|
||||||
logging:
|
logging:
|
||||||
- type: "file"
|
- type: "file"
|
||||||
level: "info"
|
level: "info"
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,7 @@ listen:
|
||||||
federation_sender: "localhost:7776"
|
federation_sender: "localhost:7776"
|
||||||
appservice_api: "localhost:7777"
|
appservice_api: "localhost:7777"
|
||||||
typing_server: "localhost:7778"
|
typing_server: "localhost:7778"
|
||||||
|
user_directory_api: "localhost:7779"
|
||||||
|
|
||||||
# The configuration for tracing the dendrite components.
|
# The configuration for tracing the dendrite components.
|
||||||
tracing:
|
tracing:
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,7 @@ listen:
|
||||||
public_rooms_api: "public_rooms_api:7775"
|
public_rooms_api: "public_rooms_api:7775"
|
||||||
federation_sender: "federation_sender:7776"
|
federation_sender: "federation_sender:7776"
|
||||||
typing_server: "typing_server:7777"
|
typing_server: "typing_server:7777"
|
||||||
|
user_directory_api: "localhost:7779"
|
||||||
|
|
||||||
# The configuration for tracing the dendrite components.
|
# The configuration for tracing the dendrite components.
|
||||||
tracing:
|
tracing:
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,18 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
|
|
||||||
|
userdirectory_api:
|
||||||
|
container_name: dendrite_userdirectory_api
|
||||||
|
hostname: userdirectory_api
|
||||||
|
entrypoint: ["bash", "./docker/services/userdirectory-api.sh"]
|
||||||
|
build: ./
|
||||||
|
volumes:
|
||||||
|
- ..:/build
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
federation_sender:
|
federation_sender:
|
||||||
container_name: dendrite_federation_sender
|
container_name: dendrite_federation_sender
|
||||||
hostname: federation_sender
|
hostname: federation_sender
|
||||||
|
|
|
||||||
5
docker/services/userdirectory-api.sh
Normal file
5
docker/services/userdirectory-api.sh
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
bash ./docker/build.sh
|
||||||
|
|
||||||
|
./bin/dendrite-userdirectory-api-server --config dendrite.yaml
|
||||||
5
userdirectoryapi/README.md
Normal file
5
userdirectoryapi/README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# User Directory API
|
||||||
|
|
||||||
|
This server is responsible for serving requests hitting `/user_directory` as per:
|
||||||
|
|
||||||
|
https://matrix.org/docs/spec/client_server/r0.5.0#post-matrix-client-r0-user-directory-search
|
||||||
50
userdirectoryapi/routing/routing.go
Normal file
50
userdirectoryapi/routing/routing.go
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
// Copyright 2019 Anton Stuetz
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package routing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/userdirectoryapi/search"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth/storage/devices"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
|
"github.com/matrix-org/dendrite/common"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
|
||||||
|
)
|
||||||
|
|
||||||
|
const pathPrefixR0 = "/_matrix/client/r0"
|
||||||
|
|
||||||
|
func Setup(apiMux *mux.Router, accountDB *accounts.Database, deviceDB *devices.Database) {
|
||||||
|
|
||||||
|
r0mux := apiMux.PathPrefix(pathPrefixR0).Subrouter()
|
||||||
|
|
||||||
|
authData := auth.Data{
|
||||||
|
AccountDB: nil,
|
||||||
|
DeviceDB: deviceDB,
|
||||||
|
AppServices: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
r0mux.Handle("/user_directory/search",
|
||||||
|
common.MakeAuthAPI("user_directory_search", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
|
||||||
|
return search.Search(req, accountDB)
|
||||||
|
})).Methods(http.MethodPost)
|
||||||
|
|
||||||
|
}
|
||||||
80
userdirectoryapi/search/search.go
Normal file
80
userdirectoryapi/search/search.go
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
// Copyright 2019 Anton Stuetz
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package search
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
|
searchtypes "github.com/matrix-org/dendrite/userdirectoryapi/types"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DEFAULT_LIMIT = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
type userSearchRequest struct {
|
||||||
|
SearchTerm string `json:"search_term"`
|
||||||
|
Limit int8 `json:"limit"`
|
||||||
|
}
|
||||||
|
type userSearchResponse struct {
|
||||||
|
Results *[]searchtypes.SearchResult `json:"results"`
|
||||||
|
Limited bool `json:"limited"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Search(req *http.Request, accountDB accounts.AccountDatabase) util.JSONResponse {
|
||||||
|
var r userSearchRequest
|
||||||
|
resErr := httputil.UnmarshalJSONRequest(req, &r)
|
||||||
|
if resErr != nil {
|
||||||
|
return *resErr
|
||||||
|
}
|
||||||
|
|
||||||
|
err := validateSearchTerm(r.SearchTerm)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.InvalidArgumentValue(err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.Limit == 0 {
|
||||||
|
r.Limit = DEFAULT_LIMIT
|
||||||
|
}
|
||||||
|
results, limited, err := accountDB.SearchUserIdAndDisplayName(req.Context(), r.SearchTerm+"%", r.Limit)
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
return mapProfilesToResponse(results, limited)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateSearchTerm(search_term string) error {
|
||||||
|
if len(search_term) == 0 {
|
||||||
|
return errors.New("Search term is empty")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapProfilesToResponse(results *[]searchtypes.SearchResult, limited bool) util.JSONResponse {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusOK,
|
||||||
|
JSON: userSearchResponse{results, limited},
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
224
userdirectoryapi/search/search_test.go
Normal file
224
userdirectoryapi/search/search_test.go
Normal file
|
|
@ -0,0 +1,224 @@
|
||||||
|
package search
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
|
searchtypes "github.com/matrix-org/dendrite/userdirectoryapi/types"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockAccountDatabase struct {
|
||||||
|
limited bool
|
||||||
|
wantError bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MockAccountDatabase) GetAccountByPassword(
|
||||||
|
ctx context.Context, localpart, plaintextPassword string,
|
||||||
|
) (*authtypes.Account, error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MockAccountDatabase) GetProfileByLocalpart(
|
||||||
|
ctx context.Context, localpart string,
|
||||||
|
) (*authtypes.Profile, error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MockAccountDatabase) SetAvatarURL(
|
||||||
|
ctx context.Context, localpart string, avatarURL string,
|
||||||
|
) error {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MockAccountDatabase) SetDisplayName(
|
||||||
|
ctx context.Context, localpart string, displayName string,
|
||||||
|
) error {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MockAccountDatabase) CreateAccount(
|
||||||
|
ctx context.Context, localpart, plaintextPassword, appserviceID string,
|
||||||
|
) (*authtypes.Account, error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MockAccountDatabase) UpdateMemberships(
|
||||||
|
ctx context.Context, eventsToAdd []gomatrixserverlib.Event, idsToRemove []string,
|
||||||
|
) error {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MockAccountDatabase) GetMembershipInRoomByLocalpart(
|
||||||
|
ctx context.Context, localpart, roomID string,
|
||||||
|
) (authtypes.Membership, error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MockAccountDatabase) GetMembershipsByLocalpart(
|
||||||
|
ctx context.Context, localpart string,
|
||||||
|
) (memberships []authtypes.Membership, err error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MockAccountDatabase) SaveAccountData(
|
||||||
|
ctx context.Context, localpart, roomID, dataType, content string,
|
||||||
|
) error {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MockAccountDatabase) GetAccountData(ctx context.Context, localpart string) (
|
||||||
|
global []gomatrixserverlib.ClientEvent,
|
||||||
|
rooms map[string][]gomatrixserverlib.ClientEvent,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MockAccountDatabase) GetAccountDataByType(
|
||||||
|
ctx context.Context, localpart, roomID, dataType string,
|
||||||
|
) (data *gomatrixserverlib.ClientEvent, err error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MockAccountDatabase) GetNewNumericLocalpart(
|
||||||
|
ctx context.Context,
|
||||||
|
) (int64, error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MockAccountDatabase) SaveThreePIDAssociation(
|
||||||
|
ctx context.Context, threepid, localpart, medium string,
|
||||||
|
) (err error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MockAccountDatabase) RemoveThreePIDAssociation(
|
||||||
|
ctx context.Context, threepid string, medium string,
|
||||||
|
) (err error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MockAccountDatabase) GetLocalpartForThreePID(
|
||||||
|
ctx context.Context, threepid string, medium string,
|
||||||
|
) (localpart string, err error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MockAccountDatabase) GetThreePIDsForLocalpart(
|
||||||
|
ctx context.Context, localpart string,
|
||||||
|
) (threepids []authtypes.ThreePID, err error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MockAccountDatabase) GetFilter(
|
||||||
|
ctx context.Context, localpart string, filterID string,
|
||||||
|
) (*gomatrixserverlib.Filter, error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MockAccountDatabase) PutFilter(
|
||||||
|
ctx context.Context, localpart string, filter *gomatrixserverlib.Filter,
|
||||||
|
) (string, error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MockAccountDatabase) CheckAccountAvailability(ctx context.Context, localpart string) (bool, error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MockAccountDatabase) GetAccountByLocalpart(ctx context.Context, localpart string,
|
||||||
|
) (*authtypes.Account, error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MockAccountDatabase) SearchUserIdAndDisplayName(ctx context.Context, searchTerm string, limit int8) (*[]searchtypes.SearchResult, bool, error) {
|
||||||
|
var searchResults []searchtypes.SearchResult
|
||||||
|
searchResults = append(searchResults, searchtypes.SearchResult{UserId: "alice:localhost", AvatarUrl: "", DisplayName: ""})
|
||||||
|
if d.wantError {
|
||||||
|
return nil, false, fmt.Errorf("")
|
||||||
|
} else {
|
||||||
|
return &searchResults, d.limited, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSearch(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
name string
|
||||||
|
wantError bool
|
||||||
|
errorCode int
|
||||||
|
results []string
|
||||||
|
limited bool
|
||||||
|
requestBody string
|
||||||
|
}
|
||||||
|
tt := []testCase{
|
||||||
|
{
|
||||||
|
"Find user do not hit limit",
|
||||||
|
false,
|
||||||
|
0,
|
||||||
|
[]string{"alice:localhost"},
|
||||||
|
false,
|
||||||
|
"{ \"search_term\": \"alice\" }",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Find user do hit limit",
|
||||||
|
false,
|
||||||
|
0,
|
||||||
|
[]string{"alice:localhost"},
|
||||||
|
true,
|
||||||
|
"{ \"search_term\": \"alice\" }",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Find user and fail",
|
||||||
|
true,
|
||||||
|
500,
|
||||||
|
[]string{"alice:localhost"},
|
||||||
|
false,
|
||||||
|
"{ \"search_term\": \"alice\" }",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Find user and fail to parse request",
|
||||||
|
true,
|
||||||
|
400,
|
||||||
|
[]string{"alice:localhost"},
|
||||||
|
false,
|
||||||
|
"{ \"search_term\": \"alicINVALID }",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
setupAccountDb := func(limited bool, wantError bool) accounts.AccountDatabase {
|
||||||
|
return &MockAccountDatabase{limited, wantError}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tt {
|
||||||
|
fmt.Printf("Executing %s", tc.name)
|
||||||
|
requestBody := ioutil.NopCloser(strings.NewReader(tc.requestBody))
|
||||||
|
req := &http.Request{Body: requestBody}
|
||||||
|
searchResults := Search(req, setupAccountDb(tc.limited, tc.wantError))
|
||||||
|
if tc.wantError {
|
||||||
|
assert.EqualValues(t, searchResults.Code, tc.errorCode)
|
||||||
|
} else {
|
||||||
|
response := searchResults.JSON.(userSearchResponse)
|
||||||
|
for _, result := range tc.results {
|
||||||
|
exists := false
|
||||||
|
for _, item := range *response.Results {
|
||||||
|
if item.UserId == result {
|
||||||
|
exists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.EqualValues(t, true, exists, "Expected value not found in response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
21
userdirectoryapi/types/searchtypes.go
Normal file
21
userdirectoryapi/types/searchtypes.go
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
// Copyright 2019 Anton Stuetz
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package searchtypes
|
||||||
|
|
||||||
|
type SearchResult struct {
|
||||||
|
UserId string `json:"user_id"`
|
||||||
|
DisplayName string `json:"display_name,omitempty"`
|
||||||
|
AvatarUrl string `json:"avatar_url,omitempty"`
|
||||||
|
}
|
||||||
28
userdirectoryapi/userdirectoryapi.go
Normal file
28
userdirectoryapi/userdirectoryapi.go
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
// Copyright 2019 Anton Stuetz
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package userdirectoryapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth/storage/devices"
|
||||||
|
"github.com/matrix-org/dendrite/common/basecomponent"
|
||||||
|
"github.com/matrix-org/dendrite/userdirectoryapi/routing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetupUserDirectoryApi(base *basecomponent.BaseDendrite, accountDB *accounts.Database, deviceDB *devices.Database) {
|
||||||
|
|
||||||
|
routing.Setup(base.APIMux, accountDB, deviceDB)
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue