Handle event_format federation in /sync responses (#3192)

This commit is contained in:
devonh 2023-08-31 15:33:38 +00:00 committed by GitHub
parent 11fd2f019b
commit bb2ab62cbf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 739 additions and 84 deletions

View file

@ -16,12 +16,21 @@
package synctypes
import (
"encoding/json"
"fmt"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/gomatrixserverlib/spec"
"github.com/sirupsen/logrus"
)
// PrevEventRef represents a reference to a previous event in a state event upgrade
type PrevEventRef struct {
PrevContent json.RawMessage `json:"prev_content"`
ReplacesState string `json:"replaces_state"`
PrevSenderID string `json:"prev_sender"`
}
type ClientEventFormat int
const (
@ -30,8 +39,21 @@ const (
// FormatSync will include only the event keys required by the /sync API. Notably, this
// means the 'room_id' will be missing from the events.
FormatSync
// FormatSyncFederation will include all event keys normally included in federated events.
// This allows clients to request federated formatted events via the /sync API.
FormatSyncFederation
)
// ClientFederationFields extends a ClientEvent to contain the additional fields present in a
// federation event. Used when the client requests `event_format` of type `federation`.
type ClientFederationFields struct {
Depth int64 `json:"depth,omitempty"`
PrevEvents []string `json:"prev_events,omitempty"`
AuthEvents []string `json:"auth_events,omitempty"`
Signatures spec.RawJSON `json:"signatures,omitempty"`
Hashes spec.RawJSON `json:"hashes,omitempty"`
}
// ClientEvent is an event which is fit for consumption by clients, in accordance with the specification.
type ClientEvent struct {
Content spec.RawJSON `json:"content"`
@ -44,6 +66,9 @@ type ClientEvent struct {
Type string `json:"type"`
Unsigned spec.RawJSON `json:"unsigned,omitempty"`
Redacts string `json:"redacts,omitempty"`
// Only sent to clients when `event_format` == `federation`.
ClientFederationFields
}
// ToClientEvents converts server events to client events.
@ -53,6 +78,11 @@ func ToClientEvents(serverEvs []gomatrixserverlib.PDU, format ClientEventFormat,
if se == nil {
continue // TODO: shouldn't happen?
}
if format == FormatSyncFederation {
evs = append(evs, ToClientEvent(se, format, string(se.SenderID()), se.StateKey(), spec.RawJSON(se.Unsigned())))
continue
}
sender := spec.UserID{}
validRoomID, err := spec.NewRoomID(se.RoomID())
if err != nil {
@ -71,28 +101,61 @@ func ToClientEvents(serverEvs []gomatrixserverlib.PDU, format ClientEventFormat,
sk = &skString
}
}
evs = append(evs, ToClientEvent(se, format, sender, sk))
unsigned := se.Unsigned()
var prev PrevEventRef
if err := json.Unmarshal(se.Unsigned(), &prev); err == nil && prev.PrevSenderID != "" {
prevUserID, err := userIDForSender(*validRoomID, spec.SenderID(prev.PrevSenderID))
if err == nil && userID != nil {
prev.PrevSenderID = prevUserID.String()
} else {
errString := "userID unknown"
if err != nil {
errString = err.Error()
}
logrus.Warnf("Failed to find userID for prev_sender in ClientEvent: %s", errString)
// NOTE: Not much can be done here, so leave the previous value in place.
}
unsigned, err = json.Marshal(prev)
if err != nil {
logrus.Errorf("Failed to marshal unsigned content for ClientEvent: %s", err.Error())
continue
}
}
evs = append(evs, ToClientEvent(se, format, sender.String(), sk, spec.RawJSON(unsigned)))
}
return evs
}
// ToClientEvent converts a single server event to a client event.
func ToClientEvent(se gomatrixserverlib.PDU, format ClientEventFormat, sender spec.UserID, stateKey *string) ClientEvent {
func ToClientEvent(se gomatrixserverlib.PDU, format ClientEventFormat, sender string, stateKey *string, unsigned spec.RawJSON) ClientEvent {
ce := ClientEvent{
Content: spec.RawJSON(se.Content()),
Sender: sender.String(),
Sender: sender,
Type: se.Type(),
StateKey: stateKey,
Unsigned: spec.RawJSON(se.Unsigned()),
Unsigned: unsigned,
OriginServerTS: se.OriginServerTS(),
EventID: se.EventID(),
Redacts: se.Redacts(),
}
if format == FormatAll {
switch format {
case FormatAll:
ce.RoomID = se.RoomID()
case FormatSync:
case FormatSyncFederation:
ce.RoomID = se.RoomID()
ce.AuthEvents = se.AuthEventIDs()
ce.PrevEvents = se.PrevEventIDs()
ce.Depth = se.Depth()
// TODO: Set Signatures & Hashes fields
}
if se.Version() == gomatrixserverlib.RoomVersionPseudoIDs {
ce.SenderKey = se.SenderID()
if format != FormatSyncFederation {
if se.Version() == gomatrixserverlib.RoomVersionPseudoIDs {
ce.SenderKey = se.SenderID()
}
}
return ce
}
@ -118,7 +181,7 @@ func ToClientEventDefault(userIDQuery spec.UserIDForSender, event gomatrixserver
sk = &skString
}
}
return ToClientEvent(event, FormatAll, sender, sk)
return ToClientEvent(event, FormatAll, sender.String(), sk, event.Unsigned())
}
// If provided state key is a user ID (state keys beginning with @ are reserved for this purpose)

View file

@ -18,12 +18,69 @@ package synctypes
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"testing"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/gomatrixserverlib/spec"
)
const testSenderID = "testSenderID"
const testUserID = "@test:localhost"
type EventFieldsToVerify struct {
EventID string
Type string
OriginServerTS spec.Timestamp
StateKey *string
Content spec.RawJSON
Unsigned spec.RawJSON
Sender string
Depth int64
PrevEvents []string
AuthEvents []string
}
func verifyEventFields(t *testing.T, got EventFieldsToVerify, want EventFieldsToVerify) {
if got.EventID != want.EventID {
t.Errorf("ClientEvent.EventID: wanted %s, got %s", want.EventID, got.EventID)
}
if got.OriginServerTS != want.OriginServerTS {
t.Errorf("ClientEvent.OriginServerTS: wanted %d, got %d", want.OriginServerTS, got.OriginServerTS)
}
if got.StateKey == nil && want.StateKey != nil {
t.Errorf("ClientEvent.StateKey: no state key present when one was wanted: %s", *want.StateKey)
}
if got.StateKey != nil && want.StateKey == nil {
t.Errorf("ClientEvent.StateKey: state key present when one was not wanted: %s", *got.StateKey)
}
if got.StateKey != nil && want.StateKey != nil && *got.StateKey != *want.StateKey {
t.Errorf("ClientEvent.StateKey: wanted %s, got %s", *want.StateKey, *got.StateKey)
}
if got.Type != want.Type {
t.Errorf("ClientEvent.Type: wanted %s, got %s", want.Type, got.Type)
}
if !bytes.Equal(got.Content, want.Content) {
t.Errorf("ClientEvent.Content: wanted %s, got %s", string(want.Content), string(got.Content))
}
if !bytes.Equal(got.Unsigned, want.Unsigned) {
t.Errorf("ClientEvent.Unsigned: wanted %s, got %s", string(want.Unsigned), string(got.Unsigned))
}
if got.Sender != want.Sender {
t.Errorf("ClientEvent.Sender: wanted %s, got %s", want.Sender, got.Sender)
}
if got.Depth != want.Depth {
t.Errorf("ClientEvent.Depth: wanted %d, got %d", want.Depth, got.Depth)
}
if !reflect.DeepEqual(got.PrevEvents, want.PrevEvents) {
t.Errorf("ClientEvent.PrevEvents: wanted %v, got %v", want.PrevEvents, got.PrevEvents)
}
if !reflect.DeepEqual(got.AuthEvents, want.AuthEvents) {
t.Errorf("ClientEvent.AuthEvents: wanted %v, got %v", want.AuthEvents, got.AuthEvents)
}
}
func TestToClientEvent(t *testing.T) { // nolint: gocyclo
ev, err := gomatrixserverlib.MustGetRoomVersion(gomatrixserverlib.RoomVersionV1).NewEventFromTrustedJSON([]byte(`{
"type": "m.room.name",
@ -49,28 +106,28 @@ func TestToClientEvent(t *testing.T) { // nolint: gocyclo
t.Fatalf("failed to create userID: %s", err)
}
sk := ""
ce := ToClientEvent(ev, FormatAll, *userID, &sk)
if ce.EventID != ev.EventID() {
t.Errorf("ClientEvent.EventID: wanted %s, got %s", ev.EventID(), ce.EventID)
}
if ce.OriginServerTS != ev.OriginServerTS() {
t.Errorf("ClientEvent.OriginServerTS: wanted %d, got %d", ev.OriginServerTS(), ce.OriginServerTS)
}
if ce.StateKey == nil || *ce.StateKey != "" {
t.Errorf("ClientEvent.StateKey: wanted '', got %v", ce.StateKey)
}
if ce.Type != ev.Type() {
t.Errorf("ClientEvent.Type: wanted %s, got %s", ev.Type(), ce.Type)
}
if !bytes.Equal(ce.Content, ev.Content()) {
t.Errorf("ClientEvent.Content: wanted %s, got %s", string(ev.Content()), string(ce.Content))
}
if !bytes.Equal(ce.Unsigned, ev.Unsigned()) {
t.Errorf("ClientEvent.Unsigned: wanted %s, got %s", string(ev.Unsigned()), string(ce.Unsigned))
}
if ce.Sender != userID.String() {
t.Errorf("ClientEvent.Sender: wanted %s, got %s", userID.String(), ce.Sender)
}
ce := ToClientEvent(ev, FormatAll, userID.String(), &sk, ev.Unsigned())
verifyEventFields(t,
EventFieldsToVerify{
EventID: ce.EventID,
Type: ce.Type,
OriginServerTS: ce.OriginServerTS,
StateKey: ce.StateKey,
Content: ce.Content,
Unsigned: ce.Unsigned,
Sender: ce.Sender,
},
EventFieldsToVerify{
EventID: ev.EventID(),
Type: ev.Type(),
OriginServerTS: ev.OriginServerTS(),
StateKey: &sk,
Content: ev.Content(),
Unsigned: ev.Unsigned(),
Sender: userID.String(),
})
j, err := json.Marshal(ce)
if err != nil {
t.Fatalf("failed to Marshal ClientEvent: %s", err)
@ -109,8 +166,378 @@ func TestToClientFormatSync(t *testing.T) {
t.Fatalf("failed to create userID: %s", err)
}
sk := ""
ce := ToClientEvent(ev, FormatSync, *userID, &sk)
ce := ToClientEvent(ev, FormatSync, userID.String(), &sk, ev.Unsigned())
if ce.RoomID != "" {
t.Errorf("ClientEvent.RoomID: wanted '', got %s", ce.RoomID)
}
}
func TestToClientEventFormatSyncFederation(t *testing.T) { // nolint: gocyclo
ev, err := gomatrixserverlib.MustGetRoomVersion(gomatrixserverlib.RoomVersionV10).NewEventFromTrustedJSON([]byte(`{
"type": "m.room.name",
"state_key": "",
"event_id": "$test:localhost",
"room_id": "!test:localhost",
"sender": "@test:localhost",
"content": {
"name": "Hello World"
},
"origin_server_ts": 123456,
"unsigned": {
"prev_content": {
"name": "Goodbye World"
}
},
"depth": 8,
"prev_events": [
"$f597Tp0Mm1PPxEgiprzJc2cZAjVhxCxACOGuwJb33Oo"
],
"auth_events": [
"$Bj0ZGgX6VTqAQdqKH4ZG3l6rlbxY3rZlC5D3MeuK1OQ",
"$QsMs6A1PUVUhgSvmHBfpqEYJPgv4DXt96r8P2AK7iXQ",
"$tBteKtlnFiwlmPJsv0wkKTMEuUVWpQH89H7Xskxve1Q"
]
}`), false)
if err != nil {
t.Fatalf("failed to create Event: %s", err)
}
userID, err := spec.NewUserID("@test:localhost", true)
if err != nil {
t.Fatalf("failed to create userID: %s", err)
}
sk := ""
ce := ToClientEvent(ev, FormatSyncFederation, userID.String(), &sk, ev.Unsigned())
verifyEventFields(t,
EventFieldsToVerify{
EventID: ce.EventID,
Type: ce.Type,
OriginServerTS: ce.OriginServerTS,
StateKey: ce.StateKey,
Content: ce.Content,
Unsigned: ce.Unsigned,
Sender: ce.Sender,
Depth: ce.Depth,
PrevEvents: ce.PrevEvents,
AuthEvents: ce.AuthEvents,
},
EventFieldsToVerify{
EventID: ev.EventID(),
Type: ev.Type(),
OriginServerTS: ev.OriginServerTS(),
StateKey: &sk,
Content: ev.Content(),
Unsigned: ev.Unsigned(),
Sender: userID.String(),
Depth: ev.Depth(),
PrevEvents: ev.PrevEventIDs(),
AuthEvents: ev.AuthEventIDs(),
})
}
func userIDForSender(roomID spec.RoomID, senderID spec.SenderID) (*spec.UserID, error) {
if senderID == "unknownSenderID" {
return nil, fmt.Errorf("Cannot find userID")
}
return spec.NewUserID(testUserID, true)
}
func TestToClientEventsFormatSyncFederation(t *testing.T) { // nolint: gocyclo
ev, err := gomatrixserverlib.MustGetRoomVersion(gomatrixserverlib.RoomVersionPseudoIDs).NewEventFromTrustedJSON([]byte(`{
"type": "m.room.name",
"state_key": "testSenderID",
"event_id": "$test:localhost",
"room_id": "!test:localhost",
"sender": "testSenderID",
"content": {
"name": "Hello World"
},
"origin_server_ts": 123456,
"unsigned": {
"prev_content": {
"name": "Goodbye World"
}
},
"depth": 8,
"prev_events": [
"$f597Tp0Mm1PPxEgiprzJc2cZAjVhxCxACOGuwJb33Oo"
],
"auth_events": [
"$Bj0ZGgX6VTqAQdqKH4ZG3l6rlbxY3rZlC5D3MeuK1OQ",
"$QsMs6A1PUVUhgSvmHBfpqEYJPgv4DXt96r8P2AK7iXQ",
"$tBteKtlnFiwlmPJsv0wkKTMEuUVWpQH89H7Xskxve1Q"
]
}`), false)
if err != nil {
t.Fatalf("failed to create Event: %s", err)
}
ev2, err := gomatrixserverlib.MustGetRoomVersion(gomatrixserverlib.RoomVersionPseudoIDs).NewEventFromTrustedJSON([]byte(`{
"type": "m.room.name",
"state_key": "testSenderID",
"event_id": "$test2:localhost",
"room_id": "!test:localhost",
"sender": "testSenderID",
"content": {
"name": "Hello World 2"
},
"origin_server_ts": 1234567,
"unsigned": {
"prev_content": {
"name": "Goodbye World 2"
},
"prev_sender": "testSenderID"
},
"depth": 9,
"prev_events": [
"$f597Tp0Mm1PPxEgiprzJc2cZAjVhxCxACOGuwJb33Oo"
],
"auth_events": [
"$Bj0ZGgX6VTqAQdqKH4ZG3l6rlbxY3rZlC5D3MeuK1OQ",
"$QsMs6A1PUVUhgSvmHBfpqEYJPgv4DXt96r8P2AK7iXQ",
"$tBteKtlnFiwlmPJsv0wkKTMEuUVWpQH89H7Xskxve1Q"
]
}`), false)
if err != nil {
t.Fatalf("failed to create Event: %s", err)
}
clientEvents := ToClientEvents([]gomatrixserverlib.PDU{ev, ev2}, FormatSyncFederation, userIDForSender)
ce := clientEvents[0]
sk := testSenderID
verifyEventFields(t,
EventFieldsToVerify{
EventID: ce.EventID,
Type: ce.Type,
OriginServerTS: ce.OriginServerTS,
StateKey: ce.StateKey,
Content: ce.Content,
Unsigned: ce.Unsigned,
Sender: ce.Sender,
Depth: ce.Depth,
PrevEvents: ce.PrevEvents,
AuthEvents: ce.AuthEvents,
},
EventFieldsToVerify{
EventID: ev.EventID(),
Type: ev.Type(),
OriginServerTS: ev.OriginServerTS(),
StateKey: &sk,
Content: ev.Content(),
Unsigned: ev.Unsigned(),
Sender: testSenderID,
Depth: ev.Depth(),
PrevEvents: ev.PrevEventIDs(),
AuthEvents: ev.AuthEventIDs(),
})
ce2 := clientEvents[1]
verifyEventFields(t,
EventFieldsToVerify{
EventID: ce2.EventID,
Type: ce2.Type,
OriginServerTS: ce2.OriginServerTS,
StateKey: ce2.StateKey,
Content: ce2.Content,
Unsigned: ce2.Unsigned,
Sender: ce2.Sender,
Depth: ce2.Depth,
PrevEvents: ce2.PrevEvents,
AuthEvents: ce2.AuthEvents,
},
EventFieldsToVerify{
EventID: ev2.EventID(),
Type: ev2.Type(),
OriginServerTS: ev2.OriginServerTS(),
StateKey: &sk,
Content: ev2.Content(),
Unsigned: ev2.Unsigned(),
Sender: testSenderID,
Depth: ev2.Depth(),
PrevEvents: ev2.PrevEventIDs(),
AuthEvents: ev2.AuthEventIDs(),
})
}
func TestToClientEventsFormatSync(t *testing.T) { // nolint: gocyclo
ev, err := gomatrixserverlib.MustGetRoomVersion(gomatrixserverlib.RoomVersionPseudoIDs).NewEventFromTrustedJSON([]byte(`{
"type": "m.room.name",
"state_key": "testSenderID",
"event_id": "$test:localhost",
"room_id": "!test:localhost",
"sender": "testSenderID",
"content": {
"name": "Hello World"
},
"origin_server_ts": 123456,
"unsigned": {
"prev_content": {
"name": "Goodbye World"
}
}
}`), false)
if err != nil {
t.Fatalf("failed to create Event: %s", err)
}
ev2, err := gomatrixserverlib.MustGetRoomVersion(gomatrixserverlib.RoomVersionPseudoIDs).NewEventFromTrustedJSON([]byte(`{
"type": "m.room.name",
"state_key": "testSenderID",
"event_id": "$test2:localhost",
"room_id": "!test:localhost",
"sender": "testSenderID",
"content": {
"name": "Hello World 2"
},
"origin_server_ts": 1234567,
"unsigned": {
"prev_content": {
"name": "Goodbye World 2"
},
"prev_sender": "testSenderID"
},
"depth": 9
}`), false)
if err != nil {
t.Fatalf("failed to create Event: %s", err)
}
clientEvents := ToClientEvents([]gomatrixserverlib.PDU{ev, ev2}, FormatSync, userIDForSender)
ce := clientEvents[0]
sk := testUserID
verifyEventFields(t,
EventFieldsToVerify{
EventID: ce.EventID,
Type: ce.Type,
OriginServerTS: ce.OriginServerTS,
StateKey: ce.StateKey,
Content: ce.Content,
Unsigned: ce.Unsigned,
Sender: ce.Sender,
},
EventFieldsToVerify{
EventID: ev.EventID(),
Type: ev.Type(),
OriginServerTS: ev.OriginServerTS(),
StateKey: &sk,
Content: ev.Content(),
Unsigned: ev.Unsigned(),
Sender: testUserID,
})
var prev PrevEventRef
prev.PrevContent = []byte(`{"name": "Goodbye World 2"}`)
prev.PrevSenderID = testUserID
expectedUnsigned, _ := json.Marshal(prev)
ce2 := clientEvents[1]
verifyEventFields(t,
EventFieldsToVerify{
EventID: ce2.EventID,
Type: ce2.Type,
OriginServerTS: ce2.OriginServerTS,
StateKey: ce2.StateKey,
Content: ce2.Content,
Unsigned: ce2.Unsigned,
Sender: ce2.Sender,
},
EventFieldsToVerify{
EventID: ev2.EventID(),
Type: ev2.Type(),
OriginServerTS: ev2.OriginServerTS(),
StateKey: &sk,
Content: ev2.Content(),
Unsigned: expectedUnsigned,
Sender: testUserID,
})
}
func TestToClientEventsFormatSyncUnknownPrevSender(t *testing.T) { // nolint: gocyclo
ev, err := gomatrixserverlib.MustGetRoomVersion(gomatrixserverlib.RoomVersionPseudoIDs).NewEventFromTrustedJSON([]byte(`{
"type": "m.room.name",
"state_key": "testSenderID",
"event_id": "$test:localhost",
"room_id": "!test:localhost",
"sender": "testSenderID",
"content": {
"name": "Hello World"
},
"origin_server_ts": 123456,
"unsigned": {
"prev_content": {
"name": "Goodbye World"
}
}
}`), false)
if err != nil {
t.Fatalf("failed to create Event: %s", err)
}
ev2, err := gomatrixserverlib.MustGetRoomVersion(gomatrixserverlib.RoomVersionPseudoIDs).NewEventFromTrustedJSON([]byte(`{
"type": "m.room.name",
"state_key": "testSenderID",
"event_id": "$test2:localhost",
"room_id": "!test:localhost",
"sender": "testSenderID",
"content": {
"name": "Hello World 2"
},
"origin_server_ts": 1234567,
"unsigned": {
"prev_content": {
"name": "Goodbye World 2"
},
"prev_sender": "unknownSenderID"
},
"depth": 9
}`), false)
if err != nil {
t.Fatalf("failed to create Event: %s", err)
}
clientEvents := ToClientEvents([]gomatrixserverlib.PDU{ev, ev2}, FormatSync, userIDForSender)
ce := clientEvents[0]
sk := testUserID
verifyEventFields(t,
EventFieldsToVerify{
EventID: ce.EventID,
Type: ce.Type,
OriginServerTS: ce.OriginServerTS,
StateKey: ce.StateKey,
Content: ce.Content,
Unsigned: ce.Unsigned,
Sender: ce.Sender,
},
EventFieldsToVerify{
EventID: ev.EventID(),
Type: ev.Type(),
OriginServerTS: ev.OriginServerTS(),
StateKey: &sk,
Content: ev.Content(),
Unsigned: ev.Unsigned(),
Sender: testUserID,
})
var prev PrevEventRef
prev.PrevContent = []byte(`{"name": "Goodbye World 2"}`)
prev.PrevSenderID = "unknownSenderID"
expectedUnsigned, _ := json.Marshal(prev)
ce2 := clientEvents[1]
verifyEventFields(t,
EventFieldsToVerify{
EventID: ce2.EventID,
Type: ce2.Type,
OriginServerTS: ce2.OriginServerTS,
StateKey: ce2.StateKey,
Content: ce2.Content,
Unsigned: ce2.Unsigned,
Sender: ce2.Sender,
},
EventFieldsToVerify{
EventID: ev2.EventID(),
Type: ev2.Type(),
OriginServerTS: ev2.OriginServerTS(),
StateKey: &sk,
Content: ev2.Content(),
Unsigned: expectedUnsigned,
Sender: testUserID,
})
}

View file

@ -78,9 +78,14 @@ type RoomEventFilter struct {
ContainsURL *bool `json:"contains_url,omitempty"`
}
const (
EventFormatClient = "client"
EventFormatFederation = "federation"
)
// Validate checks if the filter contains valid property values
func (filter *Filter) Validate() error {
if filter.EventFormat != "" && filter.EventFormat != "client" && filter.EventFormat != "federation" {
if filter.EventFormat != "" && filter.EventFormat != EventFormatClient && filter.EventFormat != EventFormatFederation {
return errors.New("Bad event_format value. Must be one of [\"client\", \"federation\"]")
}
return nil