Implement federated public rooms

- Make thirdparty endpoint r0 so riot-web loads the public room list
This commit is contained in:
Kegan Dougal 2020-03-18 20:08:55 +00:00
parent 31bc5ce45d
commit 0c51a17acd
8 changed files with 136 additions and 33 deletions

View file

@ -390,7 +390,7 @@ func Setup(
}), }),
).Methods(http.MethodGet, http.MethodOptions) ).Methods(http.MethodGet, http.MethodOptions)
unstableMux.Handle("/thirdparty/protocols", r0mux.Handle("/thirdparty/protocols",
common.MakeExternalAPI("thirdparty_protocols", func(req *http.Request) util.JSONResponse { common.MakeExternalAPI("thirdparty_protocols", func(req *http.Request) util.JSONResponse {
// TODO: Return the third party protcols // TODO: Return the third party protcols
return util.JSONResponse{ return util.JSONResponse{

View file

@ -135,7 +135,7 @@ func main() {
) )
federationapi.SetupFederationAPIComponent(base, accountDB, deviceDB, federation, &keyRing, alias, input, query, asQuery, fedSenderAPI) federationapi.SetupFederationAPIComponent(base, accountDB, deviceDB, federation, &keyRing, alias, input, query, asQuery, fedSenderAPI)
mediaapi.SetupMediaAPIComponent(base, deviceDB) mediaapi.SetupMediaAPIComponent(base, deviceDB)
publicroomsapi.SetupPublicRoomsAPIComponent(base, deviceDB, query, p2pPublicRoomProvider) publicroomsapi.SetupPublicRoomsAPIComponent(base, deviceDB, query, federation, p2pPublicRoomProvider)
syncapi.SetupSyncAPIComponent(base, deviceDB, accountDB, query, federation, cfg) syncapi.SetupSyncAPIComponent(base, deviceDB, accountDB, query, federation, cfg)
httpHandler := common.WrapHandlerInCORS(base.APIMux) httpHandler := common.WrapHandlerInCORS(base.APIMux)

View file

@ -17,8 +17,6 @@
package main package main
import ( import (
"time"
"github.com/matrix-org/go-http-js-libp2p/go_http_js_libp2p" "github.com/matrix-org/go-http-js-libp2p/go_http_js_libp2p"
) )
@ -39,10 +37,6 @@ func (p *libp2pPublicRoomsProvider) foundProviders(peerInfos []go_http_js_libp2p
p.providers = peerInfos p.providers = peerInfos
} }
func (p *libp2pPublicRoomsProvider) PollInterval() time.Duration {
return 10 * time.Second
}
func (p *libp2pPublicRoomsProvider) Homeservers() []string { func (p *libp2pPublicRoomsProvider) Homeservers() []string {
result := make([]string, len(p.providers)) result := make([]string, len(p.providers))
for i := range p.providers { for i := range p.providers {

View file

@ -18,10 +18,13 @@ import (
"context" "context"
"net/http" "net/http"
"strconv" "strconv"
"sync"
"time"
"github.com/matrix-org/dendrite/clientapi/httputil" "github.com/matrix-org/dendrite/clientapi/httputil"
"github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/publicroomsapi/storage" "github.com/matrix-org/dendrite/publicroomsapi/storage"
"github.com/matrix-org/dendrite/publicroomsapi/types"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util" "github.com/matrix-org/util"
) )
@ -44,7 +47,7 @@ func GetPostPublicRooms(
if fillErr := fillPublicRoomsReq(req, &request); fillErr != nil { if fillErr := fillPublicRoomsReq(req, &request); fillErr != nil {
return *fillErr return *fillErr
} }
response, err := PublicRooms(req.Context(), request, publicRoomDatabase) response, err := publicRooms(req.Context(), request, publicRoomDatabase)
if err != nil { if err != nil {
return jsonerror.InternalServerError() return jsonerror.InternalServerError()
} }
@ -54,7 +57,123 @@ func GetPostPublicRooms(
} }
} }
func PublicRooms(ctx context.Context, request PublicRoomReq, publicRoomDatabase storage.Database) (*gomatrixserverlib.RespPublicRooms, error) { // GetPostPublicRoomsWithExternal is the same as GetPostPublicRooms but also mixes in public rooms from the provider supplied.
func GetPostPublicRoomsWithExternal(
req *http.Request, publicRoomDatabase storage.Database, fedClient *gomatrixserverlib.FederationClient,
extRoomsProvider types.ExternalPublicRoomsProvider,
) util.JSONResponse {
var request PublicRoomReq
if fillErr := fillPublicRoomsReq(req, &request); fillErr != nil {
return *fillErr
}
response, err := publicRooms(req.Context(), request, publicRoomDatabase)
if err != nil {
return jsonerror.InternalServerError()
}
if request.Since != "" {
// TODO: handle pagination tokens sensibly rather than ignoring them.
// ignore paginated requests since we don't handle them yet over federation.
// Only the initial request will contain federated rooms.
return util.JSONResponse{
Code: http.StatusOK,
JSON: response,
}
}
// If we have already hit the limit on the number of rooms, bail.
var limit int
if request.Limit > 0 {
limit = int(request.Limit) - len(response.Chunk)
if limit <= 0 {
return util.JSONResponse{
Code: http.StatusOK,
JSON: response,
}
}
}
// downcasting `limit` is safe as we know it isn't bigger than request.Limit which is int16
fedRooms := bulkFetchPublicRoomsFromServers(req.Context(), fedClient, extRoomsProvider.Homeservers(), int16(limit))
response.Chunk = append(response.Chunk, fedRooms...)
return util.JSONResponse{
Code: http.StatusOK,
JSON: response,
}
}
// bulkFetchPublicRoomsFromServers fetches public rooms from the list of homeservers.
// Returns a list of public rooms up to the limit specified.
func bulkFetchPublicRoomsFromServers(
ctx context.Context, fedClient *gomatrixserverlib.FederationClient, homeservers []string, limit int16,
) (publicRooms []gomatrixserverlib.PublicRoom) {
// follow pipeline semantics, see https://blog.golang.org/pipelines for more info.
// goroutines send rooms to this channel
roomCh := make(chan gomatrixserverlib.PublicRoom, int(limit))
// signalling channel to tell goroutines to stop sending rooms and quit
done := make(chan bool)
// signalling to say when we can close the room channel
var wg sync.WaitGroup
wg.Add(len(homeservers))
// concurrently query for public rooms
for _, hs := range homeservers {
go func(homeserverDomain string) {
defer wg.Done()
util.GetLogger(ctx).WithField("hs", homeserverDomain).Info("Querying HS for public rooms")
fres, err := fedClient.GetPublicRooms(ctx, gomatrixserverlib.ServerName(homeserverDomain), int(limit), "", false, "")
if err != nil {
util.GetLogger(ctx).WithError(err).WithField("hs", homeserverDomain).Warn(
"bulkFetchPublicRoomsFromServers: failed to query hs",
)
return
}
for _, room := range fres.Chunk {
// atomically send a room or stop
select {
case roomCh <- room:
case <-done:
util.GetLogger(ctx).WithError(err).WithField("hs", homeserverDomain).Info("Interruped whilst sending rooms")
return
}
}
}(hs)
}
// Close the room channel when the goroutines have quit so we don't leak, but don't let it stop the in-flight request.
// This also allows the request to fail fast if all HSes experience errors as it will cause the room channel to be
// closed.
go func() {
wg.Wait()
util.GetLogger(ctx).Info("Cleaning up resources")
close(roomCh)
}()
// fan-in results with timeout. We stop when we reach the limit.
FanIn:
for len(publicRooms) < int(limit) || limit == 0 {
// add a room or timeout
select {
case room, ok := <-roomCh:
if !ok {
util.GetLogger(ctx).Info("All homeservers have been queried, returning results.")
break FanIn
}
publicRooms = append(publicRooms, room)
case <-time.After(15 * time.Second): // we've waited long enough, let's tell the client what we got.
util.GetLogger(ctx).Info("Waited 15s for federated public rooms, returning early")
break FanIn
case <-ctx.Done(): // the client hung up on us, let's stop.
util.GetLogger(ctx).Info("Client hung up, returning early")
break FanIn
}
}
// tell goroutines to stop
close(done)
return publicRooms
}
func publicRooms(ctx context.Context, request PublicRoomReq, publicRoomDatabase storage.Database) (*gomatrixserverlib.RespPublicRooms, error) {
var response gomatrixserverlib.RespPublicRooms var response gomatrixserverlib.RespPublicRooms
var limit int16 var limit int16
var offset int64 var offset int64

View file

@ -22,6 +22,7 @@ import (
"github.com/matrix-org/dendrite/publicroomsapi/storage" "github.com/matrix-org/dendrite/publicroomsapi/storage"
"github.com/matrix-org/dendrite/publicroomsapi/types" "github.com/matrix-org/dendrite/publicroomsapi/types"
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api" roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/gomatrixserverlib"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -31,6 +32,7 @@ func SetupPublicRoomsAPIComponent(
base *basecomponent.BaseDendrite, base *basecomponent.BaseDendrite,
deviceDB devices.Database, deviceDB devices.Database,
rsQueryAPI roomserverAPI.RoomserverQueryAPI, rsQueryAPI roomserverAPI.RoomserverQueryAPI,
fedClient *gomatrixserverlib.FederationClient,
extRoomsProvider types.ExternalPublicRoomsProvider, extRoomsProvider types.ExternalPublicRoomsProvider,
) { ) {
publicRoomsDB, err := storage.NewPublicRoomsServerDatabase(string(base.Cfg.Database.PublicRoomsAPI)) publicRoomsDB, err := storage.NewPublicRoomsServerDatabase(string(base.Cfg.Database.PublicRoomsAPI))
@ -45,5 +47,5 @@ func SetupPublicRoomsAPIComponent(
logrus.WithError(err).Panic("failed to start public rooms server consumer") logrus.WithError(err).Panic("failed to start public rooms server consumer")
} }
routing.Setup(base.APIMux, deviceDB, publicRoomsDB, extRoomsProvider) routing.Setup(base.APIMux, deviceDB, publicRoomsDB, fedClient, extRoomsProvider)
} }

View file

@ -15,9 +15,7 @@
package routing package routing
import ( import (
"fmt"
"net/http" "net/http"
"time"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/matrix-org/dendrite/clientapi/auth" "github.com/matrix-org/dendrite/clientapi/auth"
@ -27,6 +25,7 @@ import (
"github.com/matrix-org/dendrite/publicroomsapi/directory" "github.com/matrix-org/dendrite/publicroomsapi/directory"
"github.com/matrix-org/dendrite/publicroomsapi/storage" "github.com/matrix-org/dendrite/publicroomsapi/storage"
"github.com/matrix-org/dendrite/publicroomsapi/types" "github.com/matrix-org/dendrite/publicroomsapi/types"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util" "github.com/matrix-org/util"
) )
@ -37,7 +36,10 @@ const pathPrefixR0 = "/_matrix/client/r0"
// Due to Setup being used to call many other functions, a gocyclo nolint is // Due to Setup being used to call many other functions, a gocyclo nolint is
// applied: // applied:
// nolint: gocyclo // nolint: gocyclo
func Setup(apiMux *mux.Router, deviceDB devices.Database, publicRoomsDB storage.Database, extRoomsProvider types.ExternalPublicRoomsProvider) { func Setup(
apiMux *mux.Router, deviceDB devices.Database, publicRoomsDB storage.Database,
fedClient *gomatrixserverlib.FederationClient, extRoomsProvider types.ExternalPublicRoomsProvider,
) {
r0mux := apiMux.PathPrefix(pathPrefixR0).Subrouter() r0mux := apiMux.PathPrefix(pathPrefixR0).Subrouter()
authData := auth.Data{ authData := auth.Data{
@ -68,6 +70,7 @@ func Setup(apiMux *mux.Router, deviceDB devices.Database, publicRoomsDB storage.
r0mux.Handle("/publicRooms", r0mux.Handle("/publicRooms",
common.MakeExternalAPI("public_rooms", func(req *http.Request) util.JSONResponse { common.MakeExternalAPI("public_rooms", func(req *http.Request) util.JSONResponse {
if extRoomsProvider != nil { if extRoomsProvider != nil {
return directory.GetPostPublicRoomsWithExternal(req, publicRoomsDB, fedClient, extRoomsProvider)
} }
return directory.GetPostPublicRooms(req, publicRoomsDB) return directory.GetPostPublicRooms(req, publicRoomsDB)
}), }),
@ -79,18 +82,4 @@ func Setup(apiMux *mux.Router, deviceDB devices.Database, publicRoomsDB storage.
return directory.GetPostPublicRooms(req, publicRoomsDB) return directory.GetPostPublicRooms(req, publicRoomsDB)
}), }),
).Methods(http.MethodGet) ).Methods(http.MethodGet)
if extRoomsProvider != nil {
pollExternalPublicRooms(extRoomsProvider)
}
}
func pollExternalPublicRooms(extRoomsProvider types.ExternalPublicRoomsProvider) {
go func() {
for {
hses := extRoomsProvider.Homeservers()
fmt.Println(hses)
time.Sleep(extRoomsProvider.PollInterval())
}
}()
} }

View file

@ -22,6 +22,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/matrix-org/dendrite/common"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
) )
@ -66,7 +67,7 @@ const selectPublicRoomsWithLimitSQL = "" +
"SELECT room_id, joined_members, aliases, canonical_alias, name, topic, world_readable, guest_can_join, avatar_url" + "SELECT room_id, joined_members, aliases, canonical_alias, name, topic, world_readable, guest_can_join, avatar_url" +
" FROM publicroomsapi_public_rooms WHERE visibility = true" + " FROM publicroomsapi_public_rooms WHERE visibility = true" +
" ORDER BY joined_members DESC" + " ORDER BY joined_members DESC" +
" LIMIT $2 OFFSET $1" " LIMIT $1 OFFSET $2"
const selectPublicRoomsWithFilterSQL = "" + const selectPublicRoomsWithFilterSQL = "" +
"SELECT room_id, joined_members, aliases, canonical_alias, name, topic, world_readable, guest_can_join, avatar_url" + "SELECT room_id, joined_members, aliases, canonical_alias, name, topic, world_readable, guest_can_join, avatar_url" +
@ -192,6 +193,7 @@ func (s *publicRoomsStatements) selectPublicRooms(
if err != nil { if err != nil {
return []gomatrixserverlib.PublicRoom{}, nil return []gomatrixserverlib.PublicRoom{}, nil
} }
defer common.CloseAndLogIfError(ctx, rows, "selectPublicRooms failed to close rows")
rooms := []gomatrixserverlib.PublicRoom{} rooms := []gomatrixserverlib.PublicRoom{}
for rows.Next() { for rows.Next() {

View file

@ -14,14 +14,11 @@
package types package types
import "time"
// ExternalPublicRoomsProvider provides a list of homeservers who should be queried // ExternalPublicRoomsProvider provides a list of homeservers who should be queried
// periodically for a list of public rooms on their server. // periodically for a list of public rooms on their server.
type ExternalPublicRoomsProvider interface { type ExternalPublicRoomsProvider interface {
// The interval at which to check servers
PollInterval() time.Duration
// The list of homeserver domains to query. These servers will receive a request // The list of homeserver domains to query. These servers will receive a request
// via this API: https://matrix.org/docs/spec/server_server/latest#public-room-directory // via this API: https://matrix.org/docs/spec/server_server/latest#public-room-directory
// This will be called -on demand- by clients, so cache appropriately!
Homeservers() []string Homeservers() []string
} }