Create the initial set of state events for room creation
MODIFY
src/github.com/matrix-org/dendrite/clientapi/clientapi.go
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))
}

ADD
src/github.com/matrix-org/dendrite/clientapi/config/config.go
+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
+}

ADD
src/github.com/matrix-org/dendrite/clientapi/events/eventcontent.go
+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},
+ }
+}

RENAME
src/github.com/matrix-org/dendrite/clientapi/common/common.go
src/github.com/matrix-org/dendrite/clientapi/httputil/httputil.go
-package common
+package httputil
import (
"encoding/json"

MODIFY
src/github.com/matrix-org/dendrite/clientapi/routing/routing.go
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}
-}

MODIFY
src/github.com/matrix-org/dendrite/clientapi/writers/createroom.go
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
+ }
+
+ authEvents := authEventProvider{builtEventMap}
+ for i, e := range eventsToMake {
+ depth := i + 1 // depth starts at 1
+
+ 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) {
- // 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
+ // 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
+}
- return util.MessageResponse(404, "Not implemented yet")
+func (a *authEventProvider) ThirdPartyInvite(stateKey string) (ev *gomatrixserverlib.Event, err error) {
+ return a.events[common.StateKeyTuple{"m.room.third_party_invite", stateKey}], nil
}

ADD
src/github.com/matrix-org/dendrite/common/types.go
+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
+}

MODIFY
src/github.com/matrix-org/dendrite/roomserver/api/query.go
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

MODIFY
src/github.com/matrix-org/dendrite/roomserver/roomserver-integration-tests/main.go
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"},
},
},

MODIFY
src/github.com/matrix-org/dendrite/roomserver/state/state.go
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 {

MODIFY
vendor/manifest
vendor/manifest
{
"importpath": "github.com/matrix-org/gomatrixserverlib",
"repository": "https://github.com/matrix-org/gomatrixserverlib",
- "revision": "ce2ae9c5812346444b0ca75d57834794cde03fb7",
+ "revision": "4218890fdd60e73cc5539ec40b86fd51568f4a19",
"branch": "master"
},
{

MODIFY
vendor/src/github.com/matrix-org/gomatrixserverlib/event.go
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"`
}

Create the initial set of state events for room creation (#32)