Merge branch 'master' into crypto-improvements
commit
007e452d39
|
@ -1498,6 +1498,18 @@ impl BaseClient {
|
||||||
.on_custom_event(room, &CustomEvent::Message(e))
|
.on_custom_event(room, &CustomEvent::Message(e))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
AnySyncMessageEvent::CallInvite(e) => {
|
||||||
|
event_emitter.on_room_call_invite(room, e).await
|
||||||
|
}
|
||||||
|
AnySyncMessageEvent::CallAnswer(e) => {
|
||||||
|
event_emitter.on_room_call_answer(room, e).await
|
||||||
|
}
|
||||||
|
AnySyncMessageEvent::CallCandidates(e) => {
|
||||||
|
event_emitter.on_room_call_candidates(room, e).await
|
||||||
|
}
|
||||||
|
AnySyncMessageEvent::CallHangup(e) => {
|
||||||
|
event_emitter.on_room_call_hangup(room, e).await
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
AnySyncRoomEvent::RedactedState(_event) => {}
|
AnySyncRoomEvent::RedactedState(_event) => {}
|
||||||
|
|
|
@ -19,6 +19,10 @@ use serde_json::value::RawValue as RawJsonValue;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
events::{
|
events::{
|
||||||
|
call::{
|
||||||
|
answer::AnswerEventContent, candidates::CandidatesEventContent,
|
||||||
|
hangup::HangupEventContent, invite::InviteEventContent,
|
||||||
|
},
|
||||||
custom::CustomEventContent,
|
custom::CustomEventContent,
|
||||||
fully_read::FullyReadEventContent,
|
fully_read::FullyReadEventContent,
|
||||||
ignored_user_list::IgnoredUserListEventContent,
|
ignored_user_list::IgnoredUserListEventContent,
|
||||||
|
@ -135,6 +139,19 @@ pub trait EventEmitter: Send + Sync {
|
||||||
_: &SyncMessageEvent<FeedbackEventContent>,
|
_: &SyncMessageEvent<FeedbackEventContent>,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
/// Fires when `Client` receives a `RoomEvent::CallInvite` event
|
||||||
|
async fn on_room_call_invite(&self, _: SyncRoom, _: &SyncMessageEvent<InviteEventContent>) {}
|
||||||
|
/// Fires when `Client` receives a `RoomEvent::CallAnswer` event
|
||||||
|
async fn on_room_call_answer(&self, _: SyncRoom, _: &SyncMessageEvent<AnswerEventContent>) {}
|
||||||
|
/// Fires when `Client` receives a `RoomEvent::CallCandidates` event
|
||||||
|
async fn on_room_call_candidates(
|
||||||
|
&self,
|
||||||
|
_: SyncRoom,
|
||||||
|
_: &SyncMessageEvent<CandidatesEventContent>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
/// Fires when `Client` receives a `RoomEvent::CallHangup` event
|
||||||
|
async fn on_room_call_hangup(&self, _: SyncRoom, _: &SyncMessageEvent<HangupEventContent>) {}
|
||||||
/// Fires when `Client` receives a `RoomEvent::RoomRedaction` event.
|
/// Fires when `Client` receives a `RoomEvent::RoomRedaction` event.
|
||||||
async fn on_room_redaction(&self, _: SyncRoom, _: &SyncRedactionEvent) {}
|
async fn on_room_redaction(&self, _: SyncRoom, _: &SyncRedactionEvent) {}
|
||||||
/// Fires when `Client` receives a `RoomEvent::RoomPowerLevels` event.
|
/// Fires when `Client` receives a `RoomEvent::RoomPowerLevels` event.
|
||||||
|
@ -317,6 +334,22 @@ mod test {
|
||||||
) {
|
) {
|
||||||
self.0.lock().await.push("feedback".to_string())
|
self.0.lock().await.push("feedback".to_string())
|
||||||
}
|
}
|
||||||
|
async fn on_room_call_invite(&self, _: SyncRoom, _: &SyncMessageEvent<InviteEventContent>) {
|
||||||
|
self.0.lock().await.push("call invite".to_string())
|
||||||
|
}
|
||||||
|
async fn on_room_call_answer(&self, _: SyncRoom, _: &SyncMessageEvent<AnswerEventContent>) {
|
||||||
|
self.0.lock().await.push("call answer".to_string())
|
||||||
|
}
|
||||||
|
async fn on_room_call_candidates(
|
||||||
|
&self,
|
||||||
|
_: SyncRoom,
|
||||||
|
_: &SyncMessageEvent<CandidatesEventContent>,
|
||||||
|
) {
|
||||||
|
self.0.lock().await.push("call candidates".to_string())
|
||||||
|
}
|
||||||
|
async fn on_room_call_hangup(&self, _: SyncRoom, _: &SyncMessageEvent<HangupEventContent>) {
|
||||||
|
self.0.lock().await.push("call hangup".to_string())
|
||||||
|
}
|
||||||
async fn on_room_redaction(&self, _: SyncRoom, _: &SyncRedactionEvent) {
|
async fn on_room_redaction(&self, _: SyncRoom, _: &SyncRedactionEvent) {
|
||||||
self.0.lock().await.push("redaction".to_string())
|
self.0.lock().await.push("redaction".to_string())
|
||||||
}
|
}
|
||||||
|
@ -601,4 +634,28 @@ mod test {
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_test]
|
||||||
|
async fn event_emitter_voip() {
|
||||||
|
let vec = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let test_vec = Arc::clone(&vec);
|
||||||
|
let emitter = Box::new(EvEmitterTest(vec));
|
||||||
|
|
||||||
|
let client = get_client().await;
|
||||||
|
client.add_event_emitter(emitter).await;
|
||||||
|
|
||||||
|
let mut response = sync_response(SyncResponseFile::Voip);
|
||||||
|
client.receive_sync_response(&mut response).await.unwrap();
|
||||||
|
|
||||||
|
let v = test_vec.lock().await;
|
||||||
|
assert_eq!(
|
||||||
|
v.as_slice(),
|
||||||
|
[
|
||||||
|
"call invite",
|
||||||
|
"call answer",
|
||||||
|
"call candidates",
|
||||||
|
"call hangup",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ use matrix_sdk_common::{
|
||||||
instant::Instant,
|
instant::Instant,
|
||||||
js_int::UInt,
|
js_int::UInt,
|
||||||
locks::Mutex,
|
locks::Mutex,
|
||||||
Raw,
|
CanonicalJsonValue, Raw,
|
||||||
};
|
};
|
||||||
use olm_rs::{
|
use olm_rs::{
|
||||||
account::{IdentityKeys, OlmAccount, OneTimeKeys},
|
account::{IdentityKeys, OlmAccount, OneTimeKeys},
|
||||||
|
@ -743,7 +743,7 @@ impl ReadOnlyAccount {
|
||||||
.or_insert_with(BTreeMap::new)
|
.or_insert_with(BTreeMap::new)
|
||||||
.insert(
|
.insert(
|
||||||
DeviceKeyId::from_parts(DeviceKeyAlgorithm::Ed25519, &self.device_id),
|
DeviceKeyId::from_parts(DeviceKeyAlgorithm::Ed25519, &self.device_id),
|
||||||
self.sign_json(&json_device_keys).await,
|
self.sign_json(json_device_keys).await,
|
||||||
);
|
);
|
||||||
|
|
||||||
device_keys
|
device_keys
|
||||||
|
@ -770,8 +770,10 @@ impl ReadOnlyAccount {
|
||||||
/// # Panic
|
/// # Panic
|
||||||
///
|
///
|
||||||
/// Panics if the json value can't be serialized.
|
/// Panics if the json value can't be serialized.
|
||||||
pub async fn sign_json(&self, json: &Value) -> String {
|
pub async fn sign_json(&self, json: Value) -> String {
|
||||||
self.sign(&json.to_string()).await
|
let canonical_json: CanonicalJsonValue =
|
||||||
|
json.try_into().expect("Can't canonicalize the json value");
|
||||||
|
self.sign(&canonical_json.to_string()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn signed_one_time_keys_helper(
|
pub(crate) async fn signed_one_time_keys_helper(
|
||||||
|
@ -785,7 +787,7 @@ impl ReadOnlyAccount {
|
||||||
"key": key,
|
"key": key,
|
||||||
});
|
});
|
||||||
|
|
||||||
let signature = self.sign_json(&key_json).await;
|
let signature = self.sign_json(key_json).await;
|
||||||
|
|
||||||
let mut signature_map = BTreeMap::new();
|
let mut signature_map = BTreeMap::new();
|
||||||
|
|
||||||
|
|
|
@ -214,7 +214,7 @@ impl PrivateCrossSigningIdentity {
|
||||||
master.cross_signing_key(account.user_id().to_owned(), KeyUsage::Master);
|
master.cross_signing_key(account.user_id().to_owned(), KeyUsage::Master);
|
||||||
let signature = account
|
let signature = account
|
||||||
.sign_json(
|
.sign_json(
|
||||||
&serde_json::to_value(&public_key)
|
serde_json::to_value(&public_key)
|
||||||
.expect("Can't convert own public master key to json"),
|
.expect("Can't convert own public master key to json"),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
|
@ -23,7 +23,7 @@ use matrix_sdk_common::{
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{json, Error as JsonError, Value};
|
use serde_json::{json, Error as JsonError, Value};
|
||||||
use std::{collections::BTreeMap, sync::Arc};
|
use std::{collections::BTreeMap, convert::TryInto, sync::Arc};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use zeroize::Zeroizing;
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
|
@ -36,6 +36,7 @@ use matrix_sdk_common::{
|
||||||
api::r0::keys::{CrossSigningKey, KeyUsage},
|
api::r0::keys::{CrossSigningKey, KeyUsage},
|
||||||
identifiers::UserId,
|
identifiers::UserId,
|
||||||
locks::Mutex,
|
locks::Mutex,
|
||||||
|
CanonicalJsonValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -404,8 +405,9 @@ impl Signing {
|
||||||
pub async fn sign_json(&self, mut json: Value) -> Result<Signature, SignatureError> {
|
pub async fn sign_json(&self, mut json: Value) -> Result<Signature, SignatureError> {
|
||||||
let json_object = json.as_object_mut().ok_or(SignatureError::NotAnObject)?;
|
let json_object = json.as_object_mut().ok_or(SignatureError::NotAnObject)?;
|
||||||
let _ = json_object.remove("signatures");
|
let _ = json_object.remove("signatures");
|
||||||
let canonical_json = serde_json::to_string(json_object)?;
|
let canonical_json: CanonicalJsonValue =
|
||||||
Ok(self.sign(&canonical_json).await)
|
json.try_into().expect("Can't canonicalize the json value");
|
||||||
|
Ok(self.sign(&canonical_json.to_string()).await)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn sign(&self, message: &str) -> Signature {
|
pub async fn sign(&self, message: &str) -> Signature {
|
||||||
|
|
|
@ -14,8 +14,12 @@
|
||||||
|
|
||||||
use olm_rs::utility::OlmUtility;
|
use olm_rs::utility::OlmUtility;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
|
||||||
use matrix_sdk_common::identifiers::{DeviceKeyAlgorithm, DeviceKeyId, UserId};
|
use matrix_sdk_common::{
|
||||||
|
identifiers::{DeviceKeyAlgorithm, DeviceKeyId, UserId},
|
||||||
|
CanonicalJsonValue,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::error::SignatureError;
|
use crate::error::SignatureError;
|
||||||
|
|
||||||
|
@ -63,11 +67,12 @@ impl Utility {
|
||||||
let unsigned = json_object.remove("unsigned");
|
let unsigned = json_object.remove("unsigned");
|
||||||
let signatures = json_object.remove("signatures");
|
let signatures = json_object.remove("signatures");
|
||||||
|
|
||||||
let canonical_json = serde_json::to_string(json_object)?;
|
let canonical_json: CanonicalJsonValue = json
|
||||||
|
.clone()
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| SignatureError::NotAnObject)?;
|
||||||
|
|
||||||
if let Some(u) = unsigned {
|
let canonical_json: String = canonical_json.to_string();
|
||||||
json_object.insert("unsigned".to_string(), u);
|
|
||||||
}
|
|
||||||
|
|
||||||
let signatures = signatures.ok_or(SignatureError::NoSignatureFound)?;
|
let signatures = signatures.ok_or(SignatureError::NoSignatureFound)?;
|
||||||
let signature_object = signatures
|
let signature_object = signatures
|
||||||
|
@ -81,18 +86,66 @@ impl Utility {
|
||||||
.ok_or(SignatureError::NoSignatureFound)?;
|
.ok_or(SignatureError::NoSignatureFound)?;
|
||||||
let signature = signature.as_str().ok_or(SignatureError::NoSignatureFound)?;
|
let signature = signature.as_str().ok_or(SignatureError::NoSignatureFound)?;
|
||||||
|
|
||||||
let ret = if self
|
let ret = match self
|
||||||
.inner
|
.inner
|
||||||
.ed25519_verify(signing_key, &canonical_json, signature)
|
.ed25519_verify(signing_key, &canonical_json, signature)
|
||||||
.is_ok()
|
|
||||||
{
|
{
|
||||||
Ok(())
|
Ok(_) => Ok(()),
|
||||||
} else {
|
Err(_) => Err(SignatureError::VerificationError),
|
||||||
Err(SignatureError::VerificationError)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let json_object = json.as_object_mut().ok_or(SignatureError::NotAnObject)?;
|
||||||
|
|
||||||
|
if let Some(u) = unsigned {
|
||||||
|
json_object.insert("unsigned".to_string(), u);
|
||||||
|
}
|
||||||
|
|
||||||
json_object.insert("signatures".to_string(), signatures);
|
json_object.insert("signatures".to_string(), signatures);
|
||||||
|
|
||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::Utility;
|
||||||
|
use matrix_sdk_common::identifiers::{user_id, DeviceKeyAlgorithm, DeviceKeyId};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn signature_test() {
|
||||||
|
let mut device_keys = json!({
|
||||||
|
"device_id": "GBEWHQOYGS",
|
||||||
|
"algorithms": [
|
||||||
|
"m.olm.v1.curve25519-aes-sha2",
|
||||||
|
"m.megolm.v1.aes-sha2"
|
||||||
|
],
|
||||||
|
"keys": {
|
||||||
|
"curve25519:GBEWHQOYGS": "F8QhZ0Z1rjtWrQOblMDgZtEX5x1UrG7sZ2Kk3xliNAU",
|
||||||
|
"ed25519:GBEWHQOYGS": "n469gw7zm+KW+JsFIJKnFVvCKU14HwQyocggcCIQgZY"
|
||||||
|
},
|
||||||
|
"signatures": {
|
||||||
|
"@example:localhost": {
|
||||||
|
"ed25519:GBEWHQOYGS": "OlF2REsqjYdAfr04ONx8VS/5cB7KjrWYRlLF4eUm2foAiQL/RAfsjsa2JXZeoOHh6vEualZHbWlod49OewVqBg"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unsigned": {
|
||||||
|
"device_display_name": "Weechat-Matrix-rs"
|
||||||
|
},
|
||||||
|
"user_id": "@example:localhost"
|
||||||
|
});
|
||||||
|
|
||||||
|
let signing_key = "n469gw7zm+KW+JsFIJKnFVvCKU14HwQyocggcCIQgZY";
|
||||||
|
|
||||||
|
let utility = Utility::new();
|
||||||
|
|
||||||
|
utility
|
||||||
|
.verify_json(
|
||||||
|
&user_id!("@example:localhost"),
|
||||||
|
&DeviceKeyId::from_parts(DeviceKeyAlgorithm::Ed25519, "GBEWHQOYGS".into()),
|
||||||
|
&signing_key,
|
||||||
|
&mut device_keys,
|
||||||
|
)
|
||||||
|
.expect("Can't verify device keys");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -355,6 +355,7 @@ pub enum SyncResponseFile {
|
||||||
DefaultWithSummary,
|
DefaultWithSummary,
|
||||||
Invite,
|
Invite,
|
||||||
Leave,
|
Leave,
|
||||||
|
Voip,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get specific API responses for testing
|
/// Get specific API responses for testing
|
||||||
|
@ -365,6 +366,7 @@ pub fn sync_response(kind: SyncResponseFile) -> SyncResponse {
|
||||||
SyncResponseFile::DefaultWithSummary => &test_json::DEFAULT_SYNC_SUMMARY,
|
SyncResponseFile::DefaultWithSummary => &test_json::DEFAULT_SYNC_SUMMARY,
|
||||||
SyncResponseFile::Invite => &test_json::INVITE_SYNC,
|
SyncResponseFile::Invite => &test_json::INVITE_SYNC,
|
||||||
SyncResponseFile::Leave => &test_json::LEAVE_SYNC,
|
SyncResponseFile::Leave => &test_json::LEAVE_SYNC,
|
||||||
|
SyncResponseFile::Voip => &test_json::VOIP_SYNC,
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = Response::builder()
|
let response = Response::builder()
|
||||||
|
|
|
@ -16,7 +16,9 @@ pub use events::{
|
||||||
REACTION, REDACTED, REDACTED_INVALID, REDACTED_STATE, REDACTION, REGISTRATION_RESPONSE_ERR,
|
REACTION, REDACTED, REDACTED_INVALID, REDACTED_STATE, REDACTION, REGISTRATION_RESPONSE_ERR,
|
||||||
ROOM_ID, ROOM_MESSAGES, TYPING,
|
ROOM_ID, ROOM_MESSAGES, TYPING,
|
||||||
};
|
};
|
||||||
pub use sync::{DEFAULT_SYNC_SUMMARY, INVITE_SYNC, LEAVE_SYNC, LEAVE_SYNC_EVENT, MORE_SYNC, SYNC};
|
pub use sync::{
|
||||||
|
DEFAULT_SYNC_SUMMARY, INVITE_SYNC, LEAVE_SYNC, LEAVE_SYNC_EVENT, MORE_SYNC, SYNC, VOIP_SYNC,
|
||||||
|
};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref DEVICES: JsonValue = json!({
|
pub static ref DEVICES: JsonValue = json!({
|
||||||
|
|
|
@ -1101,3 +1101,123 @@ lazy_static! {
|
||||||
"next_batch": "s1380317562_757269739_1655566_503953763_334052043_1209862_55290918_65705002_101146"
|
"next_batch": "s1380317562_757269739_1655566_503953763_334052043_1209862_55290918_65705002_101146"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref VOIP_SYNC: JsonValue = json!({
|
||||||
|
"device_one_time_keys_count": {},
|
||||||
|
"next_batch": "s526_47314_0_7_1_1_1_11444_1",
|
||||||
|
"device_lists": {
|
||||||
|
"changed": [
|
||||||
|
"@example:example.org"
|
||||||
|
],
|
||||||
|
"left": []
|
||||||
|
},
|
||||||
|
"rooms": {
|
||||||
|
"invite": {},
|
||||||
|
"join": {
|
||||||
|
"!SVkFJHzfwvuaIEawgC:localhost": {
|
||||||
|
"summary": {},
|
||||||
|
"account_data": {
|
||||||
|
"events": []
|
||||||
|
},
|
||||||
|
"ephemeral": {
|
||||||
|
"events": [ ]
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"events": []
|
||||||
|
},
|
||||||
|
"timeline": {
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"call_id": "12345",
|
||||||
|
"lifetime": 60000,
|
||||||
|
"offer": {
|
||||||
|
"sdp": "v=0\r\no=- 6584580628695956864 2 IN IP4 127.0.0.1[...]",
|
||||||
|
"type": "offer"
|
||||||
|
},
|
||||||
|
"version": 0
|
||||||
|
},
|
||||||
|
"event_id": "$143273582443PhrSn:example.org",
|
||||||
|
"origin_server_ts": 143273582,
|
||||||
|
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||||
|
"sender": "@example:example.org",
|
||||||
|
"type": "m.call.invite",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 1234
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"answer": {
|
||||||
|
"sdp": "v=0\r\no=- 6584580628695956864 2 IN IP4 127.0.0.1[...]",
|
||||||
|
"type": "answer"
|
||||||
|
},
|
||||||
|
"call_id": "12345",
|
||||||
|
"lifetime": 60000,
|
||||||
|
"version": 0
|
||||||
|
},
|
||||||
|
"event_id": "$143273582443PhrSn:example.org",
|
||||||
|
"origin_server_ts": 143273582,
|
||||||
|
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||||
|
"sender": "@example:example.org",
|
||||||
|
"type": "m.call.answer",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 1234
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"call_id": "12345",
|
||||||
|
"candidates": [
|
||||||
|
{
|
||||||
|
"candidate": "candidate:863018703 1 udp 2122260223 10.9.64.156 43670 typ host generation 0",
|
||||||
|
"sdpMLineIndex": 0,
|
||||||
|
"sdpMid": "audio"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version": 0
|
||||||
|
},
|
||||||
|
"event_id": "$143273582443PhrSn:example.org",
|
||||||
|
"origin_server_ts": 143273582,
|
||||||
|
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||||
|
"sender": "@example:example.org",
|
||||||
|
"type": "m.call.candidates",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 1234
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"call_id": "12345",
|
||||||
|
"version": 0
|
||||||
|
},
|
||||||
|
"event_id": "$143273582443PhrSn:example.org",
|
||||||
|
"origin_server_ts": 143273582,
|
||||||
|
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||||
|
"sender": "@example:example.org",
|
||||||
|
"type": "m.call.hangup",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 1234
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"limited": true,
|
||||||
|
"prev_batch": "t392-516_47314_0_7_1_1_1_11444_1"
|
||||||
|
},
|
||||||
|
"unread_notifications": {
|
||||||
|
"highlight_count": 0,
|
||||||
|
"notification_count": 11
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"leave": {}
|
||||||
|
},
|
||||||
|
"to_device": {
|
||||||
|
"events": []
|
||||||
|
},
|
||||||
|
"presence": {
|
||||||
|
"events": []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue