diff --git a/clientapi/routing/sendevent.go b/clientapi/routing/sendevent.go index 70bf72f88..2e864adef 100644 --- a/clientapi/routing/sendevent.go +++ b/clientapi/routing/sendevent.go @@ -19,6 +19,7 @@ import ( "encoding/json" "fmt" "net/http" + "reflect" "sync" "time" @@ -96,14 +97,21 @@ func SendEvent( mutex.(*sync.Mutex).Lock() defer mutex.(*sync.Mutex).Unlock() - startedGeneratingEvent := time.Now() - var r map[string]interface{} // must be a JSON object resErr := httputil.UnmarshalJSONRequest(req, &r) if resErr != nil { return *resErr } + if stateKey != nil { + // If the existing/new state content are equal, return the existing event_id, making the request idempotent. + if resp := stateEqual(req.Context(), rsAPI, eventType, *stateKey, roomID, r); resp != nil { + return *resp + } + } + + startedGeneratingEvent := time.Now() + // If we're sending a membership update, make sure to strip the authorised // via key if it is present, otherwise other servers won't be able to auth // the event if the room is set to the "restricted" join rule. @@ -208,6 +216,37 @@ func SendEvent( return res } +// stateEqual compares the new and the existing state event content. If they are equal, returns a *util.JSONResponse +// with the existing event_id, making this an idempotent request. +func stateEqual(ctx context.Context, rsAPI api.ClientRoomserverAPI, eventType, stateKey, roomID string, newContent map[string]interface{}) *util.JSONResponse { + stateRes := api.QueryCurrentStateResponse{} + tuple := gomatrixserverlib.StateKeyTuple{ + EventType: eventType, + StateKey: stateKey, + } + err := rsAPI.QueryCurrentState(ctx, &api.QueryCurrentStateRequest{ + RoomID: roomID, + StateTuples: []gomatrixserverlib.StateKeyTuple{tuple}, + }, &stateRes) + if err != nil { + return nil + } + if existingEvent, ok := stateRes.StateEvents[tuple]; ok { + var existingContent map[string]interface{} + if err = json.Unmarshal(existingEvent.Content(), &existingContent); err != nil { + return nil + } + if reflect.DeepEqual(existingContent, newContent) { + return &util.JSONResponse{ + Code: http.StatusOK, + JSON: sendEventResponse{existingEvent.EventID()}, + } + } + + } + return nil +} + func generateSendEvent( ctx context.Context, r map[string]interface{}, diff --git a/sytest-whitelist b/sytest-whitelist index 6af8d89ff..5f6797a3e 100644 --- a/sytest-whitelist +++ b/sytest-whitelist @@ -715,4 +715,7 @@ Presence can be set from sync PUT /rooms/:room_id/redact/:event_id/:txn_id is idempotent Unnamed room comes with a name summary Named room comes with just joined member count summary -Room summary only has 5 heroes \ No newline at end of file +Room summary only has 5 heroes +Setting state twice is idempotent +Joining room twice is idempotent +Inbound federation can return missing events for shared visibility \ No newline at end of file