430 lines
14 KiB
Rust
430 lines
14 KiB
Rust
//! `PUT /_matrix/app/*/transactions/{txnId}`
|
|
//!
|
|
//! Endpoint to push an event (or batch of events) to the application service.
|
|
|
|
pub mod v1 {
|
|
//! `/v1/` ([spec])
|
|
//!
|
|
//! [spec]: https://spec.matrix.org/latest/application-service-api/#put_matrixappv1transactionstxnid
|
|
|
|
#[cfg(any(feature = "unstable-msc2409", feature = "unstable-msc3202"))]
|
|
use std::collections::BTreeMap;
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
use std::{
|
|
collections::btree_map,
|
|
ops::{Deref, DerefMut},
|
|
};
|
|
|
|
#[cfg(any(feature = "unstable-msc2409", feature = "unstable-msc3202"))]
|
|
use js_int::UInt;
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
use ruma_common::events::AnyToDeviceEvent;
|
|
#[cfg(any(feature = "unstable-msc2409", feature = "unstable-msc3202"))]
|
|
use ruma_common::OwnedUserId;
|
|
use ruma_common::{
|
|
api::{request, response, Metadata},
|
|
events::AnyTimelineEvent,
|
|
metadata,
|
|
serde::Raw,
|
|
OwnedTransactionId,
|
|
};
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
use ruma_common::{
|
|
events::receipt::Receipt, presence::PresenceState, serde::from_raw_json_value,
|
|
OwnedEventId, OwnedRoomId,
|
|
};
|
|
#[cfg(feature = "unstable-msc3202")]
|
|
use ruma_common::{DeviceKeyAlgorithm, OwnedDeviceId};
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
use serde::Deserializer;
|
|
#[cfg(any(feature = "unstable-msc2409", feature = "unstable-msc3202"))]
|
|
use serde::{Deserialize, Serialize};
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
use serde_json::{value::RawValue as RawJsonValue, Value as JsonValue};
|
|
|
|
const METADATA: Metadata = metadata! {
|
|
method: PUT,
|
|
rate_limited: false,
|
|
authentication: AccessToken,
|
|
history: {
|
|
1.0 => "/_matrix/app/v1/transactions/:txn_id",
|
|
}
|
|
};
|
|
|
|
/// Request type for the `push_events` endpoint.
|
|
#[request]
|
|
pub struct Request {
|
|
/// The transaction ID for this set of events.
|
|
///
|
|
/// Homeservers generate these IDs and they are used to ensure idempotency of results.
|
|
#[ruma_api(path)]
|
|
pub txn_id: OwnedTransactionId,
|
|
|
|
/// A list of events.
|
|
pub events: Vec<Raw<AnyTimelineEvent>>,
|
|
|
|
/// Information on E2E device updates.
|
|
#[cfg(feature = "unstable-msc3202")]
|
|
#[serde(
|
|
default,
|
|
skip_serializing_if = "DeviceLists::is_empty",
|
|
rename = "org.matrix.msc3202.device_lists"
|
|
)]
|
|
pub device_lists: DeviceLists,
|
|
|
|
/// The number of unclaimed one-time keys currently held on the server for this device, for
|
|
/// each algorithm.
|
|
#[cfg(feature = "unstable-msc3202")]
|
|
#[serde(
|
|
default,
|
|
skip_serializing_if = "BTreeMap::is_empty",
|
|
rename = "org.matrix.msc3202.device_one_time_keys_count"
|
|
)]
|
|
pub device_one_time_keys_count:
|
|
BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceId, BTreeMap<DeviceKeyAlgorithm, UInt>>>,
|
|
|
|
/// A list of key algorithms for which the server has an unused fallback key for the
|
|
/// device.
|
|
#[cfg(feature = "unstable-msc3202")]
|
|
#[serde(
|
|
default,
|
|
skip_serializing_if = "BTreeMap::is_empty",
|
|
rename = "org.matrix.msc3202.device_unused_fallback_key_types"
|
|
)]
|
|
pub device_unused_fallback_key_types:
|
|
BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceId, Vec<DeviceKeyAlgorithm>>>,
|
|
|
|
/// A list of EDUs.
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
#[serde(
|
|
default,
|
|
skip_serializing_if = "<[_]>::is_empty",
|
|
rename = "de.sorunome.msc2409.ephemeral"
|
|
)]
|
|
pub ephemeral: Vec<Edu>,
|
|
|
|
/// A list of to-device messages.
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
#[serde(
|
|
default,
|
|
skip_serializing_if = "<[_]>::is_empty",
|
|
rename = "de.sorunome.msc2409.to_device"
|
|
)]
|
|
pub to_device: Vec<Raw<AnyToDeviceEvent>>,
|
|
}
|
|
|
|
/// Response type for the `push_events` endpoint.
|
|
#[response]
|
|
#[derive(Default)]
|
|
pub struct Response {}
|
|
|
|
impl Request {
|
|
/// Creates an `Request` with the given transaction ID and list of events.
|
|
pub fn new(txn_id: OwnedTransactionId, events: Vec<Raw<AnyTimelineEvent>>) -> Request {
|
|
Request {
|
|
txn_id,
|
|
events,
|
|
#[cfg(feature = "unstable-msc3202")]
|
|
device_lists: DeviceLists::new(),
|
|
#[cfg(feature = "unstable-msc3202")]
|
|
device_one_time_keys_count: BTreeMap::new(),
|
|
#[cfg(feature = "unstable-msc3202")]
|
|
device_unused_fallback_key_types: BTreeMap::new(),
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
ephemeral: Vec::new(),
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
to_device: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Response {
|
|
/// Creates an empty `Response`.
|
|
pub fn new() -> Self {
|
|
Self {}
|
|
}
|
|
}
|
|
|
|
/// Information on E2E device updates.
|
|
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
|
#[cfg(feature = "unstable-msc3202")]
|
|
pub struct DeviceLists {
|
|
/// List of users who have updated their device identity keys or who now
|
|
/// share an encrypted room with the client since the previous sync.
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
pub changed: Vec<OwnedUserId>,
|
|
|
|
/// List of users who no longer share encrypted rooms since the previous sync
|
|
/// response.
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
pub left: Vec<OwnedUserId>,
|
|
}
|
|
|
|
#[cfg(feature = "unstable-msc3202")]
|
|
impl DeviceLists {
|
|
/// Creates an empty `DeviceLists`.
|
|
pub fn new() -> Self {
|
|
Default::default()
|
|
}
|
|
|
|
/// Returns true if there are no device list updates.
|
|
pub fn is_empty(&self) -> bool {
|
|
self.changed.is_empty() && self.left.is_empty()
|
|
}
|
|
}
|
|
|
|
/// Type for passing ephemeral data to homeservers.
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
#[derive(Clone, Debug, Serialize)]
|
|
#[non_exhaustive]
|
|
pub enum Edu {
|
|
/// An EDU representing presence updates for users of the sending homeserver.
|
|
Presence(PresenceContent),
|
|
|
|
/// An EDU representing receipt updates for users of the sending homeserver.
|
|
Receipt(ReceiptContent),
|
|
|
|
/// A typing notification EDU for a user in a room.
|
|
Typing(TypingContent),
|
|
|
|
#[doc(hidden)]
|
|
#[serde(skip)]
|
|
_Custom(JsonValue),
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
struct EduDeHelper {
|
|
/// The message type field
|
|
r#type: String,
|
|
content: Box<RawJsonValue>,
|
|
}
|
|
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
impl<'de> Deserialize<'de> for Edu {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let json = Box::<RawJsonValue>::deserialize(deserializer)?;
|
|
let EduDeHelper { r#type, content } = from_raw_json_value(&json)?;
|
|
|
|
Ok(match r#type.as_ref() {
|
|
"m.presence" => Self::Presence(from_raw_json_value(&content)?),
|
|
"m.receipt" => Self::Receipt(from_raw_json_value(&content)?),
|
|
"m.typing" => Self::Typing(from_raw_json_value(&content)?),
|
|
_ => Self::_Custom(from_raw_json_value(&content)?),
|
|
})
|
|
}
|
|
}
|
|
|
|
/// The content for "m.presence" Edu.
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
|
pub struct PresenceContent {
|
|
/// A list of presence updates that the receiving server is likely to be interested in.
|
|
pub push: Vec<PresenceUpdate>,
|
|
}
|
|
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
impl PresenceContent {
|
|
/// Creates a new `PresenceContent`.
|
|
pub fn new(push: Vec<PresenceUpdate>) -> Self {
|
|
Self { push }
|
|
}
|
|
}
|
|
|
|
/// An update to the presence of a user.
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
|
pub struct PresenceUpdate {
|
|
/// The user ID this presence EDU is for.
|
|
pub user_id: OwnedUserId,
|
|
|
|
/// The presence of the user.
|
|
pub presence: PresenceState,
|
|
|
|
/// An optional description to accompany the presence.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub status_msg: Option<String>,
|
|
|
|
/// The number of milliseconds that have elapsed since the user last did something.
|
|
pub last_active_ago: UInt,
|
|
|
|
/// Whether or not the user is currently active.
|
|
///
|
|
/// Defaults to false.
|
|
#[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
|
|
pub currently_active: bool,
|
|
}
|
|
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
impl PresenceUpdate {
|
|
/// Creates a new `PresenceUpdate` with the given `user_id`, `presence` and `last_activity`.
|
|
pub fn new(user_id: OwnedUserId, presence: PresenceState, last_activity: UInt) -> Self {
|
|
Self {
|
|
user_id,
|
|
presence,
|
|
last_active_ago: last_activity,
|
|
status_msg: None,
|
|
currently_active: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The content for "m.receipt" Edu.
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
|
#[serde(transparent)]
|
|
pub struct ReceiptContent(pub BTreeMap<OwnedRoomId, ReceiptMap>);
|
|
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
impl ReceiptContent {
|
|
/// Creates a new `ReceiptContent`.
|
|
pub fn new(receipts: BTreeMap<OwnedRoomId, ReceiptMap>) -> Self {
|
|
Self(receipts)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
impl Deref for ReceiptContent {
|
|
type Target = BTreeMap<OwnedRoomId, ReceiptMap>;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
impl DerefMut for ReceiptContent {
|
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
&mut self.0
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
impl IntoIterator for ReceiptContent {
|
|
type Item = (OwnedRoomId, ReceiptMap);
|
|
type IntoIter = btree_map::IntoIter<OwnedRoomId, ReceiptMap>;
|
|
|
|
fn into_iter(self) -> Self::IntoIter {
|
|
self.0.into_iter()
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
impl FromIterator<(OwnedRoomId, ReceiptMap)> for ReceiptContent {
|
|
fn from_iter<T>(iter: T) -> Self
|
|
where
|
|
T: IntoIterator<Item = (OwnedRoomId, ReceiptMap)>,
|
|
{
|
|
Self(BTreeMap::from_iter(iter))
|
|
}
|
|
}
|
|
|
|
/// Mapping between user and `ReceiptData`.
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
|
pub struct ReceiptMap {
|
|
/// Read receipts for users in the room.
|
|
#[serde(rename = "m.read")]
|
|
pub read: BTreeMap<OwnedUserId, ReceiptData>,
|
|
}
|
|
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
impl ReceiptMap {
|
|
/// Creates a new `ReceiptMap`.
|
|
pub fn new(read: BTreeMap<OwnedUserId, ReceiptData>) -> Self {
|
|
Self { read }
|
|
}
|
|
}
|
|
|
|
/// Metadata about the event that was last read and when.
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
|
pub struct ReceiptData {
|
|
/// Metadata for the read receipt.
|
|
pub data: Receipt,
|
|
|
|
/// The extremity event ID the user has read up to.
|
|
pub event_ids: Vec<OwnedEventId>,
|
|
}
|
|
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
impl ReceiptData {
|
|
/// Creates a new `ReceiptData`.
|
|
pub fn new(data: Receipt, event_ids: Vec<OwnedEventId>) -> Self {
|
|
Self { data, event_ids }
|
|
}
|
|
}
|
|
|
|
/// The content for "m.typing" Edu.
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
|
pub struct TypingContent {
|
|
/// The room where the user's typing status has been updated.
|
|
pub room_id: OwnedRoomId,
|
|
|
|
/// The user ID that has had their typing status changed.
|
|
pub user_id: OwnedUserId,
|
|
|
|
/// Whether the user is typing in the room or not.
|
|
pub typing: bool,
|
|
}
|
|
|
|
#[cfg(feature = "unstable-msc2409")]
|
|
impl TypingContent {
|
|
/// Creates a new `TypingContent`.
|
|
pub fn new(room_id: OwnedRoomId, user_id: OwnedUserId, typing: bool) -> Self {
|
|
Self { room_id, user_id, typing }
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "server")]
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use ruma_common::api::{OutgoingRequest, SendAccessToken};
|
|
use serde_json::json;
|
|
|
|
use super::Request;
|
|
|
|
#[test]
|
|
fn decode_request_contains_events_field() {
|
|
let dummy_event = serde_json::from_value(json!({
|
|
"type": "m.room.message",
|
|
"event_id": "$143273582443PhrSn:example.com",
|
|
"origin_server_ts": 1,
|
|
"room_id": "!roomid:room.com",
|
|
"sender": "@user:example.com",
|
|
"content": {
|
|
"body": "test",
|
|
"msgtype": "m.text",
|
|
},
|
|
}))
|
|
.unwrap();
|
|
let events = vec![dummy_event];
|
|
|
|
let req = Request::new("any_txn_id".into(), events)
|
|
.try_into_http_request::<Vec<u8>>(
|
|
"https://homeserver.tld",
|
|
SendAccessToken::IfRequired("auth_tok"),
|
|
&[ruma_common::api::MatrixVersion::V1_1],
|
|
)
|
|
.unwrap();
|
|
let json_body: serde_json::Value = serde_json::from_slice(req.body()).unwrap();
|
|
|
|
assert_eq!(
|
|
1,
|
|
json_body.as_object().unwrap().get("events").unwrap().as_array().unwrap().len()
|
|
);
|
|
}
|
|
}
|
|
}
|