diff --git a/federationclient.go b/federationclient.go index 95f77b02..ba45bd15 100644 --- a/federationclient.go +++ b/federationclient.go @@ -109,7 +109,7 @@ func (ac *FederationClient) MakeJoin( if len(roomVersions) > 0 { var vqs []string for _, v := range roomVersions { - vqs = append(vqs, fmt.Sprintf("ver=%s", v)) + vqs = append(vqs, fmt.Sprintf("ver=%s", url.QueryEscape(string(v)))) } versionQueryString = "?" + strings.Join(vqs, "&") } @@ -323,6 +323,31 @@ func (ac *FederationClient) LookupMissingEvents( return } +// Peek starts a peek on a remote server +func (ac *FederationClient) Peek( + ctx context.Context, s ServerName, roomID, peekID string, + roomVersions []RoomVersion, +) (res RespPeek, err error) { + versionQueryString := "" + if len(roomVersions) > 0 { + var vqs []string + for _, v := range roomVersions { + vqs = append(vqs, fmt.Sprintf("ver=%s", url.QueryEscape(string(v)))) + } + versionQueryString = "?" + strings.Join(vqs, "&") + } + path := federationPathPrefixV1 + "/peek/" + + url.PathEscape(roomID) + "/" + + url.PathEscape(peekID) + versionQueryString + req := NewFederationRequest("PUT", s, path) + var empty struct{} + if err = req.SetContent(empty); err != nil { + return + } + err = ac.doRequest(ctx, req, &res) + return +} + // LookupRoomAlias looks up a room alias hosted on the remote server. // The domain part of the roomAlias must match the name of the server it is // being looked up on. diff --git a/federationtypes.go b/federationtypes.go index b7cc4583..c5f72e17 100644 --- a/federationtypes.go +++ b/federationtypes.go @@ -137,6 +137,21 @@ type RespState struct { AuthEvents []Event `json:"auth_chain"` } +// A RespPeek is the content of a response to GET /_matrix/federation/v1/peek/{roomID}/{peekID} +type RespPeek struct { + // How often should we renew the peek? + RenewalInterval int64 `json:"renewal_interval"` + // A list of events giving the state of the room at the point of the request + StateEvents []Event `json:"state"` + // A list of events needed to authenticate the state events. + AuthEvents []Event `json:"auth_chain"` + // The room version that we're trying to peek. + RoomVersion RoomVersion `json:"room_version"` + // The ID of the event whose state snapshot this is - i.e. the + // most recent forward extremity in the room. + LatestEvent Event `json:"latest_event"` +} + // MissingEvents represents a request for missing events. // https://matrix.org/docs/spec/server_server/r0.1.3#post-matrix-federation-v1-get-missing-events-roomid type MissingEvents struct { @@ -256,6 +271,58 @@ func (r *RespMissingEvents) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON implements json.Marshaller +func (r RespPeek) MarshalJSON() ([]byte, error) { + if len(r.StateEvents) == 0 { + r.StateEvents = []Event{} + } + if len(r.AuthEvents) == 0 { + r.AuthEvents = []Event{} + } + return json.Marshal(r) +} + +// UnmarshalJSON implements json.Unmarshaller +func (r *RespPeek) UnmarshalJSON(data []byte) error { + r.AuthEvents = []Event{} + r.StateEvents = []Event{} + var intermediate struct { + RoomVersion RoomVersion `json:"room_version"` + StateEvents []json.RawMessage `json:"state"` + AuthEvents []json.RawMessage `json:"auth_chain"` + RenewalInterval int64 `json:"renewal_interval"` + LatestEvent json.RawMessage `json:"latest_event"` + } + if err := json.Unmarshal(data, &intermediate); err != nil { + return fmt.Errorf("RespPeek UnmarshalJSON(intermediate): %w", err) + } + r.RoomVersion = intermediate.RoomVersion + r.RenewalInterval = intermediate.RenewalInterval + if _, err := r.RoomVersion.EventFormat(); err != nil { + return fmt.Errorf("RespPeek UnmarshalJSON(roomversion): %w", err) + } + for _, raw := range intermediate.AuthEvents { + event, err := NewEventFromUntrustedJSON([]byte(raw), r.RoomVersion) + if err != nil { + return fmt.Errorf("RespPeek UnmarshalJSON(AuthEvent): %w", err) + } + r.AuthEvents = append(r.AuthEvents, event) + } + for _, raw := range intermediate.StateEvents { + event, err := NewEventFromUntrustedJSON([]byte(raw), r.RoomVersion) + if err != nil { + return fmt.Errorf("RespPeek UnmarshalJSON(StateEvent): %w", err) + } + r.StateEvents = append(r.StateEvents, event) + } + latestEvent, err := NewEventFromUntrustedJSON([]byte(intermediate.LatestEvent), r.RoomVersion) + if err != nil { + return fmt.Errorf("RespPeek UnmarshalJSON(StateEvent): %w", err) + } + r.LatestEvent = latestEvent + return nil +} + // MarshalJSON implements json.Marshaller func (r RespState) MarshalJSON() ([]byte, error) { if len(r.StateEvents) == 0 { @@ -533,6 +600,21 @@ func (r *RespSendJoin) UnmarshalJSON(data []byte) error { return nil } +// ToRespState returns a new RespState with the same data from the given RespPeek +func (r RespPeek) ToRespState() RespState { + if len(r.StateEvents) == 0 { + r.StateEvents = []Event{} + } + if len(r.AuthEvents) == 0 { + r.AuthEvents = []Event{} + } + return RespState{ + roomVersion: r.RoomVersion, + StateEvents: r.StateEvents, + AuthEvents: r.AuthEvents, + } +} + type respSendJoinFields struct { StateEvents []Event `json:"state"` AuthEvents []Event `json:"auth_chain"`