diff --git a/eventV1.go b/eventV1.go index 1404b058..39081b87 100644 --- a/eventV1.go +++ b/eventV1.go @@ -144,6 +144,10 @@ func (e *eventV1) AuthEventIDs() []string { return result } +func (e *eventV1) PrevStateEventIDs() []string { + panic("not implemented in this room version") +} + func (e *eventV1) OriginServerTS() spec.Timestamp { return e.eventFields.OriginServerTS } diff --git a/eventV2.go b/eventV2.go index 55ce556a..63c7624d 100644 --- a/eventV2.go +++ b/eventV2.go @@ -15,8 +15,9 @@ import ( type eventV2 struct { eventV1 - PrevEvents []string `json:"prev_events"` - AuthEvents []string `json:"auth_events"` + PrevEvents []string `json:"prev_events"` + AuthEvents []string `json:"auth_events"` + PrevStateEvents []string `json:"prev_state_events"` } func (e *eventV2) PrevEventIDs() []string { @@ -27,6 +28,10 @@ func (e *eventV2) AuthEventIDs() []string { return e.AuthEvents } +func (e *eventV2) PrevStateEventIDs() []string { + return e.PrevStateEvents +} + // MarshalJSON implements json.Marshaller func (e *eventV2) MarshalJSON() ([]byte, error) { if e.eventJSON == nil { @@ -185,6 +190,8 @@ func newEventFromUntrustedJSONV2(eventJSON []byte, roomVersion IRoomVersion) (PD return res, err } +// This has to exist outside the RoomVersion interface due to init cycles caused +// when you try to MustGetRoomVersion inside CheckFields. Ideally we'd refactor that... var lenientByteLimitRoomVersions = map[RoomVersion]struct{}{ RoomVersionV1: {}, RoomVersionV2: {}, @@ -204,10 +211,24 @@ var lenientByteLimitRoomVersions = map[RoomVersion]struct{}{ "org.matrix.msc3667": {}, } +var stateDAGRoomVersions = map[RoomVersion]struct{}{ + RoomVersionStateDAGs: {}, +} + func CheckFields(input PDU) error { // nolint: gocyclo - if input.AuthEventIDs() == nil || input.PrevEventIDs() == nil { - return errors.New("gomatrixserverlib: auth events and prev events must not be nil") + // don't check auth event IDs in auth DAG rooms as it doesn't exist. + _, stateDAGs := stateDAGRoomVersions[input.Version()] + if !stateDAGs && input.AuthEventIDs() == nil { + return errors.New("gomatrixserverlib: auth events must not be nil") } + if input.PrevEventIDs() == nil { + return errors.New("gomatrixserverlib: prev events must not be nil") + } + if stateDAGs && input.PrevStateEventIDs() == nil { + // create event should be [] + return errors.New("gomatrixserverlib: prev_state_events must not be nil") + } + if l := len(input.JSON()); l > maxEventLength { return EventValidationError{ Code: EventValidationTooLarge, diff --git a/eventV2_test.go b/eventV2_test.go index b9d5a6a8..bd8ce0c4 100644 --- a/eventV2_test.go +++ b/eventV2_test.go @@ -146,7 +146,7 @@ func TestCheckFields(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { for roomVersion := range roomVersionMeta { - if roomVersion == RoomVersionPseudoIDs { + if roomVersion == RoomVersionPseudoIDs || roomVersion == RoomVersionStateDAGs { continue } t.Run(tt.name+"-"+string(roomVersion), func(t *testing.T) { diff --git a/event_builder.go b/event_builder.go index 28438148..5262f2ef 100644 --- a/event_builder.go +++ b/event_builder.go @@ -29,6 +29,8 @@ type EventBuilder struct { // The events needed to authenticate this event. This can be // either []eventReference for room v1/v2, and []string for room v3 onwards. AuthEvents interface{} `json:"auth_events"` + // Previous state events, for MSC4242 state dag rooms only. Pointer to allow [] to encode for the create event. + PrevStateEvents *[]string `json:"prev_state_events,omitempty"` // The event ID of the event being redacted if this event is a "m.room.redaction". Redacts string `json:"redacts,omitempty"` // The depth of the event, This should be one greater than the maximum depth of the previous events. @@ -145,6 +147,10 @@ func (eb *EventBuilder) Build( if eb.version == nil { return nil, fmt.Errorf("EventBuilder.Build: unknown version, did you create this via NewEventBuilder?") } + isStateDAGs := eb.version.StateDAGs() + if !isStateDAGs && eb.PrevStateEvents != nil { + return nil, fmt.Errorf("prev_state_events can only be set on RoomVersionStateDAGs") + } eventFormat := eb.version.EventFormat() eventIDFormat := eb.version.EventIDFormat() @@ -213,6 +219,12 @@ func (eb *EventBuilder) Build( } } + if isStateDAGs { + if eventJSON, err = sjson.DeleteBytes(eventJSON, "auth_events"); err != nil { + return + } + } + if eventJSON, err = addContentHashesToEvent(eventJSON); err != nil { return } diff --git a/eventversion.go b/eventversion.go index 3dfbdf7c..9f38585d 100644 --- a/eventversion.go +++ b/eventversion.go @@ -38,6 +38,7 @@ type IRoomVersion interface { CheckCreateEvent(event PDU, sender spec.UserID, knownRoomVersion KnownRoomVersionFunc) error DomainlessRoomIDs() bool PrivilegedCreators() bool + StateDAGs() bool } type KnownRoomVersionFunc func(RoomVersion) bool @@ -69,6 +70,7 @@ const ( RoomVersionV12 RoomVersion = "12" RoomVersionPseudoIDs RoomVersion = "org.matrix.msc4014" RoomVersionHydra RoomVersion = "org.matrix.hydra.11" + RoomVersionStateDAGs RoomVersion = "org.matrix.msc4242.12" ) // Event format constants. @@ -417,6 +419,31 @@ var roomVersionMeta = map[RoomVersion]IRoomVersion{ domainlessRoomID: true, privilegedCreators: true, }, + // Based on v12 + RoomVersionStateDAGs: RoomVersionImpl{ + ver: RoomVersionStateDAGs, + stable: true, + stateResAlgorithm: StateResV2_1, + eventFormat: EventFormatV2, + eventIDFormat: EventIDFormatV3, + redactionAlgorithm: redactEventJSONVStateDAGs, + signatureValidityCheckFunc: StrictValiditySignatureCheck, + canonicalJSONCheck: verifyEnforcedCanonicalJSON, + checkPowerLevelEvent: checkPowerLevelEventV3, + restrictedJoinServernameFunc: extractAuthorisedViaServerName, + checkRestrictedJoin: checkRestrictedJoin, + parsePowerLevelsFunc: parseIntegerPowerLevels, + checkKnockingAllowedFunc: checkKnocking, + checkRestrictedJoinAllowedFunc: allowRestrictedJoins, + checkCreateEvent: checkCreateEventV3, + // v3 versions relax the room ID check as the room ID has no domain now. + newEventFromUntrustedJSONFunc: newEventFromUntrustedJSONV3, + newEventFromTrustedJSONFunc: newEventFromTrustedJSONV3, + newEventFromTrustedJSONWithEventIDFunc: newEventFromTrustedJSONWithEventIDV3, + domainlessRoomID: true, + privilegedCreators: true, + stateDAGs: true, + }, } // RoomVersions returns information about room versions currently @@ -501,7 +528,9 @@ type RoomVersionImpl struct { // whether auth_events should include the create event domainlessRoomID bool // creators have infinite PL - privilegedCreators bool + privilegedCreators bool + // Events form two graphs, a state DAG and an event DAG. + stateDAGs bool checkRestrictedJoin func(ctx context.Context, localServerName spec.ServerName, roomQuerier RestrictedRoomJoinQuerier, roomID spec.RoomID, senderID spec.SenderID, privilegedCreators bool) (string, error) restrictedJoinServernameFunc func(content []byte) (spec.ServerName, error) checkRestrictedJoinAllowedFunc func() error @@ -525,6 +554,10 @@ func (v RoomVersionImpl) DomainlessRoomIDs() bool { return v.domainlessRoomID } +func (v RoomVersionImpl) StateDAGs() bool { + return v.stateDAGs +} + func (v RoomVersionImpl) PrivilegedCreators() bool { return v.privilegedCreators } @@ -630,6 +663,7 @@ func (v RoomVersionImpl) NewEventBuilderFromProtoEvent(pe *ProtoEvent) *EventBui eb.StateKey = pe.StateKey eb.Type = pe.Type eb.Unsigned = pe.Unsigned + eb.PrevStateEvents = pe.PrevStateEvents return eb } diff --git a/fclient/federationtypes.go b/fclient/federationtypes.go index c46dd0c4..0dc6a9e7 100644 --- a/fclient/federationtypes.go +++ b/fclient/federationtypes.go @@ -90,6 +90,8 @@ type MissingEvents struct { EarliestEvents []string `json:"earliest_events"` // The event IDs to retrieve the previous events for. LatestEvents []string `json:"latest_events"` + // If true, walks the state DAG. Only for MSC4242 State DAG rooms. + StateDAG bool `json:"org.matrix.msc4242.state_dag"` } // A RespMissingEvents is the content of a response to GET /_matrix/federation/v1/get_missing_events/{roomID} @@ -269,6 +271,8 @@ type RespSendJoin struct { StateEvents gomatrixserverlib.EventJSONs `json:"state"` // A list of events needed to authenticate the state events. AuthEvents gomatrixserverlib.EventJSONs `json:"auth_chain"` + // MSC4242: The entire state DAG for the room + StateDAG gomatrixserverlib.EventJSONs `json:"state_dag"` // The server that originated the event. Origin spec.ServerName `json:"origin"` // The returned join event from the remote server. Used for restricted joins, @@ -318,6 +322,9 @@ func (r RespSendJoin) MarshalJSON() ([]byte, error) { if len(fields.StateEvents) == 0 { fields.StateEvents = gomatrixserverlib.EventJSONs{} } + if len(r.StateDAG) > 0 { + fields.StateDAG = r.StateDAG + } if !r.MembersOmitted { return json.Marshal(fields) @@ -350,6 +357,7 @@ type RespMakeKnock struct { type respSendJoinFields struct { StateEvents gomatrixserverlib.EventJSONs `json:"state"` AuthEvents gomatrixserverlib.EventJSONs `json:"auth_chain"` + StateDAG gomatrixserverlib.EventJSONs `json:"state_dag,omitempty"` Origin spec.ServerName `json:"origin"` Event spec.RawJSON `json:"event,omitempty"` } diff --git a/join.go b/join.go index a6483e81..61b799c7 100644 --- a/join.go +++ b/join.go @@ -43,7 +43,9 @@ type ProtoEvent struct { PrevEvents interface{} `json:"prev_events"` // The events needed to authenticate this event. This can be // either []eventReference for room v1/v2, and []string for room v3 onwards. - AuthEvents interface{} `json:"auth_events"` + AuthEvents interface{} `json:"auth_events,omitempty"` + // Previous state events, for MSC4242 state dag rooms. Pointer to allow [] to encode for the create event. + PrevStateEvents *[]string `json:"prev_state_events,omitempty"` // The event ID of the event being redacted if this event is a "m.room.redaction". Redacts string `json:"redacts,omitempty"` // The depth of the event, This should be one greater than the maximum depth of the previous events. diff --git a/pdu.go b/pdu.go index 347323db..bd9debec 100644 --- a/pdu.go +++ b/pdu.go @@ -53,6 +53,7 @@ type PDU interface { JSON() []byte // TODO: remove AuthEventIDs() []string // TODO: remove ToHeaderedJSON() ([]byte, error) // TODO: remove + PrevStateEventIDs() []string // IsSticky returns true if the event is *currently* considered "sticky" given the received time. // Sticky events are annotated as sticky and carry strong delivery guarantees to clients (and // therefore servers). `received` should be specified as the time the event was received by the diff --git a/redactevent.go b/redactevent.go index c69ccb01..163da411 100644 --- a/redactevent.go +++ b/redactevent.go @@ -141,7 +141,7 @@ func redactEventJSONV1(eventJSON []byte) ([]byte, error) { } type unredactableEvent interface { - *unredactableEventFieldsV1 | *unredactableEventFieldsV2 + *unredactableEventFieldsV1 | *unredactableEventFieldsV2 | *unredactableEventFieldsVStateDAGs GetType() string GetContent() map[string]interface{} SetContent(map[string]interface{}) @@ -172,3 +172,34 @@ func redactEventJSON[T unredactableEvent](eventJSON []byte, unredactableEvent T, // Return the redacted event encoded as JSON. return json.Marshal(&unredactableEvent) } + +type unredactableEventFieldsVStateDAGs struct { + EventID spec.RawJSON `json:"event_id,omitempty"` + Type string `json:"type"` + RoomID spec.RawJSON `json:"room_id,omitempty"` + Sender spec.RawJSON `json:"sender,omitempty"` + StateKey spec.RawJSON `json:"state_key,omitempty"` + Content map[string]interface{} `json:"content"` + Hashes spec.RawJSON `json:"hashes,omitempty"` + Signatures spec.RawJSON `json:"signatures,omitempty"` + Depth spec.RawJSON `json:"depth,omitempty"` + PrevEvents spec.RawJSON `json:"prev_events,omitempty"` + PrevStateEvents spec.RawJSON `json:"prev_state_events,omitempty"` + OriginServerTS spec.RawJSON `json:"origin_server_ts,omitempty"` +} + +func (u *unredactableEventFieldsVStateDAGs) GetType() string { + return u.Type +} + +func (u *unredactableEventFieldsVStateDAGs) GetContent() map[string]interface{} { + return u.Content +} + +func (u *unredactableEventFieldsVStateDAGs) SetContent(content map[string]interface{}) { + u.Content = content +} + +func redactEventJSONVStateDAGs(eventJSON []byte) ([]byte, error) { + return redactEventJSON(eventJSON, &unredactableEventFieldsVStateDAGs{}, unredactableContentFieldsV5) +}