From 025f78eb0189505d2d30ba7d7cc62548167365c4 Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Fri, 21 Apr 2023 16:35:44 -0600 Subject: [PATCH 01/18] Move matrix join logic to gmsl --- authchain.go | 6 +- authchain_test.go | 2 +- authstate.go | 12 +- eventversion.go | 8 ++ fclient/federationclient.go | 4 +- fclient/sendjoin.go | 238 ++++++++++++++++++++++++++++++++++++ load.go | 4 +- 7 files changed, 260 insertions(+), 14 deletions(-) create mode 100644 fclient/sendjoin.go diff --git a/authchain.go b/authchain.go index 9c888f60..1662e672 100644 --- a/authchain.go +++ b/authchain.go @@ -5,8 +5,8 @@ import ( "fmt" ) -// AuthChainProvider returns the requested list of auth events. -type AuthChainProvider func(roomVer RoomVersion, eventIDs []string) ([]*Event, error) +// EventProvider returns the requested list of events. +type EventProvider func(roomVer RoomVersion, eventIDs []string) ([]*Event, error) // VerifyEventAuthChain will verify that the event is allowed according to its auth_events, and then // recursively verify each of those auth_events. @@ -19,7 +19,7 @@ type AuthChainProvider func(roomVer RoomVersion, eventIDs []string) ([]*Event, e // The `provideEvents` function will only be called for *new* events rather than for everything as it is // assumed that this function is costly. Failing to provide all the requested events will fail this function. // Returning an error from `provideEvents` will also fail this function. -func VerifyEventAuthChain(ctx context.Context, eventToVerify *HeaderedEvent, provideEvents AuthChainProvider) error { +func VerifyEventAuthChain(ctx context.Context, eventToVerify *HeaderedEvent, provideEvents EventProvider) error { eventsByID := make(map[string]*Event) // A lookup table for verifying this auth chain evv := eventToVerify.Unwrap() eventsByID[evv.EventID()] = evv diff --git a/authchain_test.go b/authchain_test.go index e345bfcf..1bb05fbf 100644 --- a/authchain_test.go +++ b/authchain_test.go @@ -70,7 +70,7 @@ func TestVerifyEventAuthChainCascadeFailure(t *testing.T) { } } -func provideEvents(t *testing.T, events [][]byte) gomatrixserverlib.AuthChainProvider { +func provideEvents(t *testing.T, events [][]byte) gomatrixserverlib.EventProvider { eventMap := make(map[string]*gomatrixserverlib.Event) for _, eventBytes := range events { ev, err := gomatrixserverlib.MustGetRoomVersion(gomatrixserverlib.RoomVersionV1).NewEventFromTrustedJSON(eventBytes, false) diff --git a/authstate.go b/authstate.go index 7f2524d4..5cdc3479 100644 --- a/authstate.go +++ b/authstate.go @@ -163,7 +163,7 @@ func VerifyAuthRulesAtState(ctx context.Context, sp StateProvider, eventToVerify func checkAllowedByAuthEvents( event *Event, eventsByID map[string]*Event, - missingAuth AuthChainProvider, + missingAuth EventProvider, ) error { authEvents := NewAuthEvents(nil) @@ -173,7 +173,7 @@ func checkAllowedByAuthEvents( if !ok { // We don't have an entry in the eventsByID map - neither an event nor nil. if missingAuth != nil { - // If we have a AuthChainProvider then ask it for the missing event. + // If we have a EventProvider then ask it for the missing event. if ev, err := missingAuth(event.Version(), []string{ae}); err == nil && len(ev) > 0 { // It claims to have returned events - populate the eventsByID // map and the authEvents provider so that we can retry with the @@ -193,7 +193,7 @@ func checkAllowedByAuthEvents( } goto retryEvent } else { - // If we didn't have a AuthChainProvider then we can't get the event + // If we didn't have a EventProvider then we can't get the event // so just carry on without it. If it was important for anything then // Check() below will catch it. continue @@ -206,7 +206,7 @@ func checkAllowedByAuthEvents( } } else { // We had an entry in the map but it contains nil, which means that we tried - // to use the AuthChainProvider to retrieve it and failed, so at this point + // to use the EventProvider to retrieve it and failed, so at this point // we just have to ignore the event. continue } @@ -229,7 +229,7 @@ func checkAllowedByAuthEvents( // return parameter). Does not alter any input args. func CheckStateResponse( ctx context.Context, r StateResponse, roomVersion RoomVersion, - keyRing JSONVerifier, missingAuth AuthChainProvider, + keyRing JSONVerifier, missingAuth EventProvider, ) ([]*Event, []*Event, error) { logger := util.GetLogger(ctx) authEvents := r.GetAuthEvents().UntrustedEvents(roomVersion) @@ -322,7 +322,7 @@ func CheckStateResponse( func CheckSendJoinResponse( ctx context.Context, roomVersion RoomVersion, r StateResponse, keyRing JSONVerifier, joinEvent *Event, - missingAuth AuthChainProvider, + missingAuth EventProvider, ) (StateResponse, error) { // First check that the state is valid and that the events in the response // are correctly signed. diff --git a/eventversion.go b/eventversion.go index a5cf5d93..437d3af2 100644 --- a/eventversion.go +++ b/eventversion.go @@ -307,6 +307,14 @@ func StableRoomVersions() map[RoomVersion]IRoomVersion { return versions } +func RoomVersionsToList(versions map[RoomVersion]IRoomVersion) []RoomVersion { + var supportedVersions []RoomVersion + for version := range versions { + supportedVersions = append(supportedVersions, version) + } + return supportedVersions +} + // RoomVersionDescription contains information about a room version, // namely whether it is marked as supported or stable in this server // version, along with the state resolution algorithm, event ID etc diff --git a/fclient/federationclient.go b/fclient/federationclient.go index 55a8d370..4afeeea6 100644 --- a/fclient/federationclient.go +++ b/fclient/federationclient.go @@ -26,7 +26,7 @@ type FederationClient interface { // Perform operations LookupRoomAlias(ctx context.Context, origin, s spec.ServerName, roomAlias string) (res RespDirectory, err error) Peek(ctx context.Context, origin, s spec.ServerName, roomID, peekID string, roomVersions []gomatrixserverlib.RoomVersion) (res RespPeek, err error) - MakeJoin(ctx context.Context, origin, s spec.ServerName, roomID, userID string, roomVersions []gomatrixserverlib.RoomVersion) (res RespMakeJoin, err error) + MakeJoin(ctx context.Context, origin, s spec.ServerName, roomID, userID string) (res RespMakeJoin, err error) SendJoin(ctx context.Context, origin, s spec.ServerName, event *gomatrixserverlib.Event) (res RespSendJoin, err error) MakeLeave(ctx context.Context, origin, s spec.ServerName, roomID, userID string) (res RespMakeLeave, err error) SendLeave(ctx context.Context, origin, s spec.ServerName, event *gomatrixserverlib.Event) (err error) @@ -191,8 +191,8 @@ func makeVersionQueryString(roomVersions []gomatrixserverlib.RoomVersion) string // See https://matrix.org/docs/spec/server_server/unstable.html#joining-rooms func (ac *federationClient) MakeJoin( ctx context.Context, origin, s spec.ServerName, roomID, userID string, - roomVersions []gomatrixserverlib.RoomVersion, ) (res RespMakeJoin, err error) { + roomVersions := gomatrixserverlib.RoomVersionsToList(gomatrixserverlib.StableRoomVersions()) versionQueryString := makeVersionQueryString(roomVersions) path := federationPathPrefixV1 + "/make_join/" + url.PathEscape(roomID) + "/" + diff --git a/fclient/sendjoin.go b/fclient/sendjoin.go new file mode 100644 index 00000000..831d8f37 --- /dev/null +++ b/fclient/sendjoin.go @@ -0,0 +1,238 @@ +package fclient + +import ( + "context" + "crypto/ed25519" + "encoding/json" + "fmt" + "time" + + "github.com/matrix-org/gomatrixserverlib" + "github.com/matrix-org/gomatrixserverlib/spec" + "github.com/sirupsen/logrus" +) + +type SendJoinInput struct { + UserID *spec.UserID + RoomID string + ServerName spec.ServerName + Content map[string]interface{} + Unsigned map[string]interface{} + + PrivateKey ed25519.PrivateKey + KeyID gomatrixserverlib.KeyID + KeyRing *gomatrixserverlib.KeyRing + + EventProvider func( + ctx context.Context, fedClient FederationClient, + keyRing gomatrixserverlib.JSONVerifier, origin, server spec.ServerName, + ) gomatrixserverlib.EventProvider +} + +type SendJoinCallbacks struct { + FederationFailure func(serverName spec.ServerName) + FederationSuccess func(serverName spec.ServerName) +} + +func HandleSendJoin( + ctx context.Context, + fedClient FederationClient, + input SendJoinInput, + callbacks SendJoinCallbacks, +) (*gomatrixserverlib.HeaderedEvent, gomatrixserverlib.StateResponse, error) { + origin := input.UserID.Domain() + + // Try to perform a make_join using the information supplied in the + // request. + respMakeJoin, err := fedClient.MakeJoin( + ctx, + origin, + input.ServerName, + input.RoomID, + input.UserID.Raw(), + ) + if err != nil { + // TODO: Check if the user was not allowed to join the room. + callbacks.FederationFailure(input.ServerName) + return nil, nil, fmt.Errorf("r.federation.MakeJoin: %w", err) + } + callbacks.FederationSuccess(input.ServerName) + + // Set all the fields to be what they should be, this should be a no-op + // but it's possible that the remote server returned us something "odd" + stateKey := input.UserID.Raw() + respMakeJoin.JoinEvent.Type = spec.MRoomMember + respMakeJoin.JoinEvent.Sender = input.UserID.Raw() + respMakeJoin.JoinEvent.StateKey = &stateKey + respMakeJoin.JoinEvent.RoomID = input.RoomID + respMakeJoin.JoinEvent.Redacts = "" + if input.Content == nil { + input.Content = map[string]interface{}{} + } + _ = json.Unmarshal(respMakeJoin.JoinEvent.Content, &input.Content) + input.Content["membership"] = spec.Join + if err = respMakeJoin.JoinEvent.SetContent(input.Content); err != nil { + return nil, nil, fmt.Errorf("respMakeJoin.JoinEvent.SetContent: %w", err) + } + if err = respMakeJoin.JoinEvent.SetUnsigned(struct{}{}); err != nil { + return nil, nil, fmt.Errorf("respMakeJoin.JoinEvent.SetUnsigned: %w", err) + } + + // Work out if we support the room version that has been supplied in + // the make_join response. + // "If not provided, the room version is assumed to be either "1" or "2"." + // https://matrix.org/docs/spec/server_server/unstable#get-matrix-federation-v1-make-join-roomid-userid + if respMakeJoin.RoomVersion == "" { + respMakeJoin.RoomVersion = setDefaultRoomVersionFromJoinEvent(respMakeJoin.JoinEvent) + } + verImpl, err := gomatrixserverlib.GetRoomVersion(respMakeJoin.RoomVersion) + if err != nil { + return nil, nil, err + } + + // Build the join event. + event, err := respMakeJoin.JoinEvent.Build( + time.Now(), + origin, + input.KeyID, + input.PrivateKey, + respMakeJoin.RoomVersion, + ) + if err != nil { + return nil, nil, fmt.Errorf("respMakeJoin.JoinEvent.Build: %w", err) + } + + var respState gomatrixserverlib.StateResponse + // Try to perform a send_join using the newly built event. + respSendJoin, err := fedClient.SendJoin( + context.Background(), + origin, + input.ServerName, + event, + ) + if err != nil { + callbacks.FederationFailure(input.ServerName) + return nil, nil, fmt.Errorf("r.federation.SendJoin: %w", err) + } + callbacks.FederationSuccess(input.ServerName) + + // If the remote server returned an event in the "event" key of + // the send_join request then we should use that instead. It may + // contain signatures that we don't know about. + if len(respSendJoin.Event) > 0 { + var remoteEvent *gomatrixserverlib.Event + remoteEvent, err = verImpl.NewEventFromUntrustedJSON(respSendJoin.Event) + if err == nil && isWellFormedMembershipEvent( + remoteEvent, input.RoomID, input.UserID, + ) { + event = remoteEvent + } + } + + // Sanity-check the join response to ensure that it has a create + // event, that the room version is known, etc. + authEvents := respSendJoin.AuthEvents.UntrustedEvents(respMakeJoin.RoomVersion) + if err = checkEventsContainCreateEvent(authEvents); err != nil { + return nil, nil, fmt.Errorf("sanityCheckAuthChain: %w", err) + } + + // Process the join response in a goroutine. The idea here is + // that we'll try and wait for as long as possible for the work + // to complete, but if the client does give up waiting, we'll + // still continue to process the join anyway so that we don't + // waste the effort. + // TODO: Can we expand Check here to return a list of missing auth + // events rather than failing one at a time? + respState, err = gomatrixserverlib.CheckSendJoinResponse( + context.Background(), + respMakeJoin.RoomVersion, &respSendJoin, + input.KeyRing, + event, + input.EventProvider(ctx, fedClient, input.KeyRing, origin, input.ServerName), + ) + if err != nil { + return nil, nil, fmt.Errorf("respSendJoin.Check: %w", err) + } + + // If we successfully performed a send_join above then the other + // server now thinks we're a part of the room. Send the newly + // returned state to the roomserver to update our local view. + if input.Unsigned != nil { + event, err = event.SetUnsigned(input.Unsigned) + if err != nil { + // non-fatal, log and continue + logrus.WithError(err).Errorf("Failed to set unsigned content") + } + } + + return event.Headered(respMakeJoin.RoomVersion), respState, nil +} + +func setDefaultRoomVersionFromJoinEvent( + joinEvent gomatrixserverlib.EventBuilder, +) gomatrixserverlib.RoomVersion { + // if auth events are not event references we know it must be v3+ + // we have to do these shenanigans to satisfy sytest, specifically for: + // "Outbound federation rejects m.room.create events with an unknown room version" + hasEventRefs := true + authEvents, ok := joinEvent.AuthEvents.([]interface{}) + if ok { + if len(authEvents) > 0 { + _, ok = authEvents[0].(string) + if ok { + // event refs are objects, not strings, so we know we must be dealing with a v3+ room. + hasEventRefs = false + } + } + } + + if hasEventRefs { + return gomatrixserverlib.RoomVersionV1 + } + return gomatrixserverlib.RoomVersionV4 +} + +// isWellFormedMembershipEvent returns true if the event looks like a legitimate +// membership event. +func isWellFormedMembershipEvent(event *gomatrixserverlib.Event, roomID string, userID *spec.UserID) bool { + if membership, err := event.Membership(); err != nil { + return false + } else if membership != spec.Join { + return false + } + if event.RoomID() != roomID { + return false + } + if !event.StateKeyEquals(userID.Raw()) { + return false + } + return true +} + +func checkEventsContainCreateEvent(events []*gomatrixserverlib.Event) error { + // sanity check we have a create event and it has a known room version + for _, ev := range events { + if ev.Type() == spec.MRoomCreate && ev.StateKeyEquals("") { + // make sure the room version is known + content := ev.Content() + verBody := struct { + Version string `json:"room_version"` + }{} + err := json.Unmarshal(content, &verBody) + if err != nil { + return err + } + if verBody.Version == "" { + // https://matrix.org/docs/spec/client_server/r0.6.0#m-room-create + // The version of the room. Defaults to "1" if the key does not exist. + verBody.Version = "1" + } + knownVersions := gomatrixserverlib.RoomVersions() + if _, ok := knownVersions[gomatrixserverlib.RoomVersion(verBody.Version)]; !ok { + return fmt.Errorf("m.room.create event has an unknown room version: %s", verBody.Version) + } + return nil + } + } + return fmt.Errorf("response is missing m.room.create event") +} diff --git a/load.go b/load.go index 44f6e5af..69b44ed6 100644 --- a/load.go +++ b/load.go @@ -18,7 +18,7 @@ type EventLoadResult struct { type EventsLoader struct { roomVer RoomVersion keyRing JSONVerifier - provider AuthChainProvider + provider EventProvider stateProvider StateProvider // Set to true to do: // 6. Passes authorization rules based on the current state of the room, otherwise it is "soft failed". @@ -27,7 +27,7 @@ type EventsLoader struct { } // NewEventsLoader returns a new events loader -func NewEventsLoader(roomVer RoomVersion, keyRing JSONVerifier, stateProvider StateProvider, provider AuthChainProvider, performSoftFailCheck bool) *EventsLoader { +func NewEventsLoader(roomVer RoomVersion, keyRing JSONVerifier, stateProvider StateProvider, provider EventProvider, performSoftFailCheck bool) *EventsLoader { return &EventsLoader{ roomVer: roomVer, keyRing: keyRing, From 52d1bda7a912d4af489abd5d10fdd06fd3549a3d Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Mon, 24 Apr 2023 17:01:00 -0600 Subject: [PATCH 02/18] Move sendjoin to performjoin since that is what it actually does --- fclient/{sendjoin.go => performjoin.go} | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename fclient/{sendjoin.go => performjoin.go} (98%) diff --git a/fclient/sendjoin.go b/fclient/performjoin.go similarity index 98% rename from fclient/sendjoin.go rename to fclient/performjoin.go index 831d8f37..4a13b46f 100644 --- a/fclient/sendjoin.go +++ b/fclient/performjoin.go @@ -12,7 +12,7 @@ import ( "github.com/sirupsen/logrus" ) -type SendJoinInput struct { +type PerformJoinInput struct { UserID *spec.UserID RoomID string ServerName spec.ServerName @@ -29,16 +29,16 @@ type SendJoinInput struct { ) gomatrixserverlib.EventProvider } -type SendJoinCallbacks struct { +type PerformJoinCallbacks struct { FederationFailure func(serverName spec.ServerName) FederationSuccess func(serverName spec.ServerName) } -func HandleSendJoin( +func PerformJoin( ctx context.Context, fedClient FederationClient, - input SendJoinInput, - callbacks SendJoinCallbacks, + input PerformJoinInput, + callbacks PerformJoinCallbacks, ) (*gomatrixserverlib.HeaderedEvent, gomatrixserverlib.StateResponse, error) { origin := input.UserID.Domain() From c3ebcd085238ecd8e92f3cee67cd4df3e4d9dbc9 Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Mon, 24 Apr 2023 17:56:05 -0600 Subject: [PATCH 03/18] Document PerformJoin --- fclient/performjoin.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fclient/performjoin.go b/fclient/performjoin.go index 4a13b46f..03747250 100644 --- a/fclient/performjoin.go +++ b/fclient/performjoin.go @@ -34,6 +34,9 @@ type PerformJoinCallbacks struct { FederationSuccess func(serverName spec.ServerName) } +// PerformJoin provides high level functionality that will attempt a federated room +// join. On success it will return the new join event and the state snapshot returned +// as part of the join. func PerformJoin( ctx context.Context, fedClient FederationClient, From 78fc2c747b7dde9d50b1bb2ca90781264f4828ed Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Tue, 25 Apr 2023 09:23:48 -0600 Subject: [PATCH 04/18] Remove extra wrapper around EventProvider --- fclient/performjoin.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/fclient/performjoin.go b/fclient/performjoin.go index 03747250..0637c8d4 100644 --- a/fclient/performjoin.go +++ b/fclient/performjoin.go @@ -23,10 +23,7 @@ type PerformJoinInput struct { KeyID gomatrixserverlib.KeyID KeyRing *gomatrixserverlib.KeyRing - EventProvider func( - ctx context.Context, fedClient FederationClient, - keyRing gomatrixserverlib.JSONVerifier, origin, server spec.ServerName, - ) gomatrixserverlib.EventProvider + EventProvider gomatrixserverlib.EventProvider } type PerformJoinCallbacks struct { @@ -151,7 +148,7 @@ func PerformJoin( respMakeJoin.RoomVersion, &respSendJoin, input.KeyRing, event, - input.EventProvider(ctx, fedClient, input.KeyRing, origin, input.ServerName), + input.EventProvider, ) if err != nil { return nil, nil, fmt.Errorf("respSendJoin.Check: %w", err) From e09387987e041ce60ccb09c7eb0691cd4cbd74f9 Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Tue, 25 Apr 2023 09:25:27 -0600 Subject: [PATCH 05/18] Rename well formed check for join events --- fclient/performjoin.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fclient/performjoin.go b/fclient/performjoin.go index 0637c8d4..c18a2333 100644 --- a/fclient/performjoin.go +++ b/fclient/performjoin.go @@ -122,7 +122,7 @@ func PerformJoin( if len(respSendJoin.Event) > 0 { var remoteEvent *gomatrixserverlib.Event remoteEvent, err = verImpl.NewEventFromUntrustedJSON(respSendJoin.Event) - if err == nil && isWellFormedMembershipEvent( + if err == nil && isWellFormedJoinEvent( remoteEvent, input.RoomID, input.UserID, ) { event = remoteEvent @@ -192,9 +192,9 @@ func setDefaultRoomVersionFromJoinEvent( return gomatrixserverlib.RoomVersionV4 } -// isWellFormedMembershipEvent returns true if the event looks like a legitimate +// isWellFormedJoinEvent returns true if the event looks like a legitimate // membership event. -func isWellFormedMembershipEvent(event *gomatrixserverlib.Event, roomID string, userID *spec.UserID) bool { +func isWellFormedJoinEvent(event *gomatrixserverlib.Event, roomID string, userID *spec.UserID) bool { if membership, err := event.Membership(); err != nil { return false } else if membership != spec.Join { From 981aaaa05bdedb2ba33fee618ecabc84ffc77e1f Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Tue, 25 Apr 2023 09:26:09 -0600 Subject: [PATCH 06/18] Rename well formed check for join member events --- fclient/performjoin.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fclient/performjoin.go b/fclient/performjoin.go index c18a2333..c04d1955 100644 --- a/fclient/performjoin.go +++ b/fclient/performjoin.go @@ -122,7 +122,7 @@ func PerformJoin( if len(respSendJoin.Event) > 0 { var remoteEvent *gomatrixserverlib.Event remoteEvent, err = verImpl.NewEventFromUntrustedJSON(respSendJoin.Event) - if err == nil && isWellFormedJoinEvent( + if err == nil && isWellFormedJoinMemberEvent( remoteEvent, input.RoomID, input.UserID, ) { event = remoteEvent @@ -192,9 +192,9 @@ func setDefaultRoomVersionFromJoinEvent( return gomatrixserverlib.RoomVersionV4 } -// isWellFormedJoinEvent returns true if the event looks like a legitimate +// isWellFormedJoinMemberEvent returns true if the event looks like a legitimate // membership event. -func isWellFormedJoinEvent(event *gomatrixserverlib.Event, roomID string, userID *spec.UserID) bool { +func isWellFormedJoinMemberEvent(event *gomatrixserverlib.Event, roomID string, userID *spec.UserID) bool { if membership, err := event.Membership(); err != nil { return false } else if membership != spec.Join { From 39217c38c899ab343e3eae5cac3215675b0a4fab Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Tue, 25 Apr 2023 09:31:43 -0600 Subject: [PATCH 07/18] Update join comments to reflect the code --- fclient/performjoin.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/fclient/performjoin.go b/fclient/performjoin.go index c04d1955..691db373 100644 --- a/fclient/performjoin.go +++ b/fclient/performjoin.go @@ -136,11 +136,10 @@ func PerformJoin( return nil, nil, fmt.Errorf("sanityCheckAuthChain: %w", err) } - // Process the join response in a goroutine. The idea here is - // that we'll try and wait for as long as possible for the work - // to complete, but if the client does give up waiting, we'll - // still continue to process the join anyway so that we don't - // waste the effort. + // Process the send_join response. The idea here is that we'll try and wait + // for as long as possible for the work to complete by using a background + // context instead of the provided ctx. If the client does give up waiting, + // we'll still continue to process the join anyway so that we don't waste the effort. // TODO: Can we expand Check here to return a list of missing auth // events rather than failing one at a time? respState, err = gomatrixserverlib.CheckSendJoinResponse( From bd039109fd695747839aa03db55c4977646fda32 Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Tue, 25 Apr 2023 09:41:21 -0600 Subject: [PATCH 08/18] Move join response params into a struct --- fclient/performjoin.go | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/fclient/performjoin.go b/fclient/performjoin.go index 691db373..f9200870 100644 --- a/fclient/performjoin.go +++ b/fclient/performjoin.go @@ -31,6 +31,11 @@ type PerformJoinCallbacks struct { FederationSuccess func(serverName spec.ServerName) } +type PerformJoinResponse struct { + JoinEvent *gomatrixserverlib.HeaderedEvent + StateSnapshot gomatrixserverlib.StateResponse +} + // PerformJoin provides high level functionality that will attempt a federated room // join. On success it will return the new join event and the state snapshot returned // as part of the join. @@ -39,7 +44,7 @@ func PerformJoin( fedClient FederationClient, input PerformJoinInput, callbacks PerformJoinCallbacks, -) (*gomatrixserverlib.HeaderedEvent, gomatrixserverlib.StateResponse, error) { +) (*PerformJoinResponse, error) { origin := input.UserID.Domain() // Try to perform a make_join using the information supplied in the @@ -54,7 +59,7 @@ func PerformJoin( if err != nil { // TODO: Check if the user was not allowed to join the room. callbacks.FederationFailure(input.ServerName) - return nil, nil, fmt.Errorf("r.federation.MakeJoin: %w", err) + return nil, fmt.Errorf("r.federation.MakeJoin: %w", err) } callbacks.FederationSuccess(input.ServerName) @@ -72,10 +77,10 @@ func PerformJoin( _ = json.Unmarshal(respMakeJoin.JoinEvent.Content, &input.Content) input.Content["membership"] = spec.Join if err = respMakeJoin.JoinEvent.SetContent(input.Content); err != nil { - return nil, nil, fmt.Errorf("respMakeJoin.JoinEvent.SetContent: %w", err) + return nil, fmt.Errorf("respMakeJoin.JoinEvent.SetContent: %w", err) } if err = respMakeJoin.JoinEvent.SetUnsigned(struct{}{}); err != nil { - return nil, nil, fmt.Errorf("respMakeJoin.JoinEvent.SetUnsigned: %w", err) + return nil, fmt.Errorf("respMakeJoin.JoinEvent.SetUnsigned: %w", err) } // Work out if we support the room version that has been supplied in @@ -87,7 +92,7 @@ func PerformJoin( } verImpl, err := gomatrixserverlib.GetRoomVersion(respMakeJoin.RoomVersion) if err != nil { - return nil, nil, err + return nil, err } // Build the join event. @@ -99,7 +104,7 @@ func PerformJoin( respMakeJoin.RoomVersion, ) if err != nil { - return nil, nil, fmt.Errorf("respMakeJoin.JoinEvent.Build: %w", err) + return nil, fmt.Errorf("respMakeJoin.JoinEvent.Build: %w", err) } var respState gomatrixserverlib.StateResponse @@ -112,7 +117,7 @@ func PerformJoin( ) if err != nil { callbacks.FederationFailure(input.ServerName) - return nil, nil, fmt.Errorf("r.federation.SendJoin: %w", err) + return nil, fmt.Errorf("r.federation.SendJoin: %w", err) } callbacks.FederationSuccess(input.ServerName) @@ -133,7 +138,7 @@ func PerformJoin( // event, that the room version is known, etc. authEvents := respSendJoin.AuthEvents.UntrustedEvents(respMakeJoin.RoomVersion) if err = checkEventsContainCreateEvent(authEvents); err != nil { - return nil, nil, fmt.Errorf("sanityCheckAuthChain: %w", err) + return nil, fmt.Errorf("sanityCheckAuthChain: %w", err) } // Process the send_join response. The idea here is that we'll try and wait @@ -150,7 +155,7 @@ func PerformJoin( input.EventProvider, ) if err != nil { - return nil, nil, fmt.Errorf("respSendJoin.Check: %w", err) + return nil, fmt.Errorf("respSendJoin.Check: %w", err) } // If we successfully performed a send_join above then the other @@ -164,7 +169,10 @@ func PerformJoin( } } - return event.Headered(respMakeJoin.RoomVersion), respState, nil + return &PerformJoinResponse{ + JoinEvent: event.Headered(respMakeJoin.RoomVersion), + StateSnapshot: respState, + }, nil } func setDefaultRoomVersionFromJoinEvent( From 95be11c86dc07c87cafe4bb87effbf0458d2fc0a Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Tue, 25 Apr 2023 12:38:02 -0600 Subject: [PATCH 09/18] Add FederationError type & return with PerformJoin --- errors.go | 18 ++++++++++- fclient/performjoin.go | 68 ++++++++++++++++++++++++++++++------------ 2 files changed, 66 insertions(+), 20 deletions(-) diff --git a/errors.go b/errors.go index 9694b158..a0b63d2a 100644 --- a/errors.go +++ b/errors.go @@ -1,6 +1,10 @@ package gomatrixserverlib -import "fmt" +import ( + "fmt" + + "github.com/matrix-org/gomatrixserverlib/spec" +) // MissingAuthEventError refers to a situation where one of the auth // event for a given event was not found. @@ -27,3 +31,15 @@ func (e BadJSONError) Error() string { func (e BadJSONError) Unwrap() error { return e.err } + +// FederationError contains context surrounding why a federation request may have failed. +type FederationError struct { + ServerName spec.ServerName // The server being contacted. + Transient bool // Whether the failure is permanent (will fail if performed again) or not. + Reachable bool // Whether the server could be contacted. + Err error // The underlying error message. +} + +func (e FederationError) Error() string { + return fmt.Sprintf("FederationError(t=%v, r=%v): %s", e.Transient, e.Reachable, e.Err.Error()) +} diff --git a/fclient/performjoin.go b/fclient/performjoin.go index f9200870..de0ed909 100644 --- a/fclient/performjoin.go +++ b/fclient/performjoin.go @@ -26,11 +26,6 @@ type PerformJoinInput struct { EventProvider gomatrixserverlib.EventProvider } -type PerformJoinCallbacks struct { - FederationFailure func(serverName spec.ServerName) - FederationSuccess func(serverName spec.ServerName) -} - type PerformJoinResponse struct { JoinEvent *gomatrixserverlib.HeaderedEvent StateSnapshot gomatrixserverlib.StateResponse @@ -43,8 +38,7 @@ func PerformJoin( ctx context.Context, fedClient FederationClient, input PerformJoinInput, - callbacks PerformJoinCallbacks, -) (*PerformJoinResponse, error) { +) (*PerformJoinResponse, *gomatrixserverlib.FederationError) { origin := input.UserID.Domain() // Try to perform a make_join using the information supplied in the @@ -58,10 +52,13 @@ func PerformJoin( ) if err != nil { // TODO: Check if the user was not allowed to join the room. - callbacks.FederationFailure(input.ServerName) - return nil, fmt.Errorf("r.federation.MakeJoin: %w", err) + return nil, &gomatrixserverlib.FederationError{ + ServerName: input.ServerName, + Transient: true, + Reachable: false, + Err: fmt.Errorf("r.federation.MakeJoin: %w", err), + } } - callbacks.FederationSuccess(input.ServerName) // Set all the fields to be what they should be, this should be a no-op // but it's possible that the remote server returned us something "odd" @@ -77,10 +74,20 @@ func PerformJoin( _ = json.Unmarshal(respMakeJoin.JoinEvent.Content, &input.Content) input.Content["membership"] = spec.Join if err = respMakeJoin.JoinEvent.SetContent(input.Content); err != nil { - return nil, fmt.Errorf("respMakeJoin.JoinEvent.SetContent: %w", err) + return nil, &gomatrixserverlib.FederationError{ + ServerName: input.ServerName, + Transient: false, + Reachable: true, + Err: fmt.Errorf("respMakeJoin.JoinEvent.SetContent: %w", err), + } } if err = respMakeJoin.JoinEvent.SetUnsigned(struct{}{}); err != nil { - return nil, fmt.Errorf("respMakeJoin.JoinEvent.SetUnsigned: %w", err) + return nil, &gomatrixserverlib.FederationError{ + ServerName: input.ServerName, + Transient: false, + Reachable: true, + Err: fmt.Errorf("respMakeJoin.JoinEvent.SetUnsigned: %w", err), + } } // Work out if we support the room version that has been supplied in @@ -92,7 +99,12 @@ func PerformJoin( } verImpl, err := gomatrixserverlib.GetRoomVersion(respMakeJoin.RoomVersion) if err != nil { - return nil, err + return nil, &gomatrixserverlib.FederationError{ + ServerName: input.ServerName, + Transient: false, + Reachable: true, + Err: err, + } } // Build the join event. @@ -104,7 +116,12 @@ func PerformJoin( respMakeJoin.RoomVersion, ) if err != nil { - return nil, fmt.Errorf("respMakeJoin.JoinEvent.Build: %w", err) + return nil, &gomatrixserverlib.FederationError{ + ServerName: input.ServerName, + Transient: false, + Reachable: true, + Err: fmt.Errorf("respMakeJoin.JoinEvent.Build: %w", err), + } } var respState gomatrixserverlib.StateResponse @@ -116,10 +133,13 @@ func PerformJoin( event, ) if err != nil { - callbacks.FederationFailure(input.ServerName) - return nil, fmt.Errorf("r.federation.SendJoin: %w", err) + return nil, &gomatrixserverlib.FederationError{ + ServerName: input.ServerName, + Transient: true, + Reachable: false, + Err: fmt.Errorf("r.federation.SendJoin: %w", err), + } } - callbacks.FederationSuccess(input.ServerName) // If the remote server returned an event in the "event" key of // the send_join request then we should use that instead. It may @@ -138,7 +158,12 @@ func PerformJoin( // event, that the room version is known, etc. authEvents := respSendJoin.AuthEvents.UntrustedEvents(respMakeJoin.RoomVersion) if err = checkEventsContainCreateEvent(authEvents); err != nil { - return nil, fmt.Errorf("sanityCheckAuthChain: %w", err) + return nil, &gomatrixserverlib.FederationError{ + ServerName: input.ServerName, + Transient: false, + Reachable: true, + Err: fmt.Errorf("sanityCheckAuthChain: %w", err), + } } // Process the send_join response. The idea here is that we'll try and wait @@ -155,7 +180,12 @@ func PerformJoin( input.EventProvider, ) if err != nil { - return nil, fmt.Errorf("respSendJoin.Check: %w", err) + return nil, &gomatrixserverlib.FederationError{ + ServerName: input.ServerName, + Transient: false, + Reachable: true, + Err: fmt.Errorf("respSendJoin.Check: %w", err), + } } // If we successfully performed a send_join above then the other From 88595f776c63edaa7e9e9e3c5318ea6790e94c39 Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Tue, 25 Apr 2023 13:13:26 -0600 Subject: [PATCH 10/18] Change comment for better accuracy --- fclient/performjoin.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fclient/performjoin.go b/fclient/performjoin.go index de0ed909..13c9dab2 100644 --- a/fclient/performjoin.go +++ b/fclient/performjoin.go @@ -142,7 +142,7 @@ func PerformJoin( } // If the remote server returned an event in the "event" key of - // the send_join request then we should use that instead. It may + // the send_join response then we should use that instead. It may // contain signatures that we don't know about. if len(respSendJoin.Event) > 0 { var remoteEvent *gomatrixserverlib.Event From a9dcc42c1a02e71e6ecd0722a5ec9fecca22f1e4 Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Tue, 25 Apr 2023 13:21:52 -0600 Subject: [PATCH 11/18] Move room version list conversion helper to fclient --- eventversion.go | 8 -------- fclient/federationclient.go | 14 +++++++++++++- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/eventversion.go b/eventversion.go index 437d3af2..a5cf5d93 100644 --- a/eventversion.go +++ b/eventversion.go @@ -307,14 +307,6 @@ func StableRoomVersions() map[RoomVersion]IRoomVersion { return versions } -func RoomVersionsToList(versions map[RoomVersion]IRoomVersion) []RoomVersion { - var supportedVersions []RoomVersion - for version := range versions { - supportedVersions = append(supportedVersions, version) - } - return supportedVersions -} - // RoomVersionDescription contains information about a room version, // namely whether it is marked as supported or stable in this server // version, along with the state resolution algorithm, event ID etc diff --git a/fclient/federationclient.go b/fclient/federationclient.go index 4afeeea6..76ea9fc3 100644 --- a/fclient/federationclient.go +++ b/fclient/federationclient.go @@ -180,6 +180,18 @@ func makeVersionQueryString(roomVersions []gomatrixserverlib.RoomVersion) string return versionQueryString } +// Takes the map of room version implementations and converts it into a list of +// room version strings. +func roomVersionsToList( + versionsMap map[gomatrixserverlib.RoomVersion]gomatrixserverlib.IRoomVersion, +) []gomatrixserverlib.RoomVersion { + var supportedVersions []gomatrixserverlib.RoomVersion + for version := range versionsMap { + supportedVersions = append(supportedVersions, version) + } + return supportedVersions +} + // MakeJoin makes a join m.room.member event for a room on a remote matrix server. // This is used to join a room the local server isn't a member of. // We need to query a remote server because if we aren't in the room we don't @@ -192,7 +204,7 @@ func makeVersionQueryString(roomVersions []gomatrixserverlib.RoomVersion) string func (ac *federationClient) MakeJoin( ctx context.Context, origin, s spec.ServerName, roomID, userID string, ) (res RespMakeJoin, err error) { - roomVersions := gomatrixserverlib.RoomVersionsToList(gomatrixserverlib.StableRoomVersions()) + roomVersions := roomVersionsToList(gomatrixserverlib.StableRoomVersions()) versionQueryString := makeVersionQueryString(roomVersions) path := federationPathPrefixV1 + "/make_join/" + url.PathEscape(roomID) + "/" + From 6acf27d7ac52e004df94c6c4415c2c3aaaf84ee1 Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Tue, 25 Apr 2023 15:18:33 -0600 Subject: [PATCH 12/18] Refactor PerformJoin to be in / and use interfaces when performing federation --- fclient/federationclient_test.go | 12 +-- fclient/federationtypes.go | 24 ++++++ join.go | 26 +++++++ fclient/performjoin.go => performjoin.go | 93 ++++++++++++------------ 4 files changed, 103 insertions(+), 52 deletions(-) create mode 100644 join.go rename fclient/performjoin.go => performjoin.go (72%) diff --git a/fclient/federationclient_test.go b/fclient/federationclient_test.go index deebf9ed..f3a5c22f 100644 --- a/fclient/federationclient_test.go +++ b/fclient/federationclient_test.go @@ -89,8 +89,8 @@ func TestSendJoinFallback(t *testing.T) { if err != nil { t.Fatalf("SendJoin returned an error: %s", err) } - if !reflect.DeepEqual(res.StateEvents, wantRes.StateEvents) { - t.Fatalf("SendJoin response got %+v want %+v", res.StateEvents, wantRes.StateEvents) + if !reflect.DeepEqual(res.GetStateEvents(), wantRes.StateEvents) { + t.Fatalf("SendJoin response got %+v want %+v", res.GetStateEvents(), wantRes.StateEvents) } } @@ -150,11 +150,11 @@ func TestSendJoinJSON(t *testing.T) { } wantStateEvents := gomatrixserverlib.EventJSONs{[]byte(retEv)} wantAuthChain := gomatrixserverlib.EventJSONs{[]byte(retEv)} - if !reflect.DeepEqual(res.StateEvents, wantStateEvents) { - t.Fatalf("SendJoin response got state %+v want %+v", jsonify(res.StateEvents), jsonify(wantStateEvents)) + if !reflect.DeepEqual(res.GetStateEvents(), wantStateEvents) { + t.Fatalf("SendJoin response got state %+v want %+v", jsonify(res.GetStateEvents()), jsonify(wantStateEvents)) } - if !reflect.DeepEqual(res.AuthEvents, wantAuthChain) { - t.Fatalf("SendJoin response got auth %+v want %+v", jsonify(res.AuthEvents), jsonify(wantAuthChain)) + if !reflect.DeepEqual(res.GetAuthEvents(), wantAuthChain) { + t.Fatalf("SendJoin response got auth %+v want %+v", jsonify(res.GetAuthEvents()), jsonify(wantAuthChain)) } } diff --git a/fclient/federationtypes.go b/fclient/federationtypes.go index b1475eeb..ec037012 100644 --- a/fclient/federationtypes.go +++ b/fclient/federationtypes.go @@ -253,6 +253,14 @@ type RespMakeJoin struct { RoomVersion gomatrixserverlib.RoomVersion `json:"room_version"` } +func (r *RespMakeJoin) GetJoinEvent() gomatrixserverlib.EventBuilder { + return r.JoinEvent +} + +func (r *RespMakeJoin) GetRoomVersion() gomatrixserverlib.RoomVersion { + return r.RoomVersion +} + // A RespSendJoin is the content of a response to PUT /_matrix/federation/v2/send_join/{roomID}/{eventID} type RespSendJoin struct { // A list of events giving the state of the room before the request event. @@ -278,6 +286,22 @@ func (r *RespSendJoin) GetAuthEvents() gomatrixserverlib.EventJSONs { return r.AuthEvents } +func (r *RespSendJoin) GetOrigin() spec.ServerName { + return r.Origin +} + +func (r *RespSendJoin) GetJoinEvent() spec.RawJSON { + return r.Event +} + +func (r *RespSendJoin) GetMembersOmitted() bool { + return r.MembersOmitted +} + +func (r *RespSendJoin) GetServersInRoom() []string { + return r.ServersInRoom +} + // MarshalJSON implements json.Marshaller func (r RespSendJoin) MarshalJSON() ([]byte, error) { fields := respSendJoinFields{ diff --git a/join.go b/join.go new file mode 100644 index 00000000..f32837c9 --- /dev/null +++ b/join.go @@ -0,0 +1,26 @@ +package gomatrixserverlib + +import ( + "context" + + "github.com/matrix-org/gomatrixserverlib/spec" +) + +type FederatedJoinClient interface { + MakeJoin(ctx context.Context, origin, s spec.ServerName, roomID, userID string) (res MakeJoinResponse, err error) + SendJoin(ctx context.Context, origin, s spec.ServerName, event *Event) (res SendJoinResponse, err error) +} + +type MakeJoinResponse interface { + GetJoinEvent() EventBuilder + GetRoomVersion() RoomVersion +} + +type SendJoinResponse interface { + GetAuthEvents() EventJSONs + GetStateEvents() EventJSONs + GetOrigin() spec.ServerName + GetJoinEvent() spec.RawJSON + GetMembersOmitted() bool + GetServersInRoom() []string +} diff --git a/fclient/performjoin.go b/performjoin.go similarity index 72% rename from fclient/performjoin.go rename to performjoin.go index 13c9dab2..67aa37ca 100644 --- a/fclient/performjoin.go +++ b/performjoin.go @@ -1,4 +1,4 @@ -package fclient +package gomatrixserverlib import ( "context" @@ -7,7 +7,6 @@ import ( "fmt" "time" - "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib/spec" "github.com/sirupsen/logrus" ) @@ -20,15 +19,15 @@ type PerformJoinInput struct { Unsigned map[string]interface{} PrivateKey ed25519.PrivateKey - KeyID gomatrixserverlib.KeyID - KeyRing *gomatrixserverlib.KeyRing + KeyID KeyID + KeyRing *KeyRing - EventProvider gomatrixserverlib.EventProvider + EventProvider EventProvider } type PerformJoinResponse struct { - JoinEvent *gomatrixserverlib.HeaderedEvent - StateSnapshot gomatrixserverlib.StateResponse + JoinEvent *HeaderedEvent + StateSnapshot StateResponse } // PerformJoin provides high level functionality that will attempt a federated room @@ -36,9 +35,9 @@ type PerformJoinResponse struct { // as part of the join. func PerformJoin( ctx context.Context, - fedClient FederationClient, + fedClient FederatedJoinClient, input PerformJoinInput, -) (*PerformJoinResponse, *gomatrixserverlib.FederationError) { +) (*PerformJoinResponse, *FederationError) { origin := input.UserID.Domain() // Try to perform a make_join using the information supplied in the @@ -52,7 +51,7 @@ func PerformJoin( ) if err != nil { // TODO: Check if the user was not allowed to join the room. - return nil, &gomatrixserverlib.FederationError{ + return nil, &FederationError{ ServerName: input.ServerName, Transient: true, Reachable: false, @@ -63,26 +62,27 @@ func PerformJoin( // Set all the fields to be what they should be, this should be a no-op // but it's possible that the remote server returned us something "odd" stateKey := input.UserID.Raw() - respMakeJoin.JoinEvent.Type = spec.MRoomMember - respMakeJoin.JoinEvent.Sender = input.UserID.Raw() - respMakeJoin.JoinEvent.StateKey = &stateKey - respMakeJoin.JoinEvent.RoomID = input.RoomID - respMakeJoin.JoinEvent.Redacts = "" + joinEvent := respMakeJoin.GetJoinEvent() + joinEvent.Type = spec.MRoomMember + joinEvent.Sender = input.UserID.Raw() + joinEvent.StateKey = &stateKey + joinEvent.RoomID = input.RoomID + joinEvent.Redacts = "" if input.Content == nil { input.Content = map[string]interface{}{} } - _ = json.Unmarshal(respMakeJoin.JoinEvent.Content, &input.Content) + _ = json.Unmarshal(joinEvent.Content, &input.Content) input.Content["membership"] = spec.Join - if err = respMakeJoin.JoinEvent.SetContent(input.Content); err != nil { - return nil, &gomatrixserverlib.FederationError{ + if err = joinEvent.SetContent(input.Content); err != nil { + return nil, &FederationError{ ServerName: input.ServerName, Transient: false, Reachable: true, Err: fmt.Errorf("respMakeJoin.JoinEvent.SetContent: %w", err), } } - if err = respMakeJoin.JoinEvent.SetUnsigned(struct{}{}); err != nil { - return nil, &gomatrixserverlib.FederationError{ + if err = joinEvent.SetUnsigned(struct{}{}); err != nil { + return nil, &FederationError{ ServerName: input.ServerName, Transient: false, Reachable: true, @@ -94,12 +94,13 @@ func PerformJoin( // the make_join response. // "If not provided, the room version is assumed to be either "1" or "2"." // https://matrix.org/docs/spec/server_server/unstable#get-matrix-federation-v1-make-join-roomid-userid - if respMakeJoin.RoomVersion == "" { - respMakeJoin.RoomVersion = setDefaultRoomVersionFromJoinEvent(respMakeJoin.JoinEvent) + roomVersion := respMakeJoin.GetRoomVersion() + if roomVersion == "" { + roomVersion = setDefaultRoomVersionFromJoinEvent(joinEvent) } - verImpl, err := gomatrixserverlib.GetRoomVersion(respMakeJoin.RoomVersion) + verImpl, err := GetRoomVersion(roomVersion) if err != nil { - return nil, &gomatrixserverlib.FederationError{ + return nil, &FederationError{ ServerName: input.ServerName, Transient: false, Reachable: true, @@ -108,15 +109,15 @@ func PerformJoin( } // Build the join event. - event, err := respMakeJoin.JoinEvent.Build( + event, err := joinEvent.Build( time.Now(), origin, input.KeyID, input.PrivateKey, - respMakeJoin.RoomVersion, + respMakeJoin.GetRoomVersion(), ) if err != nil { - return nil, &gomatrixserverlib.FederationError{ + return nil, &FederationError{ ServerName: input.ServerName, Transient: false, Reachable: true, @@ -124,7 +125,7 @@ func PerformJoin( } } - var respState gomatrixserverlib.StateResponse + var respState StateResponse // Try to perform a send_join using the newly built event. respSendJoin, err := fedClient.SendJoin( context.Background(), @@ -133,7 +134,7 @@ func PerformJoin( event, ) if err != nil { - return nil, &gomatrixserverlib.FederationError{ + return nil, &FederationError{ ServerName: input.ServerName, Transient: true, Reachable: false, @@ -144,9 +145,9 @@ func PerformJoin( // If the remote server returned an event in the "event" key of // the send_join response then we should use that instead. It may // contain signatures that we don't know about. - if len(respSendJoin.Event) > 0 { - var remoteEvent *gomatrixserverlib.Event - remoteEvent, err = verImpl.NewEventFromUntrustedJSON(respSendJoin.Event) + if len(respSendJoin.GetJoinEvent()) > 0 { + var remoteEvent *Event + remoteEvent, err = verImpl.NewEventFromUntrustedJSON(respSendJoin.GetJoinEvent()) if err == nil && isWellFormedJoinMemberEvent( remoteEvent, input.RoomID, input.UserID, ) { @@ -156,9 +157,9 @@ func PerformJoin( // Sanity-check the join response to ensure that it has a create // event, that the room version is known, etc. - authEvents := respSendJoin.AuthEvents.UntrustedEvents(respMakeJoin.RoomVersion) + authEvents := respSendJoin.GetAuthEvents().UntrustedEvents(respMakeJoin.GetRoomVersion()) if err = checkEventsContainCreateEvent(authEvents); err != nil { - return nil, &gomatrixserverlib.FederationError{ + return nil, &FederationError{ ServerName: input.ServerName, Transient: false, Reachable: true, @@ -172,15 +173,15 @@ func PerformJoin( // we'll still continue to process the join anyway so that we don't waste the effort. // TODO: Can we expand Check here to return a list of missing auth // events rather than failing one at a time? - respState, err = gomatrixserverlib.CheckSendJoinResponse( + respState, err = CheckSendJoinResponse( context.Background(), - respMakeJoin.RoomVersion, &respSendJoin, + respMakeJoin.GetRoomVersion(), StateResponse(respSendJoin), input.KeyRing, event, input.EventProvider, ) if err != nil { - return nil, &gomatrixserverlib.FederationError{ + return nil, &FederationError{ ServerName: input.ServerName, Transient: false, Reachable: true, @@ -200,14 +201,14 @@ func PerformJoin( } return &PerformJoinResponse{ - JoinEvent: event.Headered(respMakeJoin.RoomVersion), + JoinEvent: event.Headered(respMakeJoin.GetRoomVersion()), StateSnapshot: respState, }, nil } func setDefaultRoomVersionFromJoinEvent( - joinEvent gomatrixserverlib.EventBuilder, -) gomatrixserverlib.RoomVersion { + joinEvent EventBuilder, +) RoomVersion { // if auth events are not event references we know it must be v3+ // we have to do these shenanigans to satisfy sytest, specifically for: // "Outbound federation rejects m.room.create events with an unknown room version" @@ -224,14 +225,14 @@ func setDefaultRoomVersionFromJoinEvent( } if hasEventRefs { - return gomatrixserverlib.RoomVersionV1 + return RoomVersionV1 } - return gomatrixserverlib.RoomVersionV4 + return RoomVersionV4 } // isWellFormedJoinMemberEvent returns true if the event looks like a legitimate // membership event. -func isWellFormedJoinMemberEvent(event *gomatrixserverlib.Event, roomID string, userID *spec.UserID) bool { +func isWellFormedJoinMemberEvent(event *Event, roomID string, userID *spec.UserID) bool { if membership, err := event.Membership(); err != nil { return false } else if membership != spec.Join { @@ -246,7 +247,7 @@ func isWellFormedJoinMemberEvent(event *gomatrixserverlib.Event, roomID string, return true } -func checkEventsContainCreateEvent(events []*gomatrixserverlib.Event) error { +func checkEventsContainCreateEvent(events []*Event) error { // sanity check we have a create event and it has a known room version for _, ev := range events { if ev.Type() == spec.MRoomCreate && ev.StateKeyEquals("") { @@ -264,8 +265,8 @@ func checkEventsContainCreateEvent(events []*gomatrixserverlib.Event) error { // The version of the room. Defaults to "1" if the key does not exist. verBody.Version = "1" } - knownVersions := gomatrixserverlib.RoomVersions() - if _, ok := knownVersions[gomatrixserverlib.RoomVersion(verBody.Version)]; !ok { + knownVersions := RoomVersions() + if _, ok := knownVersions[RoomVersion(verBody.Version)]; !ok { return fmt.Errorf("m.room.create event has an unknown room version: %s", verBody.Version) } return nil From 86ed2a677a39b3452a384a69d3c496da5e1282d7 Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Tue, 25 Apr 2023 18:34:19 -0600 Subject: [PATCH 13/18] Fix room version usage in PerformJoin --- performjoin.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/performjoin.go b/performjoin.go index 67aa37ca..1b09f466 100644 --- a/performjoin.go +++ b/performjoin.go @@ -114,7 +114,7 @@ func PerformJoin( origin, input.KeyID, input.PrivateKey, - respMakeJoin.GetRoomVersion(), + roomVersion, ) if err != nil { return nil, &FederationError{ @@ -157,7 +157,7 @@ func PerformJoin( // Sanity-check the join response to ensure that it has a create // event, that the room version is known, etc. - authEvents := respSendJoin.GetAuthEvents().UntrustedEvents(respMakeJoin.GetRoomVersion()) + authEvents := respSendJoin.GetAuthEvents().UntrustedEvents(roomVersion) if err = checkEventsContainCreateEvent(authEvents); err != nil { return nil, &FederationError{ ServerName: input.ServerName, @@ -175,7 +175,7 @@ func PerformJoin( // events rather than failing one at a time? respState, err = CheckSendJoinResponse( context.Background(), - respMakeJoin.GetRoomVersion(), StateResponse(respSendJoin), + roomVersion, StateResponse(respSendJoin), input.KeyRing, event, input.EventProvider, @@ -201,7 +201,7 @@ func PerformJoin( } return &PerformJoinResponse{ - JoinEvent: event.Headered(respMakeJoin.GetRoomVersion()), + JoinEvent: event.Headered(roomVersion), StateSnapshot: respState, }, nil } From 94b314bf448d5243f1e5e3d43f3735e013e3fb54 Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Wed, 26 Apr 2023 10:04:22 -0600 Subject: [PATCH 14/18] Return Event from PerformJoin --- performjoin.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/performjoin.go b/performjoin.go index 1b09f466..810b4359 100644 --- a/performjoin.go +++ b/performjoin.go @@ -26,7 +26,7 @@ type PerformJoinInput struct { } type PerformJoinResponse struct { - JoinEvent *HeaderedEvent + JoinEvent *Event StateSnapshot StateResponse } @@ -201,7 +201,7 @@ func PerformJoin( } return &PerformJoinResponse{ - JoinEvent: event.Headered(roomVersion), + JoinEvent: event, StateSnapshot: respState, }, nil } From 79da866b77d57ad42e405a42c4ad613d4d854304 Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Wed, 26 Apr 2023 10:06:37 -0600 Subject: [PATCH 15/18] Allow joining all known room versions --- fclient/federationclient.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fclient/federationclient.go b/fclient/federationclient.go index 76ea9fc3..cf7fae54 100644 --- a/fclient/federationclient.go +++ b/fclient/federationclient.go @@ -204,7 +204,7 @@ func roomVersionsToList( func (ac *federationClient) MakeJoin( ctx context.Context, origin, s spec.ServerName, roomID, userID string, ) (res RespMakeJoin, err error) { - roomVersions := roomVersionsToList(gomatrixserverlib.StableRoomVersions()) + roomVersions := roomVersionsToList(gomatrixserverlib.RoomVersions()) versionQueryString := makeVersionQueryString(roomVersions) path := federationPathPrefixV1 + "/make_join/" + url.PathEscape(roomID) + "/" + From b2a84dbc5223564446a18c3382ab400f4b6a5bb0 Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Wed, 26 Apr 2023 17:02:44 -0600 Subject: [PATCH 16/18] Add unit tests for PerformJoin --- performjoin.go | 9 ++ performjoin_test.go | 235 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 performjoin_test.go diff --git a/performjoin.go b/performjoin.go index 810b4359..c2c009d4 100644 --- a/performjoin.go +++ b/performjoin.go @@ -38,6 +38,15 @@ func PerformJoin( fedClient FederatedJoinClient, input PerformJoinInput, ) (*PerformJoinResponse, *FederationError) { + if input.UserID == nil { + return nil, &FederationError{ + ServerName: input.ServerName, + Transient: false, + Reachable: false, + Err: fmt.Errorf("UserID is nil"), + } + } + origin := input.UserID.Domain() // Try to perform a make_join using the information supplied in the diff --git a/performjoin_test.go b/performjoin_test.go new file mode 100644 index 00000000..fecbf1b7 --- /dev/null +++ b/performjoin_test.go @@ -0,0 +1,235 @@ +package gomatrixserverlib + +import ( + "context" + "crypto/ed25519" + "crypto/rand" + "encoding/hex" + "errors" + "testing" + "time" + + "github.com/matrix-org/gomatrix" + "github.com/matrix-org/gomatrixserverlib/spec" +) + +type TestMakeJoinResponse struct{ joinEvent EventBuilder } + +func (t *TestMakeJoinResponse) GetJoinEvent() EventBuilder { + return t.joinEvent +} + +func (t *TestMakeJoinResponse) GetRoomVersion() RoomVersion { + return RoomVersionV10 +} + +type TestSendJoinResponse struct { + createEvent *Event + joinEvent *Event +} + +func (t *TestSendJoinResponse) GetAuthEvents() EventJSONs { + return EventJSONs{t.createEvent.JSON()} +} + +func (t *TestSendJoinResponse) GetStateEvents() EventJSONs { + return EventJSONs{t.createEvent.JSON()} +} + +func (t *TestSendJoinResponse) GetOrigin() spec.ServerName { + return "server" +} + +func (t *TestSendJoinResponse) GetJoinEvent() spec.RawJSON { + return t.joinEvent.JSON() +} + +func (t *TestSendJoinResponse) GetMembersOmitted() bool { + return true +} + +func (t *TestSendJoinResponse) GetServersInRoom() []string { + return []string{"server"} +} + +type TestFederatedJoinClient struct { + shouldMakeFail bool + shouldSendFail bool + createEvent *Event + joinEvent *Event + joinEventBuilder EventBuilder +} + +func (t *TestFederatedJoinClient) MakeJoin(ctx context.Context, origin, s spec.ServerName, roomID, userID string) (res MakeJoinResponse, err error) { + if t.shouldMakeFail { + return nil, gomatrix.HTTPError{} + } + + return &TestMakeJoinResponse{joinEvent: t.joinEventBuilder}, nil +} +func (t *TestFederatedJoinClient) SendJoin(ctx context.Context, origin, s spec.ServerName, event *Event) (res SendJoinResponse, err error) { + if t.shouldSendFail { + return nil, gomatrix.HTTPError{} + } + + return &TestSendJoinResponse{createEvent: t.createEvent, joinEvent: t.joinEvent}, nil +} + +type joinKeyDatabase struct{ key ed25519.PublicKey } + +func (db joinKeyDatabase) FetcherName() string { + return "joinKeyDatabase" +} + +func (db *joinKeyDatabase) FetchKeys( + ctx context.Context, requests map[PublicKeyLookupRequest]spec.Timestamp, +) (map[PublicKeyLookupRequest]PublicKeyLookupResult, error) { + results := map[PublicKeyLookupRequest]PublicKeyLookupResult{} + + req1 := PublicKeyLookupRequest{"server", "ed25519:1234"} + + for req := range requests { + if req == req1 { + k, err := hex.DecodeString(hex.EncodeToString(db.key)) + vk := VerifyKey{Key: k} + if err != nil { + return nil, err + } + results[req] = PublicKeyLookupResult{ + VerifyKey: vk, + ValidUntilTS: spec.Timestamp(time.Now().Add(time.Hour).Unix() * 1000), + ExpiredTS: PublicKeyNotExpired, + } + } + } + return results, nil +} + +func (db *joinKeyDatabase) StoreKeys( + ctx context.Context, requests map[PublicKeyLookupRequest]PublicKeyLookupResult, +) error { + return nil +} + +func TestPerformJoin(t *testing.T) { + pk, sk, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("Failed generating key: %v", err) + } + keyID := KeyID("ed25519:1234") + userID, err := spec.NewUserID("@user:server", true) + if err != nil { + t.Fatalf("Invalid UserID: %v", err) + } + roomID := "!room:server" + + stateKey := "" + eb := EventBuilder{ + Sender: userID.Raw(), + RoomID: roomID, + Type: "m.room.create", + StateKey: &stateKey, + PrevEvents: []string{}, + AuthEvents: []string{}, + Depth: 0, + Content: spec.RawJSON(`{"creator":"@user:server","m.federate":true,"room_version":"10"}`), + Unsigned: spec.RawJSON(""), + } + createEvent, err := eb.Build(time.Now(), userID.Domain(), keyID, sk, RoomVersionV10) + if err != nil { + t.Fatalf("Failed building create event: %v", err) + } + + stateKey = userID.Raw() + joinEB := EventBuilder{ + Sender: userID.Raw(), + RoomID: roomID, + Type: "m.room.member", + StateKey: &stateKey, + PrevEvents: []string{createEvent.EventID()}, + AuthEvents: []string{createEvent.EventID()}, + Depth: 1, + Content: spec.RawJSON(`{"membership":"join"}`), + Unsigned: spec.RawJSON(""), + } + joinEvent, err := joinEB.Build(time.Now(), userID.Domain(), keyID, sk, RoomVersionV10) + if err != nil { + t.Fatalf("Failed building create event: %v", err) + } + + eventProvider := func(roomVer RoomVersion, eventIDs []string) ([]*Event, error) { + for _, eventID := range eventIDs { + if eventID == createEvent.EventID() { + return []*Event{createEvent}, nil + } + } + return []*Event{}, nil + } + + tests := map[string]struct { + FedClient FederatedJoinClient + Input PerformJoinInput + ExpectedErr bool + ExpectedHTTPErr bool + }{ + "invalid_user_id": { + FedClient: &TestFederatedJoinClient{shouldMakeFail: false, shouldSendFail: false}, + Input: PerformJoinInput{UserID: nil}, + ExpectedErr: true, + ExpectedHTTPErr: false, + }, + "make_join_http_err": { + FedClient: &TestFederatedJoinClient{shouldMakeFail: true, shouldSendFail: false}, + Input: PerformJoinInput{UserID: userID}, + ExpectedErr: true, + ExpectedHTTPErr: true, + }, + "send_join_http_err": { + FedClient: &TestFederatedJoinClient{shouldMakeFail: false, shouldSendFail: true}, + Input: PerformJoinInput{UserID: userID, RoomID: roomID, PrivateKey: sk, KeyID: keyID}, + ExpectedErr: true, + ExpectedHTTPErr: true, + }, + "successful_join": { + FedClient: &TestFederatedJoinClient{shouldMakeFail: false, shouldSendFail: false, createEvent: createEvent, joinEvent: joinEvent, joinEventBuilder: joinEB}, + Input: PerformJoinInput{ + UserID: userID, + RoomID: roomID, + PrivateKey: sk, + KeyID: keyID, + KeyRing: &KeyRing{[]KeyFetcher{&TestRequestKeyDummy{}}, &joinKeyDatabase{key: pk}}, + EventProvider: eventProvider, + }, + ExpectedErr: false, + ExpectedHTTPErr: false, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + res, err := PerformJoin(context.Background(), tc.FedClient, tc.Input) + if tc.ExpectedErr { + if err == nil { + t.Fatalf("Expected an error but none received") + } + if tc.ExpectedHTTPErr { + var httpErr gomatrix.HTTPError + if ok := errors.As(err.Err, &httpErr); !ok { + t.Fatalf("Expected HTTPError, got: %v", err) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected err: %v", err) + } + if res == nil { + t.Fatalf("Nil response received") + } + + if res.JoinEvent.EventID() != joinEvent.EventID() { + t.Fatalf("Expected join eventID %v, got %v", joinEvent.EventID(), res.JoinEvent.EventID()) + } + } + }) + } +} From ab0fb40cbefc431fceb2c435c4da23034e7eb12e Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Wed, 26 Apr 2023 17:36:59 -0600 Subject: [PATCH 17/18] Add PerformJoin test for default room version --- performjoin.go | 19 ++++-------- performjoin_test.go | 70 +++++++++++++++++++++++++++++++-------------- 2 files changed, 53 insertions(+), 36 deletions(-) diff --git a/performjoin.go b/performjoin.go index c2c009d4..ec34a196 100644 --- a/performjoin.go +++ b/performjoin.go @@ -116,6 +116,7 @@ func PerformJoin( Err: err, } } + println(roomVersion) // Build the join event. event, err := joinEvent.Build( @@ -221,22 +222,12 @@ func setDefaultRoomVersionFromJoinEvent( // if auth events are not event references we know it must be v3+ // we have to do these shenanigans to satisfy sytest, specifically for: // "Outbound federation rejects m.room.create events with an unknown room version" - hasEventRefs := true - authEvents, ok := joinEvent.AuthEvents.([]interface{}) - if ok { - if len(authEvents) > 0 { - _, ok = authEvents[0].(string) - if ok { - // event refs are objects, not strings, so we know we must be dealing with a v3+ room. - hasEventRefs = false - } - } - } - - if hasEventRefs { + switch joinEvent.AuthEvents.(type) { + case []string: + return RoomVersionV4 + default: return RoomVersionV1 } - return RoomVersionV4 } // isWellFormedJoinMemberEvent returns true if the event looks like a legitimate diff --git a/performjoin_test.go b/performjoin_test.go index fecbf1b7..11a91bd1 100644 --- a/performjoin_test.go +++ b/performjoin_test.go @@ -13,14 +13,17 @@ import ( "github.com/matrix-org/gomatrixserverlib/spec" ) -type TestMakeJoinResponse struct{ joinEvent EventBuilder } +type TestMakeJoinResponse struct { + roomVersion RoomVersion + joinEvent EventBuilder +} func (t *TestMakeJoinResponse) GetJoinEvent() EventBuilder { return t.joinEvent } func (t *TestMakeJoinResponse) GetRoomVersion() RoomVersion { - return RoomVersionV10 + return t.roomVersion } type TestSendJoinResponse struct { @@ -55,6 +58,7 @@ func (t *TestSendJoinResponse) GetServersInRoom() []string { type TestFederatedJoinClient struct { shouldMakeFail bool shouldSendFail bool + roomVersion RoomVersion createEvent *Event joinEvent *Event joinEventBuilder EventBuilder @@ -65,7 +69,7 @@ func (t *TestFederatedJoinClient) MakeJoin(ctx context.Context, origin, s spec.S return nil, gomatrix.HTTPError{} } - return &TestMakeJoinResponse{joinEvent: t.joinEventBuilder}, nil + return &TestMakeJoinResponse{joinEvent: t.joinEventBuilder, roomVersion: t.roomVersion}, nil } func (t *TestFederatedJoinClient) SendJoin(ctx context.Context, origin, s spec.ServerName, event *Event) (res SendJoinResponse, err error) { if t.shouldSendFail { @@ -167,31 +171,49 @@ func TestPerformJoin(t *testing.T) { } tests := map[string]struct { - FedClient FederatedJoinClient - Input PerformJoinInput - ExpectedErr bool - ExpectedHTTPErr bool + FedClient FederatedJoinClient + Input PerformJoinInput + ExpectedErr bool + ExpectedHTTPErr bool + ExpectedRoomVersion RoomVersion }{ "invalid_user_id": { - FedClient: &TestFederatedJoinClient{shouldMakeFail: false, shouldSendFail: false}, - Input: PerformJoinInput{UserID: nil}, - ExpectedErr: true, - ExpectedHTTPErr: false, + FedClient: &TestFederatedJoinClient{shouldMakeFail: false, shouldSendFail: false, roomVersion: RoomVersionV10}, + Input: PerformJoinInput{UserID: nil}, + ExpectedErr: true, + ExpectedHTTPErr: false, + ExpectedRoomVersion: joinEvent.Version(), }, "make_join_http_err": { - FedClient: &TestFederatedJoinClient{shouldMakeFail: true, shouldSendFail: false}, - Input: PerformJoinInput{UserID: userID}, - ExpectedErr: true, - ExpectedHTTPErr: true, + FedClient: &TestFederatedJoinClient{shouldMakeFail: true, shouldSendFail: false, roomVersion: RoomVersionV10}, + Input: PerformJoinInput{UserID: userID}, + ExpectedErr: true, + ExpectedHTTPErr: true, + ExpectedRoomVersion: joinEvent.Version(), }, "send_join_http_err": { - FedClient: &TestFederatedJoinClient{shouldMakeFail: false, shouldSendFail: true}, - Input: PerformJoinInput{UserID: userID, RoomID: roomID, PrivateKey: sk, KeyID: keyID}, - ExpectedErr: true, - ExpectedHTTPErr: true, + FedClient: &TestFederatedJoinClient{shouldMakeFail: false, shouldSendFail: true, roomVersion: RoomVersionV10}, + Input: PerformJoinInput{UserID: userID, RoomID: roomID, PrivateKey: sk, KeyID: keyID}, + ExpectedErr: true, + ExpectedHTTPErr: true, + ExpectedRoomVersion: joinEvent.Version(), + }, + "default_room_version": { + FedClient: &TestFederatedJoinClient{shouldMakeFail: false, shouldSendFail: false, roomVersion: "", createEvent: createEvent, joinEvent: joinEvent, joinEventBuilder: joinEB}, + Input: PerformJoinInput{ + UserID: userID, + RoomID: roomID, + PrivateKey: sk, + KeyID: keyID, + KeyRing: &KeyRing{[]KeyFetcher{&TestRequestKeyDummy{}}, &joinKeyDatabase{key: pk}}, + EventProvider: eventProvider, + }, + ExpectedErr: false, + ExpectedHTTPErr: false, + ExpectedRoomVersion: RoomVersionV4, }, "successful_join": { - FedClient: &TestFederatedJoinClient{shouldMakeFail: false, shouldSendFail: false, createEvent: createEvent, joinEvent: joinEvent, joinEventBuilder: joinEB}, + FedClient: &TestFederatedJoinClient{shouldMakeFail: false, shouldSendFail: false, roomVersion: RoomVersionV10, createEvent: createEvent, joinEvent: joinEvent, joinEventBuilder: joinEB}, Input: PerformJoinInput{ UserID: userID, RoomID: roomID, @@ -200,8 +222,9 @@ func TestPerformJoin(t *testing.T) { KeyRing: &KeyRing{[]KeyFetcher{&TestRequestKeyDummy{}}, &joinKeyDatabase{key: pk}}, EventProvider: eventProvider, }, - ExpectedErr: false, - ExpectedHTTPErr: false, + ExpectedErr: false, + ExpectedHTTPErr: false, + ExpectedRoomVersion: joinEvent.Version(), }, } @@ -229,6 +252,9 @@ func TestPerformJoin(t *testing.T) { if res.JoinEvent.EventID() != joinEvent.EventID() { t.Fatalf("Expected join eventID %v, got %v", joinEvent.EventID(), res.JoinEvent.EventID()) } + if res.JoinEvent.Version() != tc.ExpectedRoomVersion { + t.Fatalf("Expected room version %v, got %v", tc.ExpectedRoomVersion, res.JoinEvent.Version()) + } } }) } From c135a99829a91a9581302a62e7559e8505f192b5 Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Wed, 26 Apr 2023 18:07:27 -0600 Subject: [PATCH 18/18] Fix default room version test with old strange logic --- performjoin.go | 18 ++++++++++++++---- performjoin_test.go | 8 ++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/performjoin.go b/performjoin.go index ec34a196..8e687046 100644 --- a/performjoin.go +++ b/performjoin.go @@ -222,12 +222,22 @@ func setDefaultRoomVersionFromJoinEvent( // if auth events are not event references we know it must be v3+ // we have to do these shenanigans to satisfy sytest, specifically for: // "Outbound federation rejects m.room.create events with an unknown room version" - switch joinEvent.AuthEvents.(type) { - case []string: - return RoomVersionV4 - default: + hasEventRefs := true + authEvents, ok := joinEvent.AuthEvents.([]interface{}) + if ok { + if len(authEvents) > 0 { + _, ok = authEvents[0].(string) + if ok { + // event refs are objects, not strings, so we know we must be dealing with a v3+ room. + hasEventRefs = false + } + } + } + + if hasEventRefs { return RoomVersionV1 } + return RoomVersionV4 } // isWellFormedJoinMemberEvent returns true if the event looks like a legitimate diff --git a/performjoin_test.go b/performjoin_test.go index 11a91bd1..2cb1d8aa 100644 --- a/performjoin_test.go +++ b/performjoin_test.go @@ -133,8 +133,8 @@ func TestPerformJoin(t *testing.T) { RoomID: roomID, Type: "m.room.create", StateKey: &stateKey, - PrevEvents: []string{}, - AuthEvents: []string{}, + PrevEvents: []interface{}{}, + AuthEvents: []interface{}{}, Depth: 0, Content: spec.RawJSON(`{"creator":"@user:server","m.federate":true,"room_version":"10"}`), Unsigned: spec.RawJSON(""), @@ -150,8 +150,8 @@ func TestPerformJoin(t *testing.T) { RoomID: roomID, Type: "m.room.member", StateKey: &stateKey, - PrevEvents: []string{createEvent.EventID()}, - AuthEvents: []string{createEvent.EventID()}, + PrevEvents: []interface{}{createEvent.EventID()}, + AuthEvents: []interface{}{createEvent.EventID()}, Depth: 1, Content: spec.RawJSON(`{"membership":"join"}`), Unsigned: spec.RawJSON(""),