From b15ce900abe2309c2d8ce73c1be9254bc772490c Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 22 Aug 2017 11:12:51 +0100 Subject: [PATCH] Implement public rooms APIs (#185) * Move events contents to common * Basic database structure * Complete database update * Support visibility update and retrieval * Add HTTP methods for visibility update and retrieval * Add the database for the new component * Add a listener for the new component * Fix attribute update statements * Create public rooms component * Fix failing test * Add roomserver consumer * Fix a bug in aliases creation * Add a check on type * Implement public rooms directory * Use auth API for visibility update * Support filtering * Add component to monolith * Various fixes * Fix computation of next public rooms batch * Retrieve state events from the roomserver query API + avoid dupes on join * Split update of string or boolean attribute in two separate functions * Use event type to detect duplicate joins * Improve the joined members counter computation * Use event.RoomID() --- dendrite-config.yaml | 2 + .../dendrite/clientapi/readers/profile.go | 3 +- .../dendrite/clientapi/routing/routing.go | 14 - .../dendrite/clientapi/writers/createroom.go | 18 +- .../dendrite/clientapi/writers/membership.go | 3 +- .../dendrite/cmd/client-api-proxy/main.go | 33 ++- .../cmd/dendrite-monolith-server/main.go | 18 ++ .../dendrite-public-rooms-api-server/main.go | 85 ++++++ .../dendrite/common/config/config.go | 4 + .../events => common}/eventcontent.go | 28 +- .../matrix-org/dendrite/common/test/config.go | 2 + .../matrix-org/dendrite/common/test/server.go | 1 + .../dendrite/publicroomsapi/README.md | 5 + .../publicroomsapi/consumers/roomserver.go | 101 +++++++ .../publicroomsapi/directory/directory.go | 73 +++++ .../publicroomsapi/directory/public_rooms.go | 113 ++++++++ .../publicroomsapi/routing/routing.go | 51 ++++ .../publicroomsapi/storage/prepare.go | 35 +++ .../storage/public_rooms_table.go | 268 ++++++++++++++++++ .../publicroomsapi/storage/storage.go | 247 ++++++++++++++++ .../dendrite/publicroomsapi/types/types.go | 28 ++ .../roomserver/storage/room_aliases_table.go | 2 +- 22 files changed, 1098 insertions(+), 36 deletions(-) create mode 100644 src/github.com/matrix-org/dendrite/cmd/dendrite-public-rooms-api-server/main.go rename src/github.com/matrix-org/dendrite/{clientapi/events => common}/eventcontent.go (77%) create mode 100644 src/github.com/matrix-org/dendrite/publicroomsapi/README.md create mode 100644 src/github.com/matrix-org/dendrite/publicroomsapi/consumers/roomserver.go create mode 100644 src/github.com/matrix-org/dendrite/publicroomsapi/directory/directory.go create mode 100644 src/github.com/matrix-org/dendrite/publicroomsapi/directory/public_rooms.go create mode 100644 src/github.com/matrix-org/dendrite/publicroomsapi/routing/routing.go create mode 100644 src/github.com/matrix-org/dendrite/publicroomsapi/storage/prepare.go create mode 100644 src/github.com/matrix-org/dendrite/publicroomsapi/storage/public_rooms_table.go create mode 100644 src/github.com/matrix-org/dendrite/publicroomsapi/storage/storage.go create mode 100644 src/github.com/matrix-org/dendrite/publicroomsapi/types/types.go diff --git a/dendrite-config.yaml b/dendrite-config.yaml index 8275ac4e0..a91429c22 100644 --- a/dendrite-config.yaml +++ b/dendrite-config.yaml @@ -72,6 +72,7 @@ database: room_server: "postgres://dendrite:itsasecret@localhost/dendrite_roomserver?sslmode=disable" server_key: "postgres://dendrite:itsasecret@localhost/dendrite_serverkey?sslmode=disable" federation_sender: "postgres://dendrite:itsasecret@localhost/dendrite_federationsender?sslmode=disable" + public_rooms_api: "postgres://dendrite:itsasecret@localhost/dendrite_publicroomsapi?sslmode=disable" # The TCP host:port pairs to bind the internal HTTP APIs to. # These shouldn't be exposed to the public internet. @@ -82,3 +83,4 @@ listen: federation_api: "localhost:7772" sync_api: "localhost:7773" media_api: "localhost:7774" + public_rooms_api: "localhost:7775" diff --git a/src/github.com/matrix-org/dendrite/clientapi/readers/profile.go b/src/github.com/matrix-org/dendrite/clientapi/readers/profile.go index 15ca4961b..069fb1c2f 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/readers/profile.go +++ b/src/github.com/matrix-org/dendrite/clientapi/readers/profile.go @@ -23,6 +23,7 @@ import ( "github.com/matrix-org/dendrite/clientapi/httputil" "github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/clientapi/producers" + "github.com/matrix-org/dendrite/common" "github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/gomatrixserverlib" @@ -271,7 +272,7 @@ func buildMembershipEvents( StateKey: &userID, } - content := events.MemberContent{ + content := common.MemberContent{ Membership: "join", } diff --git a/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go b/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go index 8a5799c0e..32301c784 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go +++ b/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go @@ -265,20 +265,6 @@ func Setup( }), ) - r0mux.Handle("/publicRooms", - common.MakeAPI("public_rooms", func(req *http.Request) util.JSONResponse { - // TODO: Return a list of public rooms - return util.JSONResponse{ - Code: 200, - JSON: struct { - Chunk []struct{} `json:"chunk"` - Start string `json:"start"` - End string `json:"end"` - }{[]struct{}{}, "", ""}, - } - }), - ) - unstableMux.Handle("/thirdparty/protocols", common.MakeAPI("thirdparty_protocols", func(req *http.Request) util.JSONResponse { // TODO: Return the third party protcols diff --git a/src/github.com/matrix-org/dendrite/clientapi/writers/createroom.go b/src/github.com/matrix-org/dendrite/clientapi/writers/createroom.go index e43ae780d..58826c2d9 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/writers/createroom.go +++ b/src/github.com/matrix-org/dendrite/clientapi/writers/createroom.go @@ -24,10 +24,10 @@ import ( log "github.com/Sirupsen/logrus" "github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts" - "github.com/matrix-org/dendrite/clientapi/events" "github.com/matrix-org/dendrite/clientapi/httputil" "github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/clientapi/producers" + "github.com/matrix-org/dendrite/common" "github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/util" @@ -132,7 +132,7 @@ func createRoom(req *http.Request, device *authtypes.Device, return httputil.LogThenError(req, err) } - membershipContent := events.MemberContent{ + membershipContent := common.MemberContent{ Membership: "join", DisplayName: profile.DisplayName, AvatarURL: profile.AvatarURL, @@ -159,16 +159,16 @@ func createRoom(req *http.Request, device *authtypes.Device, // harder to reason about, hence sticking to a strict static ordering. // TODO: Synapse has txn/token ID on each event. Do we need to do this here? eventsToMake := []fledglingEvent{ - {"m.room.create", "", events.CreateContent{Creator: userID}}, + {"m.room.create", "", common.CreateContent{Creator: userID}}, {"m.room.member", userID, membershipContent}, - {"m.room.power_levels", "", events.InitialPowerLevelsContent(userID)}, + {"m.room.power_levels", "", common.InitialPowerLevelsContent(userID)}, // TODO: m.room.canonical_alias - {"m.room.join_rules", "", events.JoinRulesContent{"public"}}, // FIXME: Allow this to be changed - {"m.room.history_visibility", "", events.HistoryVisibilityContent{"joined"}}, // FIXME: Allow this to be changed - {"m.room.guest_access", "", events.GuestAccessContent{"can_join"}}, // FIXME: Allow this to be changed + {"m.room.join_rules", "", common.JoinRulesContent{"public"}}, // FIXME: Allow this to be changed + {"m.room.history_visibility", "", common.HistoryVisibilityContent{"joined"}}, // FIXME: Allow this to be changed + {"m.room.guest_access", "", common.GuestAccessContent{"can_join"}}, // FIXME: Allow this to be changed // TODO: Other initial state items - {"m.room.name", "", events.NameContent{r.Name}}, // FIXME: Only send the name event if a name is supplied, to avoid sending a false room name removal event - {"m.room.topic", "", events.TopicContent{r.Topic}}, + {"m.room.name", "", common.NameContent{r.Name}}, // FIXME: Only send the name event if a name is supplied, to avoid sending a false room name removal event + {"m.room.topic", "", common.TopicContent{r.Topic}}, // TODO: invite events // TODO: 3pid invite events // TODO: m.room.aliases diff --git a/src/github.com/matrix-org/dendrite/clientapi/writers/membership.go b/src/github.com/matrix-org/dendrite/clientapi/writers/membership.go index 7b199a606..ab3a16459 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/writers/membership.go +++ b/src/github.com/matrix-org/dendrite/clientapi/writers/membership.go @@ -23,6 +23,7 @@ import ( "github.com/matrix-org/dendrite/clientapi/httputil" "github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/clientapi/producers" + "github.com/matrix-org/dendrite/common" "github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/gomatrixserverlib" @@ -69,7 +70,7 @@ func SendMembership( membership = "leave" } - content := events.MemberContent{ + content := common.MemberContent{ Membership: membership, DisplayName: profile.DisplayName, AvatarURL: profile.AvatarURL, diff --git a/src/github.com/matrix-org/dendrite/cmd/client-api-proxy/main.go b/src/github.com/matrix-org/dendrite/cmd/client-api-proxy/main.go index 1c6cc4f2c..477f8d127 100644 --- a/src/github.com/matrix-org/dendrite/cmd/client-api-proxy/main.go +++ b/src/github.com/matrix-org/dendrite/cmd/client-api-proxy/main.go @@ -47,12 +47,13 @@ Arguments: ` var ( - syncServerURL = flag.String("sync-api-server-url", "", "The base URL of the listening 'dendrite-sync-api-server' process. E.g. 'http://localhost:4200'") - clientAPIURL = flag.String("client-api-server-url", "", "The base URL of the listening 'dendrite-client-api-server' process. E.g. 'http://localhost:4321'") - mediaAPIURL = flag.String("media-api-server-url", "", "The base URL of the listening 'dendrite-media-api-server' process. E.g. 'http://localhost:7779'") - bindAddress = flag.String("bind-address", ":8008", "The listening port for the proxy.") - certFile = flag.String("tls-cert", "", "The PEM formatted X509 certificate to use for TLS") - keyFile = flag.String("tls-key", "", "The PEM private key to use for TLS") + syncServerURL = flag.String("sync-api-server-url", "", "The base URL of the listening 'dendrite-sync-api-server' process. E.g. 'http://localhost:4200'") + clientAPIURL = flag.String("client-api-server-url", "", "The base URL of the listening 'dendrite-client-api-server' process. E.g. 'http://localhost:4321'") + mediaAPIURL = flag.String("media-api-server-url", "", "The base URL of the listening 'dendrite-media-api-server' process. E.g. 'http://localhost:7779'") + publicRoomsAPIURL = flag.String("public-rooms-api-server-url", "", "The base URL of the listening 'dendrite-public-rooms-api-server' process. E.g. 'http://localhost:7775'") + bindAddress = flag.String("bind-address", ":8008", "The listening port for the proxy.") + certFile = flag.String("tls-cert", "", "The PEM formatted X509 certificate to use for TLS") + keyFile = flag.String("tls-key", "", "The PEM private key to use for TLS") ) func makeProxy(targetURL string) (*httputil.ReverseProxy, error) { @@ -122,6 +123,12 @@ func main() { os.Exit(1) } + if *publicRoomsAPIURL == "" { + flag.Usage() + fmt.Fprintln(os.Stderr, "no --public-rooms-api-server-url specified.") + os.Exit(1) + } + syncProxy, err := makeProxy(*syncServerURL) if err != nil { panic(err) @@ -134,8 +141,14 @@ func main() { if err != nil { panic(err) } + publicRoomsProxy, err := makeProxy(*publicRoomsAPIURL) + if err != nil { + panic(err) + } http.Handle("/_matrix/client/r0/sync", syncProxy) + http.Handle("/_matrix/client/r0/directory/list/", publicRoomsProxy) + http.Handle("/_matrix/client/r0/publicRooms", publicRoomsProxy) http.Handle("/_matrix/media/v1/", mediaProxy) http.Handle("/", clientProxy) @@ -146,9 +159,11 @@ func main() { } fmt.Println("Proxying requests to:") - fmt.Println(" /_matrix/client/r0/sync => ", *syncServerURL+"/api/_matrix/client/r0/sync") - fmt.Println(" /_matrix/media/v1 => ", *mediaAPIURL+"/api/_matrix/media/v1") - fmt.Println(" /* => ", *clientAPIURL+"/api/*") + fmt.Println(" /_matrix/client/r0/sync => ", *syncServerURL+"/api/_matrix/client/r0/sync") + fmt.Println(" /_matrix/client/r0/directory/list => ", *publicRoomsAPIURL+"/_matrix/client/r0/directory/list") + fmt.Println(" /_matrix/client/r0/publicRooms => ", *publicRoomsAPIURL+"/_matrix/media/client/r0/publicRooms") + fmt.Println(" /_matrix/media/v1 => ", *mediaAPIURL+"/api/_matrix/media/v1") + fmt.Println(" /* => ", *clientAPIURL+"/api/*") fmt.Println("Listening on ", *bindAddress) if *certFile != "" && *keyFile != "" { panic(srv.ListenAndServeTLS(*certFile, *keyFile)) diff --git a/src/github.com/matrix-org/dendrite/cmd/dendrite-monolith-server/main.go b/src/github.com/matrix-org/dendrite/cmd/dendrite-monolith-server/main.go index a856a7249..25b269b89 100644 --- a/src/github.com/matrix-org/dendrite/cmd/dendrite-monolith-server/main.go +++ b/src/github.com/matrix-org/dendrite/cmd/dendrite-monolith-server/main.go @@ -52,6 +52,10 @@ import ( "github.com/matrix-org/dendrite/federationsender/queue" federationsender_storage "github.com/matrix-org/dendrite/federationsender/storage" + publicroomsapi_consumers "github.com/matrix-org/dendrite/publicroomsapi/consumers" + publicroomsapi_routing "github.com/matrix-org/dendrite/publicroomsapi/routing" + publicroomsapi_storage "github.com/matrix-org/dendrite/publicroomsapi/storage" + log "github.com/Sirupsen/logrus" sarama "gopkg.in/Shopify/sarama.v1" ) @@ -119,6 +123,7 @@ type monolith struct { mediaAPIDB *mediaapi_storage.Database syncAPIDB *syncapi_storage.SyncServerDatabase federationSenderDB *federationsender_storage.Database + publicRoomsAPIDB *publicroomsapi_storage.PublicRoomsServerDatabase federation *gomatrixserverlib.FederationClient keyRing gomatrixserverlib.KeyRing @@ -171,6 +176,10 @@ func (m *monolith) setupDatabases() { if err != nil { log.Panicf("startup: failed to create federation sender database with data source %s : %s", m.cfg.Database.FederationSender, err) } + m.publicRoomsAPIDB, err = publicroomsapi_storage.NewPublicRoomsServerDatabase(string(m.cfg.Database.PublicRoomsAPI)) + if err != nil { + log.Panicf("startup: failed to setup public rooms api database with data source %s : %s", m.cfg.Database.PublicRoomsAPI, err) + } } func (m *monolith) setupFederation() { @@ -290,6 +299,13 @@ func (m *monolith) setupConsumers() { log.Panicf("startup: failed to start client API server consumer: %s", err) } + publicRoomsAPIConsumer := publicroomsapi_consumers.NewOutputRoomEvent( + m.cfg, m.kafkaConsumer(), m.publicRoomsAPIDB, m.queryAPI, + ) + if err = publicRoomsAPIConsumer.Start(); err != nil { + log.Panicf("startup: failed to start room server consumer: %s", err) + } + federationSenderQueues := queue.NewOutgoingQueues(m.cfg.Matrix.ServerName, m.federation) federationSenderRoomConsumer := federationsender_consumers.NewOutputRoomEvent( @@ -318,4 +334,6 @@ func (m *monolith) setupAPIs() { federationapi_routing.Setup( m.api, *m.cfg, m.queryAPI, m.roomServerProducer, m.keyRing, m.federation, ) + + publicroomsapi_routing.Setup(m.api, m.deviceDB, m.publicRoomsAPIDB) } diff --git a/src/github.com/matrix-org/dendrite/cmd/dendrite-public-rooms-api-server/main.go b/src/github.com/matrix-org/dendrite/cmd/dendrite-public-rooms-api-server/main.go new file mode 100644 index 000000000..c8e705f91 --- /dev/null +++ b/src/github.com/matrix-org/dendrite/cmd/dendrite-public-rooms-api-server/main.go @@ -0,0 +1,85 @@ +// 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 ( + "flag" + "net/http" + "os" + + "github.com/gorilla/mux" + "github.com/matrix-org/dendrite/clientapi/auth/storage/devices" + "github.com/matrix-org/dendrite/common" + "github.com/matrix-org/dendrite/common/config" + "github.com/matrix-org/dendrite/publicroomsapi/consumers" + "github.com/matrix-org/dendrite/publicroomsapi/routing" + "github.com/matrix-org/dendrite/publicroomsapi/storage" + "github.com/matrix-org/dendrite/roomserver/api" + + log "github.com/Sirupsen/logrus" + sarama "gopkg.in/Shopify/sarama.v1" +) + +var configPath = flag.String("config", "dendrite.yaml", "The path to the config file. For more information, see the config file in this repository.") + +func main() { + common.SetupLogging(os.Getenv("LOG_DIR")) + + flag.Parse() + + if *configPath == "" { + log.Fatal("--config must be supplied") + } + cfg, err := config.Load(*configPath) + if err != nil { + log.Fatalf("Invalid config file: %s", err) + } + + queryAPI := api.NewRoomserverQueryAPIHTTP(cfg.RoomServerURL(), nil) + + db, err := storage.NewPublicRoomsServerDatabase(string(cfg.Database.PublicRoomsAPI)) + if err != nil { + log.Panicf("startup: failed to create public rooms server database with data source %s : %s", cfg.Database.PublicRoomsAPI, err) + } + + deviceDB, err := devices.NewDatabase(string(cfg.Database.Device), cfg.Matrix.ServerName) + if err != nil { + log.Panicf("startup: failed to create device database with data source %s : %s", cfg.Database.Device, err) + } + + kafkaConsumer, err := sarama.NewConsumer(cfg.Kafka.Addresses, nil) + if err != nil { + log.WithFields(log.Fields{ + log.ErrorKey: err, + "addresses": cfg.Kafka.Addresses, + }).Panic("Failed to setup kafka consumers") + } + + roomConsumer := consumers.NewOutputRoomEvent(cfg, kafkaConsumer, db, queryAPI) + if err != nil { + log.Panicf("startup: failed to create room server consumer: %s", err) + } + if err = roomConsumer.Start(); err != nil { + log.Panicf("startup: failed to start room server consumer: %s", err) + } + + log.Info("Starting public rooms server on ", cfg.Listen.PublicRoomsAPI) + + api := mux.NewRouter() + routing.Setup(api, deviceDB, db) + common.SetupHTTPAPI(http.DefaultServeMux, api) + + log.Fatal(http.ListenAndServe(string(cfg.Listen.PublicRoomsAPI), nil)) +} diff --git a/src/github.com/matrix-org/dendrite/common/config/config.go b/src/github.com/matrix-org/dendrite/common/config/config.go index ae0fe62cd..8d76a03d4 100644 --- a/src/github.com/matrix-org/dendrite/common/config/config.go +++ b/src/github.com/matrix-org/dendrite/common/config/config.go @@ -133,6 +133,9 @@ type Dendrite struct { // The FederationSender database stores information used by the FederationSender // It is only accessed by the FederationSender. FederationSender DataSource `yaml:"federation_sender"` + // The PublicRoomsAPI database stores information used to compute the public + // room directory. It is only accessed by the PublicRoomsAPI server. + PublicRoomsAPI DataSource `yaml:"public_rooms_api"` } `yaml:"database"` // The internal addresses the components will listen on. @@ -144,6 +147,7 @@ type Dendrite struct { SyncAPI Address `yaml:"sync_api"` RoomServer Address `yaml:"room_server"` FederationSender Address `yaml:"federation_sender"` + PublicRoomsAPI Address `yaml:"public_rooms_api"` } `yaml:"listen"` } diff --git a/src/github.com/matrix-org/dendrite/clientapi/events/eventcontent.go b/src/github.com/matrix-org/dendrite/common/eventcontent.go similarity index 77% rename from src/github.com/matrix-org/dendrite/clientapi/events/eventcontent.go rename to src/github.com/matrix-org/dendrite/common/eventcontent.go index e16b54004..7aa869388 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/events/eventcontent.go +++ b/src/github.com/matrix-org/dendrite/common/eventcontent.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package events +package common // CreateContent is the event content for http://matrix.org/docs/spec/client_server/r0.2.0.html#m-room-create type CreateContent struct { @@ -90,3 +90,29 @@ func InitialPowerLevelsContent(roomCreator string) PowerLevelContent { Users: map[string]int{roomCreator: 100}, } } + +// AliasesContent is the event content for http://matrix.org/docs/spec/client_server/r0.2.0.html#m-room-aliases +type AliasesContent struct { + Aliases []string `json:"aliases"` +} + +// CanonicalAliasContent is the event content for http://matrix.org/docs/spec/client_server/r0.2.0.html#m-room-canonical-alias +type CanonicalAliasContent struct { + Alias string `json:"alias"` +} + +// AvatarContent is the event content for http://matrix.org/docs/spec/client_server/r0.2.0.html#m-room-avatar +type AvatarContent struct { + Info ImageInfo `json:"info,omitempty"` + URL string `json:"url"` + ThumbnailURL string `json:"thumbnail_url,omitempty"` + ThumbnailInfo ImageInfo `json:"thumbnail_info,omitempty"` +} + +// ImageInfo implements the ImageInfo structure from http://matrix.org/docs/spec/client_server/r0.2.0.html#m-room-avatar +type ImageInfo struct { + Mimetype string `json:"mimetype"` + Height int64 `json:"h"` + Width int64 `json:"w"` + Size int64 `json:"size"` +} diff --git a/src/github.com/matrix-org/dendrite/common/test/config.go b/src/github.com/matrix-org/dendrite/common/test/config.go index 948c60f10..0efba5dfb 100644 --- a/src/github.com/matrix-org/dendrite/common/test/config.go +++ b/src/github.com/matrix-org/dendrite/common/test/config.go @@ -95,12 +95,14 @@ func MakeConfig(configDir, kafkaURI, database, host string, startPort int) (*con cfg.Database.RoomServer = config.DataSource(database) cfg.Database.ServerKey = config.DataSource(database) cfg.Database.SyncAPI = config.DataSource(database) + cfg.Database.PublicRoomsAPI = config.DataSource(database) cfg.Listen.ClientAPI = assignAddress() cfg.Listen.FederationAPI = assignAddress() cfg.Listen.MediaAPI = assignAddress() cfg.Listen.RoomServer = assignAddress() cfg.Listen.SyncAPI = assignAddress() + cfg.Listen.PublicRoomsAPI = assignAddress() return &cfg, port, nil } diff --git a/src/github.com/matrix-org/dendrite/common/test/server.go b/src/github.com/matrix-org/dendrite/common/test/server.go index 8e1af2041..4fdd5e638 100644 --- a/src/github.com/matrix-org/dendrite/common/test/server.go +++ b/src/github.com/matrix-org/dendrite/common/test/server.go @@ -94,6 +94,7 @@ func StartProxy(bindAddr string, cfg *config.Dendrite) (*exec.Cmd, chan error) { "--sync-api-server-url", "http://" + string(cfg.Listen.SyncAPI), "--client-api-server-url", "http://" + string(cfg.Listen.ClientAPI), "--media-api-server-url", "http://" + string(cfg.Listen.MediaAPI), + "--public-rooms-api-server-url", "http://" + string(cfg.Listen.PublicRoomsAPI), "--tls-cert", "server.crt", "--tls-key", "server.key", } diff --git a/src/github.com/matrix-org/dendrite/publicroomsapi/README.md b/src/github.com/matrix-org/dendrite/publicroomsapi/README.md new file mode 100644 index 000000000..594fe29c5 --- /dev/null +++ b/src/github.com/matrix-org/dendrite/publicroomsapi/README.md @@ -0,0 +1,5 @@ +# Public rooms API + +This server is responsible for serving requests hitting `/publicRooms` and `/directory/list/room/{roomID}` as per: + +https://matrix.org/docs/spec/client_server/r0.2.0.html#listing-rooms diff --git a/src/github.com/matrix-org/dendrite/publicroomsapi/consumers/roomserver.go b/src/github.com/matrix-org/dendrite/publicroomsapi/consumers/roomserver.go new file mode 100644 index 000000000..959151052 --- /dev/null +++ b/src/github.com/matrix-org/dendrite/publicroomsapi/consumers/roomserver.go @@ -0,0 +1,101 @@ +// 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 consumers + +import ( + "encoding/json" + + log "github.com/Sirupsen/logrus" + "github.com/matrix-org/dendrite/common" + "github.com/matrix-org/dendrite/common/config" + "github.com/matrix-org/dendrite/publicroomsapi/storage" + "github.com/matrix-org/dendrite/roomserver/api" + sarama "gopkg.in/Shopify/sarama.v1" +) + +// OutputRoomEvent consumes events that originated in the room server. +type OutputRoomEvent struct { + roomServerConsumer *common.ContinualConsumer + db *storage.PublicRoomsServerDatabase + query api.RoomserverQueryAPI +} + +// NewOutputRoomEvent creates a new OutputRoomEvent consumer. Call Start() to begin consuming from room servers. +func NewOutputRoomEvent( + cfg *config.Dendrite, + kafkaConsumer sarama.Consumer, + store *storage.PublicRoomsServerDatabase, + queryAPI api.RoomserverQueryAPI, +) *OutputRoomEvent { + consumer := common.ContinualConsumer{ + Topic: string(cfg.Kafka.Topics.OutputRoomEvent), + Consumer: kafkaConsumer, + PartitionStore: store, + } + s := &OutputRoomEvent{ + roomServerConsumer: &consumer, + db: store, + query: queryAPI, + } + consumer.ProcessMessage = s.onMessage + + return s +} + +// Start consuming from room servers +func (s *OutputRoomEvent) Start() error { + return s.roomServerConsumer.Start() +} + +// onMessage is called when the sync server receives a new event from the room server output log. +func (s *OutputRoomEvent) onMessage(msg *sarama.ConsumerMessage) error { + // Parse out the event JSON + var output api.OutputEvent + if err := json.Unmarshal(msg.Value, &output); err != nil { + // If the message was invalid, log it and move on to the next message in the stream + log.WithError(err).Errorf("roomserver output log: message parse failure") + return nil + } + + if output.Type != api.OutputTypeNewRoomEvent { + log.WithField("type", output.Type).Debug( + "roomserver output log: ignoring unknown output type", + ) + return nil + } + + ev := output.NewRoomEvent.Event + log.WithFields(log.Fields{ + "event_id": ev.EventID(), + "room_id": ev.RoomID(), + "type": ev.Type(), + }).Info("received event from roomserver") + + addQueryReq := api.QueryEventsByIDRequest{output.NewRoomEvent.AddsStateEventIDs} + var addQueryRes api.QueryEventsByIDResponse + if err := s.query.QueryEventsByID(&addQueryReq, &addQueryRes); err != nil { + log.Warn(err) + return err + } + + remQueryReq := api.QueryEventsByIDRequest{output.NewRoomEvent.RemovesStateEventIDs} + var remQueryRes api.QueryEventsByIDResponse + if err := s.query.QueryEventsByID(&remQueryReq, &remQueryRes); err != nil { + log.Warn(err) + return err + } + + return s.db.UpdateRoomFromEvents(addQueryRes.Events, remQueryRes.Events) +} diff --git a/src/github.com/matrix-org/dendrite/publicroomsapi/directory/directory.go b/src/github.com/matrix-org/dendrite/publicroomsapi/directory/directory.go new file mode 100644 index 000000000..1718a18af --- /dev/null +++ b/src/github.com/matrix-org/dendrite/publicroomsapi/directory/directory.go @@ -0,0 +1,73 @@ +// 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 directory + +import ( + "net/http" + + "github.com/matrix-org/dendrite/clientapi/httputil" + "github.com/matrix-org/dendrite/publicroomsapi/storage" + + "github.com/matrix-org/util" +) + +type roomVisibility struct { + Visibility string `json:"visibility"` +} + +// GetVisibility implements GET /directory/list/room/{roomID} +func GetVisibility( + req *http.Request, publicRoomsDatabase *storage.PublicRoomsServerDatabase, + roomID string, +) util.JSONResponse { + isPublic, err := publicRoomsDatabase.GetRoomVisibility(roomID) + if err != nil { + return httputil.LogThenError(req, err) + } + + var v roomVisibility + if isPublic { + v.Visibility = "public" + } else { + v.Visibility = "private" + } + + return util.JSONResponse{ + Code: 200, + JSON: v, + } +} + +// SetVisibility implements PUT /directory/list/room/{roomID} +// TODO: Check if user has the power level to edit the room visibility +func SetVisibility( + req *http.Request, publicRoomsDatabase *storage.PublicRoomsServerDatabase, + roomID string, +) util.JSONResponse { + var v roomVisibility + if reqErr := httputil.UnmarshalJSONRequest(req, &v); reqErr != nil { + return *reqErr + } + + isPublic := v.Visibility == "public" + if err := publicRoomsDatabase.SetRoomVisibility(isPublic, roomID); err != nil { + return httputil.LogThenError(req, err) + } + + return util.JSONResponse{ + Code: 200, + JSON: struct{}{}, + } +} diff --git a/src/github.com/matrix-org/dendrite/publicroomsapi/directory/public_rooms.go b/src/github.com/matrix-org/dendrite/publicroomsapi/directory/public_rooms.go new file mode 100644 index 000000000..4566715c2 --- /dev/null +++ b/src/github.com/matrix-org/dendrite/publicroomsapi/directory/public_rooms.go @@ -0,0 +1,113 @@ +// 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 directory + +import ( + "net/http" + "strconv" + + "github.com/matrix-org/dendrite/clientapi/httputil" + "github.com/matrix-org/dendrite/clientapi/jsonerror" + "github.com/matrix-org/dendrite/publicroomsapi/storage" + "github.com/matrix-org/dendrite/publicroomsapi/types" + "github.com/matrix-org/util" +) + +type publicRoomReq struct { + Since string `json:"since,omitempty"` + Limit int16 `json:"limit,omitempty"` + Filter filter `json:"filter,omitempty"` +} + +type filter struct { + SearchTerms string `json:"generic_search_term,omitempty"` +} + +type publicRoomRes struct { + Chunk []types.PublicRoom `json:"chunk"` + NextBatch string `json:"next_batch,omitempty"` + PrevBatch string `json:"prev_batch,omitempty"` + Estimate int64 `json:"total_room_count_estimate,omitempty"` +} + +// GetPublicRooms implements GET /publicRooms +func GetPublicRooms( + req *http.Request, publicRoomDatabase *storage.PublicRoomsServerDatabase, +) util.JSONResponse { + var limit int16 + var offset int64 + var request publicRoomReq + var response publicRoomRes + + if fillErr := fillPublicRoomsReq(req, &request); fillErr != nil { + return *fillErr + } + + limit = request.Limit + offset, err := strconv.ParseInt(request.Since, 10, 64) + // ParseInt returns 0 and an error when trying to parse an empty string + // In that case, we want to assign 0 so we ignore the error + if err != nil && len(request.Since) > 0 { + return httputil.LogThenError(req, err) + } + + if response.Estimate, err = publicRoomDatabase.CountPublicRooms(); err != nil { + return httputil.LogThenError(req, err) + } + + if offset > 0 { + response.PrevBatch = strconv.Itoa(int(offset) - 1) + } + nextIndex := int(offset) + int(limit) + if response.Estimate > int64(nextIndex) { + response.NextBatch = strconv.Itoa(nextIndex) + } + + if response.Chunk, err = publicRoomDatabase.GetPublicRooms(offset, limit, request.Filter.SearchTerms); err != nil { + return httputil.LogThenError(req, err) + } + + return util.JSONResponse{ + Code: 200, + JSON: response, + } +} + +// fillPublicRoomsReq fills the Limit, Since and Filter attributes of a GET or POST request +// on /publicRooms by parsing the incoming HTTP request +func fillPublicRoomsReq(httpReq *http.Request, request *publicRoomReq) *util.JSONResponse { + if httpReq.Method == "GET" { + limit, err := strconv.Atoi(httpReq.FormValue("limit")) + // Atoi returns 0 and an error when trying to parse an empty string + // In that case, we want to assign 0 so we ignore the error + if err != nil && len(httpReq.FormValue("limit")) > 0 { + reqErr := httputil.LogThenError(httpReq, err) + return &reqErr + } + request.Limit = int16(limit) + request.Since = httpReq.FormValue("since") + return nil + } else if httpReq.Method == "POST" { + if reqErr := httputil.UnmarshalJSONRequest(httpReq, request); reqErr != nil { + return reqErr + } + return nil + } + + return &util.JSONResponse{ + Code: 405, + JSON: jsonerror.NotFound("Bad method"), + } +} diff --git a/src/github.com/matrix-org/dendrite/publicroomsapi/routing/routing.go b/src/github.com/matrix-org/dendrite/publicroomsapi/routing/routing.go new file mode 100644 index 000000000..18b8cc57a --- /dev/null +++ b/src/github.com/matrix-org/dendrite/publicroomsapi/routing/routing.go @@ -0,0 +1,51 @@ +// 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 routing + +import ( + "net/http" + + "github.com/gorilla/mux" + "github.com/matrix-org/dendrite/clientapi/auth/authtypes" + "github.com/matrix-org/dendrite/clientapi/auth/storage/devices" + "github.com/matrix-org/dendrite/common" + "github.com/matrix-org/dendrite/publicroomsapi/directory" + "github.com/matrix-org/dendrite/publicroomsapi/storage" + "github.com/matrix-org/util" +) + +const pathPrefixR0 = "/_matrix/client/r0" + +// Setup configures the given mux with publicroomsapi server listeners +func Setup(apiMux *mux.Router, deviceDB *devices.Database, publicRoomsDB *storage.PublicRoomsServerDatabase) { + r0mux := apiMux.PathPrefix(pathPrefixR0).Subrouter() + r0mux.Handle("/directory/list/room/{roomID}", + common.MakeAPI("directory_list", func(req *http.Request) util.JSONResponse { + vars := mux.Vars(req) + return directory.GetVisibility(req, publicRoomsDB, vars["roomID"]) + }), + ).Methods("GET") + r0mux.Handle("/directory/list/room/{roomID}", + common.MakeAuthAPI("directory_list", deviceDB, func(req *http.Request, device *authtypes.Device) util.JSONResponse { + vars := mux.Vars(req) + return directory.SetVisibility(req, publicRoomsDB, vars["roomID"]) + }), + ).Methods("PUT", "OPTIONS") + r0mux.Handle("/publicRooms", + common.MakeAPI("public_rooms", func(req *http.Request) util.JSONResponse { + return directory.GetPublicRooms(req, publicRoomsDB) + }), + ).Methods("GET", "POST", "OPTIONS") +} diff --git a/src/github.com/matrix-org/dendrite/publicroomsapi/storage/prepare.go b/src/github.com/matrix-org/dendrite/publicroomsapi/storage/prepare.go new file mode 100644 index 000000000..b19765992 --- /dev/null +++ b/src/github.com/matrix-org/dendrite/publicroomsapi/storage/prepare.go @@ -0,0 +1,35 @@ +// 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 storage + +import ( + "database/sql" +) + +// a statementList is a list of SQL statements to prepare and a pointer to where to store the resulting prepared statement. +type statementList []struct { + statement **sql.Stmt + sql string +} + +// prepare the SQL for each statement in the list and assign the result to the prepared statement. +func (s statementList) prepare(db *sql.DB) (err error) { + for _, statement := range s { + if *statement.statement, err = db.Prepare(statement.sql); err != nil { + return + } + } + return +} diff --git a/src/github.com/matrix-org/dendrite/publicroomsapi/storage/public_rooms_table.go b/src/github.com/matrix-org/dendrite/publicroomsapi/storage/public_rooms_table.go new file mode 100644 index 000000000..5cef577e7 --- /dev/null +++ b/src/github.com/matrix-org/dendrite/publicroomsapi/storage/public_rooms_table.go @@ -0,0 +1,268 @@ +// 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 storage + +import ( + "database/sql" + "errors" + "fmt" + + "github.com/lib/pq" + "github.com/matrix-org/dendrite/publicroomsapi/types" +) + +var editableAttributes = []string{ + "aliases", + "canonical_alias", + "name", + "topic", + "world_readable", + "guest_can_join", + "avatar_url", + "visibility", +} + +const publicRoomsSchema = ` +-- Stores all of the rooms with data needed to create the server's room directory +CREATE TABLE IF NOT EXISTS publicroomsapi_public_rooms( + -- The room's ID + room_id TEXT NOT NULL PRIMARY KEY, + -- Number of joined members in the room + joined_members INTEGER NOT NULL DEFAULT 0, + -- Aliases of the room (empty array if none) + aliases TEXT[] NOT NULL DEFAULT '{}'::TEXT[], + -- Canonical alias of the room (empty string if none) + canonical_alias TEXT NOT NULL DEFAULT '', + -- Name of the room (empty string if none) + name TEXT NOT NULL DEFAULT '', + -- Topic of the room (empty string if none) + topic TEXT NOT NULL DEFAULT '', + -- Is the room world readable? + world_readable BOOLEAN NOT NULL DEFAULT false, + -- Can guest join the room? + guest_can_join BOOLEAN NOT NULL DEFAULT false, + -- URL of the room avatar (empty string if none) + avatar_url TEXT NOT NULL DEFAULT '', + -- Visibility of the room: true means the room is publicly visible, false + -- means the room is private + visibility BOOLEAN NOT NULL DEFAULT false +); +` + +const countPublicRoomsSQL = "" + + "SELECT COUNT(*) FROM publicroomsapi_public_rooms" + + " WHERE visibility = true" + +const selectPublicRoomsSQL = "" + + "SELECT room_id, joined_members, aliases, canonical_alias, name, topic, world_readable, guest_can_join, avatar_url" + + " FROM publicroomsapi_public_rooms WHERE visibility = true" + + " ORDER BY joined_members DESC" + + " OFFSET $1" + +const selectPublicRoomsWithLimitSQL = "" + + "SELECT room_id, joined_members, aliases, canonical_alias, name, topic, world_readable, guest_can_join, avatar_url" + + " FROM publicroomsapi_public_rooms WHERE visibility = true" + + " ORDER BY joined_members DESC" + + " OFFSET $1 LIMIT $2" + +const selectPublicRoomsWithFilterSQL = "" + + "SELECT room_id, joined_members, aliases, canonical_alias, name, topic, world_readable, guest_can_join, avatar_url" + + " FROM publicroomsapi_public_rooms" + + " WHERE visibility = true" + + " AND (LOWER(name) LIKE LOWER($1)" + + " OR LOWER(topic) LIKE LOWER($1)" + + " OR LOWER(ARRAY_TO_STRING(aliases, ',')) LIKE LOWER($1))" + + " ORDER BY joined_members DESC" + + " OFFSET $2" + +const selectPublicRoomsWithLimitAndFilterSQL = "" + + "SELECT room_id, joined_members, aliases, canonical_alias, name, topic, world_readable, guest_can_join, avatar_url" + + " FROM publicroomsapi_public_rooms" + + " WHERE visibility = true" + + " AND (LOWER(name) LIKE LOWER($1)" + + " OR LOWER(topic) LIKE LOWER($1)" + + " OR LOWER(ARRAY_TO_STRING(aliases, ',')) LIKE LOWER($1))" + + " ORDER BY joined_members DESC" + + " OFFSET $2 LIMIT $3" + +const selectRoomVisibilitySQL = "" + + "SELECT visibility FROM publicroomsapi_public_rooms" + + " WHERE room_id = $1" + +const insertNewRoomSQL = "" + + "INSERT INTO publicroomsapi_public_rooms(room_id)" + + " VALUES ($1)" + +const incrementJoinedMembersInRoomSQL = "" + + "UPDATE publicroomsapi_public_rooms" + + " SET joined_members = joined_members + 1" + + " WHERE room_id = $1" + +const decrementJoinedMembersInRoomSQL = "" + + "UPDATE publicroomsapi_public_rooms" + + " SET joined_members = joined_members - 1" + + " WHERE room_id = $1" + +const updateRoomAttributeSQL = "" + + "UPDATE publicroomsapi_public_rooms" + + " SET %s = $1" + + " WHERE room_id = $2" + +type publicRoomsStatements struct { + countPublicRoomsStmt *sql.Stmt + selectPublicRoomsStmt *sql.Stmt + selectPublicRoomsWithLimitStmt *sql.Stmt + selectPublicRoomsWithFilterStmt *sql.Stmt + selectPublicRoomsWithLimitAndFilterStmt *sql.Stmt + selectRoomVisibilityStmt *sql.Stmt + insertNewRoomStmt *sql.Stmt + incrementJoinedMembersInRoomStmt *sql.Stmt + decrementJoinedMembersInRoomStmt *sql.Stmt + updateRoomAttributeStmts map[string]*sql.Stmt +} + +func (s *publicRoomsStatements) prepare(db *sql.DB) (err error) { + _, err = db.Exec(publicRoomsSchema) + if err != nil { + return + } + + stmts := statementList{ + {&s.countPublicRoomsStmt, countPublicRoomsSQL}, + {&s.selectPublicRoomsStmt, selectPublicRoomsSQL}, + {&s.selectPublicRoomsWithLimitStmt, selectPublicRoomsWithLimitSQL}, + {&s.selectPublicRoomsWithFilterStmt, selectPublicRoomsWithFilterSQL}, + {&s.selectPublicRoomsWithLimitAndFilterStmt, selectPublicRoomsWithLimitAndFilterSQL}, + {&s.selectRoomVisibilityStmt, selectRoomVisibilitySQL}, + {&s.insertNewRoomStmt, insertNewRoomSQL}, + {&s.incrementJoinedMembersInRoomStmt, incrementJoinedMembersInRoomSQL}, + {&s.decrementJoinedMembersInRoomStmt, decrementJoinedMembersInRoomSQL}, + } + + if err = stmts.prepare(db); err != nil { + return + } + + s.updateRoomAttributeStmts = make(map[string]*sql.Stmt) + for _, editable := range editableAttributes { + stmt := fmt.Sprintf(updateRoomAttributeSQL, editable) + if s.updateRoomAttributeStmts[editable], err = db.Prepare(stmt); err != nil { + return + } + } + + return +} + +func (s *publicRoomsStatements) countPublicRooms() (nb int64, err error) { + err = s.countPublicRoomsStmt.QueryRow().Scan(&nb) + return +} + +func (s *publicRoomsStatements) selectPublicRooms(offset int64, limit int16, filter string) ([]types.PublicRoom, error) { + var rows *sql.Rows + var err error + + if len(filter) > 0 { + pattern := "%" + filter + "%" + if limit == 0 { + rows, err = s.selectPublicRoomsWithFilterStmt.Query(pattern, offset) + } else { + rows, err = s.selectPublicRoomsWithLimitAndFilterStmt.Query(pattern, offset, limit) + } + } else { + if limit == 0 { + rows, err = s.selectPublicRoomsStmt.Query(offset) + } else { + rows, err = s.selectPublicRoomsWithLimitStmt.Query(offset, limit) + } + } + + if err != nil { + return []types.PublicRoom{}, nil + } + + rooms := []types.PublicRoom{} + for rows.Next() { + var r types.PublicRoom + var aliases pq.StringArray + + err = rows.Scan( + &r.RoomID, &r.NumJoinedMembers, &aliases, &r.CanonicalAlias, + &r.Name, &r.Topic, &r.WorldReadable, &r.GuestCanJoin, &r.AvatarURL, + ) + if err != nil { + return rooms, err + } + + r.Aliases = make([]string, len(aliases)) + for i := range aliases { + r.Aliases[i] = aliases[i] + } + + rooms = append(rooms, r) + } + + return rooms, nil +} + +func (s *publicRoomsStatements) selectRoomVisibility(roomID string) (v bool, err error) { + err = s.selectRoomVisibilityStmt.QueryRow(roomID).Scan(&v) + return +} + +func (s *publicRoomsStatements) insertNewRoom(roomID string) error { + _, err := s.insertNewRoomStmt.Exec(roomID) + return err +} + +func (s *publicRoomsStatements) incrementJoinedMembersInRoom(roomID string) error { + _, err := s.incrementJoinedMembersInRoomStmt.Exec(roomID) + return err +} + +func (s *publicRoomsStatements) decrementJoinedMembersInRoom(roomID string) error { + _, err := s.decrementJoinedMembersInRoomStmt.Exec(roomID) + return err +} + +func (s *publicRoomsStatements) updateRoomAttribute(attrName string, attrValue attributeValue, roomID string) error { + isEditable := false + for _, editable := range editableAttributes { + if editable == attrName { + isEditable = true + } + } + + if !isEditable { + return errors.New("Cannot edit " + attrName) + } + + var value interface{} + if attrName == "aliases" { + // Aliases need a special conversion + valueAsSlice, isSlice := attrValue.([]string) + if !isSlice { + // attrValue isn't a slice of strings + return errors.New("New list of aliases is of the wrong type") + } + value = pq.StringArray(valueAsSlice) + } else { + value = attrValue + } + + _, err := s.updateRoomAttributeStmts[attrName].Exec(value, roomID) + return err +} diff --git a/src/github.com/matrix-org/dendrite/publicroomsapi/storage/storage.go b/src/github.com/matrix-org/dendrite/publicroomsapi/storage/storage.go new file mode 100644 index 000000000..83861180f --- /dev/null +++ b/src/github.com/matrix-org/dendrite/publicroomsapi/storage/storage.go @@ -0,0 +1,247 @@ +// 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 storage + +import ( + "database/sql" + "encoding/json" + + "github.com/matrix-org/dendrite/common" + "github.com/matrix-org/dendrite/publicroomsapi/types" + + "github.com/matrix-org/gomatrixserverlib" +) + +// PublicRoomsServerDatabase represents a public rooms server database. +type PublicRoomsServerDatabase struct { + db *sql.DB + partitions common.PartitionOffsetStatements + statements publicRoomsStatements +} + +type attributeValue interface{} + +// NewPublicRoomsServerDatabase creates a new public rooms server database. +func NewPublicRoomsServerDatabase(dataSourceName string) (*PublicRoomsServerDatabase, error) { + var db *sql.DB + var err error + if db, err = sql.Open("postgres", dataSourceName); err != nil { + return nil, err + } + partitions := common.PartitionOffsetStatements{} + if err = partitions.Prepare(db, "publicroomsapi"); err != nil { + return nil, err + } + statements := publicRoomsStatements{} + if err = statements.prepare(db); err != nil { + return nil, err + } + return &PublicRoomsServerDatabase{db, partitions, statements}, nil +} + +// PartitionOffsets implements common.PartitionStorer +func (d *PublicRoomsServerDatabase) PartitionOffsets(topic string) ([]common.PartitionOffset, error) { + return d.partitions.SelectPartitionOffsets(topic) +} + +// SetPartitionOffset implements common.PartitionStorer +func (d *PublicRoomsServerDatabase) SetPartitionOffset(topic string, partition int32, offset int64) error { + return d.partitions.UpsertPartitionOffset(topic, partition, offset) +} + +// GetRoomVisibility returns the room visibility as a boolean: true if the room +// is publicly visible, false if not. +// Returns an error if the retrieval failed. +func (d *PublicRoomsServerDatabase) GetRoomVisibility(roomID string) (bool, error) { + return d.statements.selectRoomVisibility(roomID) +} + +// SetRoomVisibility updates the visibility attribute of a room. This attribute +// must be set to true if the room is publicly visible, false if not. +// Returns an error if the update failed. +func (d *PublicRoomsServerDatabase) SetRoomVisibility(visible bool, roomID string) error { + return d.statements.updateRoomAttribute("visibility", visible, roomID) +} + +// CountPublicRooms returns the number of room set as publicly visible on the server. +// Returns an error if the retrieval failed. +func (d *PublicRoomsServerDatabase) CountPublicRooms() (int64, error) { + return d.statements.countPublicRooms() +} + +// GetPublicRooms returns an array containing the local rooms set as publicly visible, ordered by their number +// of joined members. This array can be limited by a given number of elements, and offset by a given value. +// If the limit is 0, doesn't limit the number of results. If the offset is 0 too, the array contains all +// the rooms set as publicly visible on the server. +// Returns an error if the retrieval failed. +func (d *PublicRoomsServerDatabase) GetPublicRooms(offset int64, limit int16, filter string) ([]types.PublicRoom, error) { + return d.statements.selectPublicRooms(offset, limit, filter) +} + +// UpdateRoomFromEvents iterate over a slice of state events and call +// UpdateRoomFromEvent on each of them to update the database representation of +// the rooms updated by each event. +// The slice of events to remove is used to update the number of joined members +// for the room in the database. +// If the update triggered by one of the events failed, aborts the process and +// returns an error. +func (d *PublicRoomsServerDatabase) UpdateRoomFromEvents( + eventsToAdd []gomatrixserverlib.Event, eventsToRemove []gomatrixserverlib.Event, +) error { + for _, event := range eventsToAdd { + if err := d.UpdateRoomFromEvent(event); err != nil { + return err + } + } + + for _, event := range eventsToRemove { + if event.Type() == "m.room.member" { + if err := d.updateNumJoinedUsers(event, true); err != nil { + return err + } + } + } + + return nil +} + +// UpdateRoomFromEvent updates the database representation of a room from a Matrix event, by +// checking the event's type to know which attribute to change and using the event's content +// to define the new value of the attribute. +// If the event doesn't match with any property used to compute the public room directory, +// does nothing. +// If something went wrong during the process, returns an error. +func (d *PublicRoomsServerDatabase) UpdateRoomFromEvent(event gomatrixserverlib.Event) error { + // Process the event according to its type + switch event.Type() { + case "m.room.create": + return d.statements.insertNewRoom(event.RoomID()) + case "m.room.member": + return d.updateNumJoinedUsers(event, false) + case "m.room.aliases": + return d.updateRoomAliases(event) + case "m.room.canonical_alias": + var content common.CanonicalAliasContent + field := &(content.Alias) + attrName := "canonical_alias" + return d.updateStringAttribute(attrName, event, &content, field) + case "m.room.name": + var content common.NameContent + field := &(content.Name) + attrName := "name" + return d.updateStringAttribute(attrName, event, &content, field) + case "m.room.topic": + var content common.TopicContent + field := &(content.Topic) + attrName := "topic" + return d.updateStringAttribute(attrName, event, &content, field) + case "m.room.avatar": + var content common.AvatarContent + field := &(content.URL) + attrName := "avatar_url" + return d.updateStringAttribute(attrName, event, &content, field) + case "m.room.history_visibility": + var content common.HistoryVisibilityContent + field := &(content.HistoryVisibility) + attrName := "world_readable" + strForTrue := "world_readable" + return d.updateBooleanAttribute(attrName, event, &content, field, strForTrue) + case "m.room.guest_access": + var content common.GuestAccessContent + field := &(content.GuestAccess) + attrName := "guest_can_join" + strForTrue := "can_join" + return d.updateBooleanAttribute(attrName, event, &content, field, strForTrue) + } + + // If the event type didn't match, return with no error + return nil +} + +// updateNumJoinedUsers updates the number of joined user in the database representation +// of a room using a given "m.room.member" Matrix event. +// If the membership property of the event isn't "join", ignores it and returs nil. +// If the remove parameter is set to false, increments the joined members counter in the +// database, if set to truem decrements it. +// Returns an error if the update failed. +func (d *PublicRoomsServerDatabase) updateNumJoinedUsers( + membershipEvent gomatrixserverlib.Event, remove bool, +) error { + membership, err := membershipEvent.Membership() + if err != nil { + return err + } + + if membership != "join" { + return nil + } + + if remove { + return d.statements.decrementJoinedMembersInRoom(membershipEvent.RoomID()) + } + return d.statements.incrementJoinedMembersInRoom(membershipEvent.RoomID()) +} + +// updateStringAttribute updates a given string attribute in the database +// representation of a room using a given string data field from content of the +// Matrix event triggering the update. +// Returns an error if decoding the Matrix event's content or updating the attribute +// failed. +func (d *PublicRoomsServerDatabase) updateStringAttribute( + attrName string, event gomatrixserverlib.Event, content interface{}, + field *string, +) error { + if err := json.Unmarshal(event.Content(), content); err != nil { + return err + } + + return d.statements.updateRoomAttribute(attrName, *field, event.RoomID()) +} + +// updateBooleanAttribute updates a given boolean attribute in the database +// representation of a room using a given string data field from content of the +// Matrix event triggering the update. +// The attribute is set to true if the field matches a given string, false if not. +// Returns an error if decoding the Matrix event's content or updating the attribute +// failed. +func (d *PublicRoomsServerDatabase) updateBooleanAttribute( + attrName string, event gomatrixserverlib.Event, content interface{}, + field *string, strForTrue string, +) error { + if err := json.Unmarshal(event.Content(), content); err != nil { + return err + } + + var attrValue bool + if *field == strForTrue { + attrValue = true + } else { + attrValue = false + } + + return d.statements.updateRoomAttribute(attrName, attrValue, event.RoomID()) +} + +// updateRoomAliases decodes the content of a "m.room.aliases" Matrix event and update the list of aliases of +// a given room with it. +// Returns an error if decoding the Matrix event or updating the list failed. +func (d *PublicRoomsServerDatabase) updateRoomAliases(aliasesEvent gomatrixserverlib.Event) error { + var content common.AliasesContent + if err := json.Unmarshal(aliasesEvent.Content(), &content); err != nil { + return err + } + + return d.statements.updateRoomAttribute("aliases", content.Aliases, aliasesEvent.RoomID()) +} diff --git a/src/github.com/matrix-org/dendrite/publicroomsapi/types/types.go b/src/github.com/matrix-org/dendrite/publicroomsapi/types/types.go new file mode 100644 index 000000000..c284bcca4 --- /dev/null +++ b/src/github.com/matrix-org/dendrite/publicroomsapi/types/types.go @@ -0,0 +1,28 @@ +// 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 types + +// PublicRoom represents a local public room +type PublicRoom struct { + RoomID string `json:"room_id"` + Aliases []string `json:"aliases,omitempty"` + CanonicalAlias string `json:"canonical_alias,omitempty"` + Name string `json:"name,omitempty"` + Topic string `json:"topic,omitempty"` + AvatarURL string `json:"avatar_url,omitempty"` + NumJoinedMembers int64 `json:"num_joined_members"` + WorldReadable bool `json:"world_readable"` + GuestCanJoin bool `json:"guest_can_join"` +} diff --git a/src/github.com/matrix-org/dendrite/roomserver/storage/room_aliases_table.go b/src/github.com/matrix-org/dendrite/roomserver/storage/room_aliases_table.go index 433835d7a..bfd6cc090 100644 --- a/src/github.com/matrix-org/dendrite/roomserver/storage/room_aliases_table.go +++ b/src/github.com/matrix-org/dendrite/roomserver/storage/room_aliases_table.go @@ -27,7 +27,7 @@ CREATE TABLE IF NOT EXISTS roomserver_room_aliases ( room_id TEXT NOT NULL ); -CREATE UNIQUE INDEX IF NOT EXISTS roomserver_room_id_idx ON roomserver_room_aliases(room_id); +CREATE INDEX IF NOT EXISTS roomserver_room_id_idx ON roomserver_room_aliases(room_id); ` const insertRoomAliasSQL = "" +