diff --git a/spec/integ/crypto/crypto.spec.ts b/spec/integ/crypto/crypto.spec.ts index 50b5f4dea05..fd3b2778cfd 100644 --- a/spec/integ/crypto/crypto.spec.ts +++ b/spec/integ/crypto/crypto.spec.ts @@ -630,6 +630,27 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED); }); + newBackendOnly( + "fails with NOT_JOINED if user is not member of room (MSC4115 unstable prefix)", + async () => { + fetchMock.get("path:/_matrix/client/v3/room_keys/version", { + status: 404, + body: { errcode: "M_NOT_FOUND", error: "No current backup version." }, + }); + expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); + await startClientAndAwaitFirstSync(); + + const ev = await sendEventAndAwaitDecryption({ + unsigned: { + [UNSIGNED_MEMBERSHIP_FIELD.altName!]: "leave", + }, + }); + expect(ev.decryptionFailureReason).toEqual( + DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED, + ); + }, + ); + newBackendOnly( "fails with another error when the server reports user was a member of the room", async () => { @@ -654,6 +675,30 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, }, ); + newBackendOnly( + "fails with another error when the server reports user was a member of the room (MSC4115 unstable prefix)", + async () => { + // This tests that when the server reports that the user + // was invited at the time the event was sent, then we + // don't get a HISTORICAL_MESSAGE_USER_NOT_JOINED error, + // and instead get some other error, since the user should + // have gotten the key for the event. + fetchMock.get("path:/_matrix/client/v3/room_keys/version", { + status: 404, + body: { errcode: "M_NOT_FOUND", error: "No current backup version." }, + }); + expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); + await startClientAndAwaitFirstSync(); + + const ev = await sendEventAndAwaitDecryption({ + unsigned: { + [UNSIGNED_MEMBERSHIP_FIELD.altName!]: "invite", + }, + }); + expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP); + }, + ); + newBackendOnly( "fails with another error when the server reports user was a member of the room", async () => { @@ -676,6 +721,29 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP); }, ); + + newBackendOnly( + "fails with another error when the server reports user was a member of the room (MSC4115 unstable prefix)", + async () => { + // This tests that when the server reports the user's + // membership, and reports that the user was joined, then we + // don't get a HISTORICAL_MESSAGE_USER_NOT_JOINED error, and + // instead get some other error. + fetchMock.get("path:/_matrix/client/v3/room_keys/version", { + status: 404, + body: { errcode: "M_NOT_FOUND", error: "No current backup version." }, + }); + expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); + await startClientAndAwaitFirstSync(); + + const ev = await sendEventAndAwaitDecryption({ + unsigned: { + [UNSIGNED_MEMBERSHIP_FIELD.altName!]: "join", + }, + }); + expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP); + }, + ); }); it("Decryption fails with Unable to decrypt for other errors", async () => { diff --git a/src/@types/event.ts b/src/@types/event.ts index 5d8a26c9b23..44ef4c1434c 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { UnstableValue } from "../NamespacedValue"; +import { NamespacedValue, UnstableValue } from "../NamespacedValue"; import { PolicyRuleEventContent, RoomAvatarEventContent, @@ -302,7 +302,7 @@ export const UNSIGNED_THREAD_ID_FIELD = new UnstableValue("thread_id", "org.matr * * @experimental */ -export const UNSIGNED_MEMBERSHIP_FIELD = new UnstableValue("membership", "io.element.msc4115.membership"); +export const UNSIGNED_MEMBERSHIP_FIELD = new NamespacedValue("membership", "io.element.msc4115.membership"); /** * Mapped type from event type to content type for all specified non-state room events. diff --git a/src/models/event.ts b/src/models/event.ts index 7b9365aad50..e78a2f5b233 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -77,7 +77,6 @@ export interface IUnsigned { "invite_room_state"?: StrippedState[]; "m.relations"?: Record; // No common pattern for aggregated relations [UNSIGNED_THREAD_ID_FIELD.name]?: string; - [UNSIGNED_MEMBERSHIP_FIELD.name]?: Membership | string; } export interface IThreadBundledRelationship { @@ -717,13 +716,9 @@ export class MatrixEvent extends TypedEventEmitter { const unsigned = this.getUnsigned(); - if (typeof unsigned[UNSIGNED_MEMBERSHIP_FIELD.name] === "string") { - return unsigned[UNSIGNED_MEMBERSHIP_FIELD.name]; - } else { - return undefined; - } + return UNSIGNED_MEMBERSHIP_FIELD.findIn(unsigned); } /**