From 49ed708ca4e3fb62ef4f10999e2278529e016096 Mon Sep 17 00:00:00 2001 From: Kegsay Date: Fri, 10 Mar 2017 11:32:53 +0000 Subject: [PATCH] Create the initial set of state events for room creation (#32) --- .../dendrite/clientapi/clientapi.go | 15 +- .../dendrite/clientapi/config/config.go | 10 ++ .../dendrite/clientapi/events/eventcontent.go | 62 +++++++ .../common.go => httputil/httputil.go} | 2 +- .../dendrite/clientapi/routing/routing.go | 23 +-- .../dendrite/clientapi/writers/createroom.go | 167 ++++++++++++++++-- .../matrix-org/dendrite/common/types.go | 13 ++ .../dendrite/roomserver/api/query.go | 15 +- .../roomserver-integration-tests/main.go | 3 +- .../dendrite/roomserver/state/state.go | 6 +- vendor/manifest | 4 +- .../matrix-org/gomatrixserverlib/event.go | 2 +- 12 files changed, 269 insertions(+), 53 deletions(-) create mode 100644 src/github.com/matrix-org/dendrite/clientapi/config/config.go create mode 100644 src/github.com/matrix-org/dendrite/clientapi/events/eventcontent.go rename src/github.com/matrix-org/dendrite/clientapi/{common/common.go => httputil/httputil.go} (98%) create mode 100644 src/github.com/matrix-org/dendrite/common/types.go diff --git a/src/github.com/matrix-org/dendrite/clientapi/clientapi.go b/src/github.com/matrix-org/dendrite/clientapi/clientapi.go index 95055b29e..a487135bc 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/clientapi.go +++ b/src/github.com/matrix-org/dendrite/clientapi/clientapi.go @@ -5,6 +5,9 @@ import ( "os" "path/filepath" + "golang.org/x/crypto/ed25519" + + "github.com/matrix-org/dendrite/clientapi/config" "github.com/matrix-org/dendrite/clientapi/routing" log "github.com/Sirupsen/logrus" @@ -36,6 +39,16 @@ func main() { setupLogging(logDir) } log.Info("Starting clientapi") - routing.Setup(http.DefaultServeMux, http.DefaultClient) + // TODO: Rather than generating a new key on every startup, we should be + // reading a PEM formatted file instead. + _, privKey, err := ed25519.GenerateKey(nil) + if err != nil { + log.Panicf("Failed to generate private key: %s", err) + } + routing.Setup(http.DefaultServeMux, http.DefaultClient, config.ClientAPI{ + ServerName: "localhost", + KeyID: "ed25519:something", + PrivateKey: privKey, + }) log.Fatal(http.ListenAndServe(bindAddr, nil)) } diff --git a/src/github.com/matrix-org/dendrite/clientapi/config/config.go b/src/github.com/matrix-org/dendrite/clientapi/config/config.go new file mode 100644 index 000000000..f743dcba8 --- /dev/null +++ b/src/github.com/matrix-org/dendrite/clientapi/config/config.go @@ -0,0 +1,10 @@ +package config + +import "golang.org/x/crypto/ed25519" + +// ClientAPI contains the config information necessary to spin up a clientapi process. +type ClientAPI struct { + ServerName string + PrivateKey ed25519.PrivateKey + KeyID string +} diff --git a/src/github.com/matrix-org/dendrite/clientapi/events/eventcontent.go b/src/github.com/matrix-org/dendrite/clientapi/events/eventcontent.go new file mode 100644 index 000000000..401237dbc --- /dev/null +++ b/src/github.com/matrix-org/dendrite/clientapi/events/eventcontent.go @@ -0,0 +1,62 @@ +package events + +// CreateContent is the event content for http://matrix.org/docs/spec/client_server/r0.2.0.html#m-room-create +type CreateContent struct { + Creator string `json:"creator"` + Federate *bool `json:"m.federate,omitempty"` +} + +// MemberContent is the event content for http://matrix.org/docs/spec/client_server/r0.2.0.html#m-room-member +type MemberContent struct { + Membership string `json:"membership"` + DisplayName string `json:"displayname,omitempty"` + AvatarURL string `json:"avatar_url,omitempty"` + // TODO: ThirdPartyInvite string `json:"third_party_invite,omitempty"` +} + +// JoinRulesContent is the event content for http://matrix.org/docs/spec/client_server/r0.2.0.html#m-room-join-rules +type JoinRulesContent struct { + JoinRule string `json:"join_rule"` +} + +// HistoryVisibilityContent is the event content for http://matrix.org/docs/spec/client_server/r0.2.0.html#m-room-history-visibility +type HistoryVisibilityContent struct { + HistoryVisibility string `json:"history_visibility"` +} + +// PowerLevelContent is the event content for http://matrix.org/docs/spec/client_server/r0.2.0.html#m-room-power-levels +type PowerLevelContent struct { + EventsDefault int `json:"events_default"` + Invite int `json:"invite"` + StateDefault int `json:"state_default"` + Redact int `json:"redact"` + Ban int `json:"ban"` + UsersDefault int `json:"users_default"` + Events map[string]int `json:"events"` + Kick int `json:"kick"` + Users map[string]int `json:"users"` +} + +// InitialPowerLevelsContent returns the initial values for m.room.power_levels on room creation +// if they have not been specified. +// http://matrix.org/docs/spec/client_server/r0.2.0.html#m-room-power-levels +// https://github.com/matrix-org/synapse/blob/v0.19.2/synapse/handlers/room.py#L294 +func InitialPowerLevelsContent(roomCreator string) PowerLevelContent { + return PowerLevelContent{ + EventsDefault: 0, + Invite: 0, + StateDefault: 50, + Redact: 50, + Ban: 50, + UsersDefault: 0, + Events: map[string]int{ + "m.room.name": 50, + "m.room.power_levels": 100, + "m.room.history_visibility": 100, + "m.room.canonical_alias": 50, + "m.room.avatar": 50, + }, + Kick: 50, + Users: map[string]int{roomCreator: 100}, + } +} diff --git a/src/github.com/matrix-org/dendrite/clientapi/common/common.go b/src/github.com/matrix-org/dendrite/clientapi/httputil/httputil.go similarity index 98% rename from src/github.com/matrix-org/dendrite/clientapi/common/common.go rename to src/github.com/matrix-org/dendrite/clientapi/httputil/httputil.go index cbc94a6f5..d1f6c4d59 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/common/common.go +++ b/src/github.com/matrix-org/dendrite/clientapi/httputil/httputil.go @@ -1,4 +1,4 @@ -package common +package httputil import ( "encoding/json" 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 22fcfbd68..50f502405 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go +++ b/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/gorilla/mux" + "github.com/matrix-org/dendrite/clientapi/config" "github.com/matrix-org/dendrite/clientapi/readers" "github.com/matrix-org/dendrite/clientapi/writers" "github.com/matrix-org/util" @@ -14,17 +15,17 @@ const pathPrefixR0 = "/_matrix/client/r0" // Setup registers HTTP handlers with the given ServeMux. It also supplies the given http.Client // to clients which need to make outbound HTTP requests. -func Setup(servMux *http.ServeMux, httpClient *http.Client) { +func Setup(servMux *http.ServeMux, httpClient *http.Client, cfg config.ClientAPI) { apiMux := mux.NewRouter() r0mux := apiMux.PathPrefix(pathPrefixR0).Subrouter() - r0mux.Handle("/createRoom", make("createRoom", wrap(func(req *http.Request) util.JSONResponse { - return writers.CreateRoom(req) + r0mux.Handle("/createRoom", make("createRoom", util.NewJSONRequestHandler(func(req *http.Request) util.JSONResponse { + return writers.CreateRoom(req, cfg) }))) - r0mux.Handle("/sync", make("sync", wrap(func(req *http.Request) util.JSONResponse { + r0mux.Handle("/sync", make("sync", util.NewJSONRequestHandler(func(req *http.Request) util.JSONResponse { return readers.Sync(req) }))) r0mux.Handle("/rooms/{roomID}/send/{eventType}", - make("send_message", wrap(func(req *http.Request) util.JSONResponse { + make("send_message", util.NewJSONRequestHandler(func(req *http.Request) util.JSONResponse { vars := mux.Vars(req) return writers.SendMessage(req, vars["roomID"], vars["eventType"]) })), @@ -38,15 +39,3 @@ func Setup(servMux *http.ServeMux, httpClient *http.Client) { func make(metricsName string, h util.JSONRequestHandler) http.Handler { return prometheus.InstrumentHandler(metricsName, util.MakeJSONAPI(h)) } - -// jsonRequestHandlerWrapper is a wrapper to allow in-line functions to conform to util.JSONRequestHandler -type jsonRequestHandlerWrapper struct { - function func(req *http.Request) util.JSONResponse -} - -func (r *jsonRequestHandlerWrapper) OnIncomingRequest(req *http.Request) util.JSONResponse { - return r.function(req) -} -func wrap(f func(req *http.Request) util.JSONResponse) *jsonRequestHandlerWrapper { - return &jsonRequestHandlerWrapper{f} -} 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 1dde64e74..6e7d07f45 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/writers/createroom.go +++ b/src/github.com/matrix-org/dendrite/clientapi/writers/createroom.go @@ -5,11 +5,16 @@ import ( "fmt" "net/http" "strings" + "time" log "github.com/Sirupsen/logrus" "github.com/matrix-org/dendrite/clientapi/auth" - "github.com/matrix-org/dendrite/clientapi/common" + "github.com/matrix-org/dendrite/clientapi/config" + "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/common" + "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/util" ) @@ -64,24 +69,30 @@ type createRoomResponse struct { RoomAlias string `json:"room_alias,omitempty"` // in synapse not spec } +// fledglingEvent is a helper representation of an event used when creating many events in succession. +type fledglingEvent struct { + Type string + StateKey string + Content interface{} +} + // CreateRoom implements /createRoom -func CreateRoom(req *http.Request) util.JSONResponse { - serverName := "localhost" +func CreateRoom(req *http.Request, cfg config.ClientAPI) util.JSONResponse { // TODO: Check room ID doesn't clash with an existing one, and we // probably shouldn't be using pseudo-random strings, maybe GUIDs? - roomID := fmt.Sprintf("!%s:%s", util.RandomString(16), serverName) - return createRoom(req, roomID) + roomID := fmt.Sprintf("!%s:%s", util.RandomString(16), cfg.ServerName) + return createRoom(req, cfg, roomID) } // createRoom implements /createRoom -func createRoom(req *http.Request, roomID string) util.JSONResponse { +func createRoom(req *http.Request, cfg config.ClientAPI, roomID string) util.JSONResponse { logger := util.GetLogger(req.Context()) userID, resErr := auth.VerifyAccessToken(req) if resErr != nil { return *resErr } var r createRoomRequest - resErr = common.UnmarshalJSONRequest(req, &r) + resErr = httputil.UnmarshalJSONRequest(req, &r) if resErr != nil { return *resErr } @@ -98,7 +109,11 @@ func createRoom(req *http.Request, roomID string) util.JSONResponse { logger.WithFields(log.Fields{ "userID": userID, "roomID": roomID, - }).Info("Creating room") + }).Info("Creating new room") + + // Remember events we've built and key off the state tuple so we can look them up easily when filling in auth_events + builtEventMap := make(map[common.StateKeyTuple]*gomatrixserverlib.Event) + var builtEvents []*gomatrixserverlib.Event // send events into the room in order of: // 1- m.room.create @@ -117,12 +132,136 @@ func createRoom(req *http.Request, roomID string) util.JSONResponse { // This differs from Synapse slightly. Synapse would vary the ordering of 3-7 // depending on if those events were in "initial_state" or not. This made it // 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.member", userID, events.MemberContent{Membership: "join"}}, // TODO: Set avatar_url / displayname + {"m.room.power_levels", "", events.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 + // TODO: m.room.guest_access + // TODO: Other initial state items + // TODO: m.room.name + // TODO: m.room.topic + // TODO: invite events + // TODO: 3pid invite events + // TODO m.room.aliases + } - // f.e event: - // - validate required keys/types (EventValidator in synapse) - // - set additional keys (displayname/avatar_url for m.room.member) - // - set token(?) and txn id - // - then https://github.com/matrix-org/synapse/blob/v0.19.2/synapse/handlers/message.py#L419 + authEvents := authEventProvider{builtEventMap} + for i, e := range eventsToMake { + depth := i + 1 // depth starts at 1 - return util.MessageResponse(404, "Not implemented yet") + builder := gomatrixserverlib.EventBuilder{ + Sender: userID, + RoomID: roomID, + Type: e.Type, + StateKey: &e.StateKey, + Depth: int64(depth), + } + builder.SetContent(e.Content) + if i > 0 { + builder.PrevEvents = []gomatrixserverlib.EventReference{builtEvents[i-1].EventReference()} + } + ev, err := buildEvent(&builder, builtEventMap, cfg) + if err != nil { + return util.ErrorResponse(err) + } + + if err := gomatrixserverlib.Allowed(*ev, &authEvents); err != nil { + return util.ErrorResponse(err) + } + + // Add the event to the list of auth events + builtEventMap[common.StateKeyTuple{e.Type, e.StateKey}] = ev + builtEvents = append(builtEvents, ev) + + } + + return util.JSONResponse{ + Code: 200, + JSON: builtEvents, + } +} + +// buildEvent fills out auth_events for the builder then builds the event +func buildEvent(builder *gomatrixserverlib.EventBuilder, + events map[common.StateKeyTuple]*gomatrixserverlib.Event, + cfg config.ClientAPI) (*gomatrixserverlib.Event, error) { + + eventsNeeded, err := gomatrixserverlib.StateNeededForEventBuilder(builder) + if err != nil { + return nil, err + } + builder.AuthEvents = authEventsFromStateNeeded(eventsNeeded, events) + eventID := fmt.Sprintf("$%s:%s", util.RandomString(16), cfg.ServerName) + now := time.Now() + event, err := builder.Build(eventID, now, cfg.ServerName, cfg.KeyID, cfg.PrivateKey) + if err != nil { + return nil, fmt.Errorf("cannot build event %s : Builder failed to build. %s", builder.Type, err) + } + return &event, nil +} + +func authEventsFromStateNeeded(eventsNeeded gomatrixserverlib.StateNeeded, + events map[common.StateKeyTuple]*gomatrixserverlib.Event) (authEvents []gomatrixserverlib.EventReference) { + + // These events are only "needed" if they exist, so if they don't exist we can safely ignore them. + if eventsNeeded.Create { + ev := events[common.StateKeyTuple{"m.room.create", ""}] + if ev != nil { + authEvents = append(authEvents, ev.EventReference()) + } + } + if eventsNeeded.JoinRules { + ev := events[common.StateKeyTuple{"m.room.join_rules", ""}] + if ev != nil { + authEvents = append(authEvents, ev.EventReference()) + } + } + if eventsNeeded.PowerLevels { + ev := events[common.StateKeyTuple{"m.room.power_levels", ""}] + if ev != nil { + authEvents = append(authEvents, ev.EventReference()) + } + } + + for _, userID := range eventsNeeded.Member { + ev := events[common.StateKeyTuple{"m.room.member", userID}] + if ev != nil { + authEvents = append(authEvents, ev.EventReference()) + } + } + for _, token := range eventsNeeded.ThirdPartyInvite { + ev := events[common.StateKeyTuple{"m.room.member", token}] + if ev != nil { + authEvents = append(authEvents, ev.EventReference()) + } + } + return +} + +type authEventProvider struct { + events map[common.StateKeyTuple]*gomatrixserverlib.Event +} + +func (a *authEventProvider) Create() (ev *gomatrixserverlib.Event, err error) { + return a.events[common.StateKeyTuple{"m.room.create", ""}], nil +} + +func (a *authEventProvider) JoinRules() (ev *gomatrixserverlib.Event, err error) { + return a.events[common.StateKeyTuple{"m.room.join_rules", ""}], nil +} + +func (a *authEventProvider) PowerLevels() (ev *gomatrixserverlib.Event, err error) { + return a.events[common.StateKeyTuple{"m.room.power_levels", ""}], nil +} + +func (a *authEventProvider) Member(stateKey string) (ev *gomatrixserverlib.Event, err error) { + return a.events[common.StateKeyTuple{"m.room.member", stateKey}], nil +} + +func (a *authEventProvider) ThirdPartyInvite(stateKey string) (ev *gomatrixserverlib.Event, err error) { + return a.events[common.StateKeyTuple{"m.room.third_party_invite", stateKey}], nil } diff --git a/src/github.com/matrix-org/dendrite/common/types.go b/src/github.com/matrix-org/dendrite/common/types.go new file mode 100644 index 000000000..125838f21 --- /dev/null +++ b/src/github.com/matrix-org/dendrite/common/types.go @@ -0,0 +1,13 @@ +package common + +// StateKeyTuple is a pair of an event type and state_key. +// This is typically used as a key in a map. +type StateKeyTuple struct { + // The "type" key of a matrix event. + EventType string + // The "state_key" of a matrix event. + // The empty string is a legitimate value for the "state_key" in matrix + // so take care to initialise this field lest you accidentally request a + // "state_key" with the go default of the empty string. + EventStateKey string +} diff --git a/src/github.com/matrix-org/dendrite/roomserver/api/query.go b/src/github.com/matrix-org/dendrite/roomserver/api/query.go index a3a647e67..06e75a745 100644 --- a/src/github.com/matrix-org/dendrite/roomserver/api/query.go +++ b/src/github.com/matrix-org/dendrite/roomserver/api/query.go @@ -4,29 +4,18 @@ import ( "bytes" "encoding/json" "fmt" + "github.com/matrix-org/dendrite/common" "github.com/matrix-org/gomatrixserverlib" "net/http" ) -// StateKeyTuple is a pair of an event type and state_key. -// This is used when requesting parts of the state of a room. -type StateKeyTuple struct { - // The "type" key - EventType string - // The "state_key" of a matrix event. - // The empty string is a legitimate value for the "state_key" in matrix - // so take care to initialise this field lest you accidentally request a - // "state_key" with the go default of the empty string. - EventStateKey string -} - // QueryLatestEventsAndStateRequest is a request to QueryLatestEventsAndState type QueryLatestEventsAndStateRequest struct { // The roomID to query the latest events for. RoomID string // The state key tuples to fetch from the room current state. // If this list is empty or nil then no state events are returned. - StateToFetch []StateKeyTuple + StateToFetch []common.StateKeyTuple } // QueryLatestEventsAndStateResponse is a response to QueryLatestEventsAndState diff --git a/src/github.com/matrix-org/dendrite/roomserver/roomserver-integration-tests/main.go b/src/github.com/matrix-org/dendrite/roomserver/roomserver-integration-tests/main.go index 96a00577b..6c051cb90 100644 --- a/src/github.com/matrix-org/dendrite/roomserver/roomserver-integration-tests/main.go +++ b/src/github.com/matrix-org/dendrite/roomserver/roomserver-integration-tests/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "github.com/matrix-org/dendrite/common" "github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/gomatrixserverlib" "os" @@ -367,7 +368,7 @@ func main() { if err := q.QueryLatestEventsAndState( &api.QueryLatestEventsAndStateRequest{ RoomID: "!HCXfdvrfksxuYnIFiJ:matrix.org", - StateToFetch: []api.StateKeyTuple{ + StateToFetch: []common.StateKeyTuple{ {"m.room.member", "@richvdh:matrix.org"}, }, }, diff --git a/src/github.com/matrix-org/dendrite/roomserver/state/state.go b/src/github.com/matrix-org/dendrite/roomserver/state/state.go index ec7b8e08a..b014c722b 100644 --- a/src/github.com/matrix-org/dendrite/roomserver/state/state.go +++ b/src/github.com/matrix-org/dendrite/roomserver/state/state.go @@ -4,7 +4,7 @@ package state import ( "fmt" - "github.com/matrix-org/dendrite/roomserver/api" + "github.com/matrix-org/dendrite/common" "github.com/matrix-org/dendrite/roomserver/types" "github.com/matrix-org/util" "sort" @@ -200,7 +200,7 @@ func DifferenceBetweeenStateSnapshots(db RoomStateDatabase, oldStateNID, newStat // stringTuplesToNumericTuples converts the string state key tuples into numeric IDs // If there isn't a numeric ID for either the event type or the event state key then the tuple is discarded. // Returns an error if there was a problem talking to the database. -func stringTuplesToNumericTuples(db RoomStateDatabase, stringTuples []api.StateKeyTuple) ([]types.StateKeyTuple, error) { +func stringTuplesToNumericTuples(db RoomStateDatabase, stringTuples []common.StateKeyTuple) ([]types.StateKeyTuple, error) { eventTypes := make([]string, len(stringTuples)) stateKeys := make([]string, len(stringTuples)) for i := range stringTuples { @@ -239,7 +239,7 @@ func stringTuplesToNumericTuples(db RoomStateDatabase, stringTuples []api.StateK // This is typically the state before an event or the current state of a room. // Returns a sorted list of state entries or an error if there was a problem talking to the database. func LoadStateAtSnapshotForStringTuples( - db RoomStateDatabase, stateNID types.StateSnapshotNID, stateKeyTuples []api.StateKeyTuple, + db RoomStateDatabase, stateNID types.StateSnapshotNID, stateKeyTuples []common.StateKeyTuple, ) ([]types.StateEntry, error) { numericTuples, err := stringTuplesToNumericTuples(db, stateKeyTuples) if err != nil { diff --git a/vendor/manifest b/vendor/manifest index 6a3e8ee53..5435122fa 100644 --- a/vendor/manifest +++ b/vendor/manifest @@ -92,7 +92,7 @@ { "importpath": "github.com/matrix-org/gomatrixserverlib", "repository": "https://github.com/matrix-org/gomatrixserverlib", - "revision": "ce2ae9c5812346444b0ca75d57834794cde03fb7", + "revision": "4218890fdd60e73cc5539ec40b86fd51568f4a19", "branch": "master" }, { @@ -206,4 +206,4 @@ "branch": "master" } ] -} \ No newline at end of file +} diff --git a/vendor/src/github.com/matrix-org/gomatrixserverlib/event.go b/vendor/src/github.com/matrix-org/gomatrixserverlib/event.go index e1b6ee7d8..439642a31 100644 --- a/vendor/src/github.com/matrix-org/gomatrixserverlib/event.go +++ b/vendor/src/github.com/matrix-org/gomatrixserverlib/event.go @@ -100,7 +100,7 @@ func (eb *EventBuilder) Build(eventID string, now time.Time, origin, keyID strin EventBuilder EventID string `json:"event_id"` RawContent rawJSON `json:"content"` - RawUnsigned rawJSON `json:"unsigned"` + RawUnsigned rawJSON `json:"unsigned,omitempty"` OriginServerTS int64 `json:"origin_server_ts"` Origin string `json:"origin"` }