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

main
Kegsay 2017-03-10 11:32:53 +00:00 committed by GitHub
parent e667f17e14
commit 49ed708ca4
12 changed files with 269 additions and 53 deletions

View File

@ -5,6 +5,9 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"golang.org/x/crypto/ed25519"
"github.com/matrix-org/dendrite/clientapi/config"
"github.com/matrix-org/dendrite/clientapi/routing" "github.com/matrix-org/dendrite/clientapi/routing"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
@ -36,6 +39,16 @@ func main() {
setupLogging(logDir) setupLogging(logDir)
} }
log.Info("Starting clientapi") 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)) log.Fatal(http.ListenAndServe(bindAddr, nil))
} }

View File

@ -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
}

View File

@ -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},
}
}

View File

@ -1,4 +1,4 @@
package common package httputil
import ( import (
"encoding/json" "encoding/json"

View File

@ -4,6 +4,7 @@ import (
"net/http" "net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/matrix-org/dendrite/clientapi/config"
"github.com/matrix-org/dendrite/clientapi/readers" "github.com/matrix-org/dendrite/clientapi/readers"
"github.com/matrix-org/dendrite/clientapi/writers" "github.com/matrix-org/dendrite/clientapi/writers"
"github.com/matrix-org/util" "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 // Setup registers HTTP handlers with the given ServeMux. It also supplies the given http.Client
// to clients which need to make outbound HTTP requests. // 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() apiMux := mux.NewRouter()
r0mux := apiMux.PathPrefix(pathPrefixR0).Subrouter() r0mux := apiMux.PathPrefix(pathPrefixR0).Subrouter()
r0mux.Handle("/createRoom", make("createRoom", wrap(func(req *http.Request) util.JSONResponse { r0mux.Handle("/createRoom", make("createRoom", util.NewJSONRequestHandler(func(req *http.Request) util.JSONResponse {
return writers.CreateRoom(req) 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) return readers.Sync(req)
}))) })))
r0mux.Handle("/rooms/{roomID}/send/{eventType}", 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) vars := mux.Vars(req)
return writers.SendMessage(req, vars["roomID"], vars["eventType"]) 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 { func make(metricsName string, h util.JSONRequestHandler) http.Handler {
return prometheus.InstrumentHandler(metricsName, util.MakeJSONAPI(h)) 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}
}

View File

@ -5,11 +5,16 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
"time"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"github.com/matrix-org/dendrite/clientapi/auth" "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/clientapi/jsonerror"
"github.com/matrix-org/dendrite/common"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util" "github.com/matrix-org/util"
) )
@ -64,24 +69,30 @@ type createRoomResponse struct {
RoomAlias string `json:"room_alias,omitempty"` // in synapse not spec 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 // CreateRoom implements /createRoom
func CreateRoom(req *http.Request) util.JSONResponse { func CreateRoom(req *http.Request, cfg config.ClientAPI) util.JSONResponse {
serverName := "localhost"
// TODO: Check room ID doesn't clash with an existing one, and we // TODO: Check room ID doesn't clash with an existing one, and we
// probably shouldn't be using pseudo-random strings, maybe GUIDs? // probably shouldn't be using pseudo-random strings, maybe GUIDs?
roomID := fmt.Sprintf("!%s:%s", util.RandomString(16), serverName) roomID := fmt.Sprintf("!%s:%s", util.RandomString(16), cfg.ServerName)
return createRoom(req, roomID) return createRoom(req, cfg, roomID)
} }
// createRoom implements /createRoom // 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()) logger := util.GetLogger(req.Context())
userID, resErr := auth.VerifyAccessToken(req) userID, resErr := auth.VerifyAccessToken(req)
if resErr != nil { if resErr != nil {
return *resErr return *resErr
} }
var r createRoomRequest var r createRoomRequest
resErr = common.UnmarshalJSONRequest(req, &r) resErr = httputil.UnmarshalJSONRequest(req, &r)
if resErr != nil { if resErr != nil {
return *resErr return *resErr
} }
@ -98,7 +109,11 @@ func createRoom(req *http.Request, roomID string) util.JSONResponse {
logger.WithFields(log.Fields{ logger.WithFields(log.Fields{
"userID": userID, "userID": userID,
"roomID": roomID, "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: // send events into the room in order of:
// 1- m.room.create // 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 // 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 // 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. // 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: authEvents := authEventProvider{builtEventMap}
// - validate required keys/types (EventValidator in synapse) for i, e := range eventsToMake {
// - set additional keys (displayname/avatar_url for m.room.member) depth := i + 1 // depth starts at 1
// - set token(?) and txn id
// - then https://github.com/matrix-org/synapse/blob/v0.19.2/synapse/handlers/message.py#L419
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
} }

View File

@ -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
}

View File

@ -4,29 +4,18 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/matrix-org/dendrite/common"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
"net/http" "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 // QueryLatestEventsAndStateRequest is a request to QueryLatestEventsAndState
type QueryLatestEventsAndStateRequest struct { type QueryLatestEventsAndStateRequest struct {
// The roomID to query the latest events for. // The roomID to query the latest events for.
RoomID string RoomID string
// The state key tuples to fetch from the room current state. // The state key tuples to fetch from the room current state.
// If this list is empty or nil then no state events are returned. // If this list is empty or nil then no state events are returned.
StateToFetch []StateKeyTuple StateToFetch []common.StateKeyTuple
} }
// QueryLatestEventsAndStateResponse is a response to QueryLatestEventsAndState // QueryLatestEventsAndStateResponse is a response to QueryLatestEventsAndState

View File

@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"github.com/matrix-org/dendrite/common"
"github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
"os" "os"
@ -367,7 +368,7 @@ func main() {
if err := q.QueryLatestEventsAndState( if err := q.QueryLatestEventsAndState(
&api.QueryLatestEventsAndStateRequest{ &api.QueryLatestEventsAndStateRequest{
RoomID: "!HCXfdvrfksxuYnIFiJ:matrix.org", RoomID: "!HCXfdvrfksxuYnIFiJ:matrix.org",
StateToFetch: []api.StateKeyTuple{ StateToFetch: []common.StateKeyTuple{
{"m.room.member", "@richvdh:matrix.org"}, {"m.room.member", "@richvdh:matrix.org"},
}, },
}, },

View File

@ -4,7 +4,7 @@ package state
import ( import (
"fmt" "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/dendrite/roomserver/types"
"github.com/matrix-org/util" "github.com/matrix-org/util"
"sort" "sort"
@ -200,7 +200,7 @@ func DifferenceBetweeenStateSnapshots(db RoomStateDatabase, oldStateNID, newStat
// stringTuplesToNumericTuples converts the string state key tuples into numeric IDs // 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. // 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. // 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)) eventTypes := make([]string, len(stringTuples))
stateKeys := make([]string, len(stringTuples)) stateKeys := make([]string, len(stringTuples))
for i := range 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. // 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. // Returns a sorted list of state entries or an error if there was a problem talking to the database.
func LoadStateAtSnapshotForStringTuples( func LoadStateAtSnapshotForStringTuples(
db RoomStateDatabase, stateNID types.StateSnapshotNID, stateKeyTuples []api.StateKeyTuple, db RoomStateDatabase, stateNID types.StateSnapshotNID, stateKeyTuples []common.StateKeyTuple,
) ([]types.StateEntry, error) { ) ([]types.StateEntry, error) {
numericTuples, err := stringTuplesToNumericTuples(db, stateKeyTuples) numericTuples, err := stringTuplesToNumericTuples(db, stateKeyTuples)
if err != nil { if err != nil {

2
vendor/manifest vendored
View File

@ -92,7 +92,7 @@
{ {
"importpath": "github.com/matrix-org/gomatrixserverlib", "importpath": "github.com/matrix-org/gomatrixserverlib",
"repository": "https://github.com/matrix-org/gomatrixserverlib", "repository": "https://github.com/matrix-org/gomatrixserverlib",
"revision": "ce2ae9c5812346444b0ca75d57834794cde03fb7", "revision": "4218890fdd60e73cc5539ec40b86fd51568f4a19",
"branch": "master" "branch": "master"
}, },
{ {

View File

@ -100,7 +100,7 @@ func (eb *EventBuilder) Build(eventID string, now time.Time, origin, keyID strin
EventBuilder EventBuilder
EventID string `json:"event_id"` EventID string `json:"event_id"`
RawContent rawJSON `json:"content"` RawContent rawJSON `json:"content"`
RawUnsigned rawJSON `json:"unsigned"` RawUnsigned rawJSON `json:"unsigned,omitempty"`
OriginServerTS int64 `json:"origin_server_ts"` OriginServerTS int64 `json:"origin_server_ts"`
Origin string `json:"origin"` Origin string `json:"origin"`
} }