// Copyright 2021 Jonas Platte // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Types and traits related for event handlers. For usage, see //! [`Client::register_event_handler`]. //! //! ### How it works //! //! The `register_event_handler` method registers event handlers of different //! signatures by actually storing boxed closures that all have the same //! signature of `async (EventHandlerData) -> ()` where `EventHandlerData` is a //! private type that contains all of the data an event handler *might* need. //! //! The stored closure takes care of deserializing the event which the //! `EventHandlerData` contains as a (borrowed) [`serde_json::value::RawValue`], //! extracing the context arguments from other fields of `EventHandlerData` and //! calling / `.await`ing the event handler if the previous steps succeeded. //! It also logs any errors from the above chain of function calls. //! //! For more details, see the [`EventHandler`] trait. use std::{borrow::Cow, future::Future, ops::Deref}; use matrix_sdk_base::deserialized_responses::{EncryptionInfo, SyncRoomEvent}; use ruma::{events::AnySyncStateEvent, serde::Raw}; use serde::Deserialize; use serde_json::value::RawValue as RawJsonValue; use crate::{room, Client}; #[doc(hidden)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum EventKind { GlobalAccountData, RoomAccountData, EphemeralRoomData, Message { redacted: bool }, State { redacted: bool }, StrippedState, InitialState, ToDevice, Presence, } /// A statically-known event kind/type that can be retrieved from an event sync. pub trait SyncEvent { #[doc(hidden)] const ID: (EventKind, &'static str); } /// Interface for event handlers. /// /// This trait is an abstraction for a certain kind of functions / closures, /// specifically: /// /// * They must have at least one argument, which is the event itself, a type /// that implements [`SyncEvent`]. Any additional arguments need to implement /// the [`EventHandlerContext`] trait. /// * Their return type has to be one of: `()`, `Result<(), impl /// std::error::Error>` or `anyhow::Result<()>` (requires the `anyhow` Cargo /// feature to be enabled) /// /// ### How it works /// /// This trait is basically a very constrained version of `Fn`: It requires at /// least one argument, which is represented as its own generic parameter `Ev` /// with the remaining parameter types being represented by the second generic /// parameter `Ctx`; they have to be stuffed into one generic parameter as a /// tuple because Rust doesn't have variadic generics. /// /// `Ev` and `Ctx` are generic parameters rather than associated types because /// the argument list is a generic parameter for the `Fn` traits too, so a /// single type could implement `Fn` multiple times with different argument /// lists¹. Luckily, when calling [`Client::register_event_handler`] with a /// closure argument the trait solver takes into account that only a single one /// of the implementations applies (even though this could theoretically change /// through a dependency upgrade) and uses that rather than raising an ambiguity /// error. This is the same trick used by web frameworks like actix-web and /// axum. /// /// ¹ the only thing stopping such types from existing in stable Rust is that /// all manual implementations of the `Fn` traits require a Nightly feature pub trait EventHandler: Clone + Send + Sync + 'static { /// The future returned by `handle_event`. #[doc(hidden)] type Future: Future + Send + 'static; /// The event type being handled, for example a message event of type /// `m.room.message`. #[doc(hidden)] const ID: (EventKind, &'static str); /// Create a future for handling the given event. /// /// `data` provides additional data about the event, for example the room it /// appeared in. /// /// Returns `None` if one of the context extractors failed. #[doc(hidden)] fn handle_event(&self, ev: Ev, data: EventHandlerData<'_>) -> Option; } #[doc(hidden)] #[derive(Debug)] pub struct EventHandlerData<'a> { pub client: Client, pub room: Option, pub raw: &'a RawJsonValue, pub encryption_info: Option<&'a EncryptionInfo>, } /// Context for an event handler. /// /// This trait defines the set of types that may be used as additional arguments /// in event handler functions after the event itself. pub trait EventHandlerContext: Sized { #[doc(hidden)] fn from_data(_: &EventHandlerData<'_>) -> Option; } impl EventHandlerContext for Client { fn from_data(data: &EventHandlerData<'_>) -> Option { Some(data.client.clone()) } } /// This event handler context argument is only applicable to room-specific /// events. /// /// Trying to use it in the event handler for another event, for example a /// global account data or presence event, will result in the event handler /// being skipped and an error getting logged. impl EventHandlerContext for room::Room { fn from_data(data: &EventHandlerData<'_>) -> Option { data.room.clone() } } /// The raw JSON form of an event. /// /// Used as a context argument for event handlers (see /// [`Client::register_event_handler`]). // FIXME: This could be made to not own the raw JSON value with some changes to // the traits above, but only with GATs. #[derive(Clone, Debug)] pub struct RawEvent(pub Box); impl Deref for RawEvent { type Target = RawJsonValue; fn deref(&self) -> &Self::Target { &self.0 } } impl EventHandlerContext for RawEvent { fn from_data(data: &EventHandlerData<'_>) -> Option { Some(Self(data.raw.to_owned())) } } impl EventHandlerContext for Option { fn from_data(data: &EventHandlerData<'_>) -> Option { Some(data.encryption_info.cloned()) } } /// Return types supported for event handlers implement this trait. /// /// It is not meant to be implemented outside of matrix-sdk. pub trait EventHandlerResult: Sized { #[doc(hidden)] fn print_error(&self, event_type: &str); } impl EventHandlerResult for () { fn print_error(&self, _event_type: &str) {} } impl EventHandlerResult for Result<(), E> { fn print_error(&self, event_type: &str) { if let Err(e) = self { tracing::error!("Event handler for `{}` failed: {}", event_type, e); } } } #[cfg(feature = "anyhow")] impl EventHandlerResult for anyhow::Result<()> { fn print_error(&self, event_type: &str) { if let Err(e) = self { tracing::error!("Event handler for `{}` failed: {:?}", event_type, e); } } } #[derive(Deserialize)] struct UnsignedDetails { redacted_because: Option, } /// Event handling internals. impl Client { pub(crate) async fn handle_sync_events( &self, kind: EventKind, room: &Option, events: &[Raw], ) -> serde_json::Result<()> { #[derive(Deserialize)] struct ExtractType<'a> { #[serde(borrow, rename = "type")] event_type: Cow<'a, str>, } self.handle_sync_events_wrapped_with( room, events, |ev| (ev, None), |raw| Ok((kind, raw.deserialize_as::()?.event_type)), ) .await } pub(crate) async fn handle_sync_state_events( &self, room: &Option, state_events: &[Raw], ) -> serde_json::Result<()> { #[derive(Deserialize)] struct StateEventDetails<'a> { #[serde(borrow, rename = "type")] event_type: Cow<'a, str>, unsigned: Option, } self.handle_sync_events_wrapped_with( room, state_events, |ev| (ev, None), |raw| { let StateEventDetails { event_type, unsigned } = raw.deserialize_as()?; let redacted = unsigned.and_then(|u| u.redacted_because).is_some(); Ok((EventKind::State { redacted }, event_type)) }, ) .await } pub(crate) async fn handle_sync_timeline_events( &self, room: &Option, timeline_events: &[SyncRoomEvent], ) -> serde_json::Result<()> { #[derive(Deserialize)] struct TimelineEventDetails<'a> { #[serde(borrow, rename = "type")] event_type: Cow<'a, str>, state_key: Option, unsigned: Option, } self.handle_sync_events_wrapped_with( room, timeline_events, |e| (&e.event, e.encryption_info.as_ref()), |raw| { let TimelineEventDetails { event_type, state_key, unsigned } = raw.deserialize_as()?; let redacted = unsigned.and_then(|u| u.redacted_because).is_some(); let kind = match state_key { Some(_) => EventKind::State { redacted }, None => EventKind::Message { redacted }, }; Ok((kind, event_type)) }, ) .await } async fn handle_sync_events_wrapped_with<'a, T: 'a, U: 'a>( &self, room: &Option, list: &'a [U], get_event_details: impl Fn(&'a U) -> (&'a Raw, Option<&'a EncryptionInfo>), get_id: impl Fn(&Raw) -> serde_json::Result<(EventKind, Cow<'_, str>)>, ) -> serde_json::Result<()> { for x in list { let (raw_event, encryption_info) = get_event_details(x); let (ev_kind, ev_type) = get_id(raw_event)?; let event_handler_id = (ev_kind, &*ev_type); if let Some(handlers) = self.event_handlers.read().await.get(&event_handler_id) { for handler in &*handlers { let data = EventHandlerData { client: self.clone(), room: room.clone(), raw: raw_event.json(), encryption_info, }; matrix_sdk_common::executor::spawn((handler)(data)); } } } Ok(()) } } macro_rules! impl_event_handler { ($($ty:ident),* $(,)?) => { impl EventHandler for Fun where Ev: SyncEvent, Fun: Fn(Ev, $($ty),*) -> Fut + Clone + Send + Sync + 'static, Fut: Future + Send + 'static, Fut::Output: EventHandlerResult, $($ty: EventHandlerContext),* { type Future = Fut; const ID: (EventKind, &'static str) = Ev::ID; fn handle_event(&self, ev: Ev, _d: EventHandlerData<'_>) -> Option { Some((self)(ev, $($ty::from_data(&_d)?),*)) } } }; } impl_event_handler!(); impl_event_handler!(A); impl_event_handler!(A, B); impl_event_handler!(A, B, C); impl_event_handler!(A, B, C, D); impl_event_handler!(A, B, C, D, E); impl_event_handler!(A, B, C, D, E, F); impl_event_handler!(A, B, C, D, E, F, G); impl_event_handler!(A, B, C, D, E, F, G, H); mod static_events { use ruma::events::{ self, presence::{PresenceEvent, PresenceEventContent}, StaticEventContent, }; use super::{EventKind, SyncEvent}; impl SyncEvent for events::GlobalAccountDataEvent where C: StaticEventContent + events::GlobalAccountDataEventContent, { const ID: (EventKind, &'static str) = (EventKind::GlobalAccountData, C::TYPE); } impl SyncEvent for events::RoomAccountDataEvent where C: StaticEventContent + events::RoomAccountDataEventContent, { const ID: (EventKind, &'static str) = (EventKind::RoomAccountData, C::TYPE); } impl SyncEvent for events::SyncEphemeralRoomEvent where C: StaticEventContent + events::EphemeralRoomEventContent, { const ID: (EventKind, &'static str) = (EventKind::EphemeralRoomData, C::TYPE); } impl SyncEvent for events::SyncMessageEvent where C: StaticEventContent + events::MessageEventContent, { const ID: (EventKind, &'static str) = (EventKind::Message { redacted: false }, C::TYPE); } impl SyncEvent for events::SyncStateEvent where C: StaticEventContent + events::StateEventContent, { const ID: (EventKind, &'static str) = (EventKind::State { redacted: false }, C::TYPE); } impl SyncEvent for events::StrippedStateEvent where C: StaticEventContent + events::StateEventContent, { const ID: (EventKind, &'static str) = (EventKind::StrippedState, C::TYPE); } impl SyncEvent for events::InitialStateEvent where C: StaticEventContent + events::StateEventContent, { const ID: (EventKind, &'static str) = (EventKind::InitialState, C::TYPE); } impl SyncEvent for events::ToDeviceEvent where C: StaticEventContent + events::ToDeviceEventContent, { const ID: (EventKind, &'static str) = (EventKind::ToDevice, C::TYPE); } impl SyncEvent for PresenceEvent { const ID: (EventKind, &'static str) = (EventKind::Presence, PresenceEventContent::TYPE); } impl SyncEvent for events::RedactedSyncMessageEvent where C: StaticEventContent + events::RedactedMessageEventContent, { const ID: (EventKind, &'static str) = (EventKind::Message { redacted: true }, C::TYPE); } impl SyncEvent for events::RedactedSyncStateEvent where C: StaticEventContent + events::RedactedStateEventContent, { const ID: (EventKind, &'static str) = (EventKind::State { redacted: true }, C::TYPE); } impl SyncEvent for events::room::redaction::SyncRedactionEvent { const ID: (EventKind, &'static str) = ( EventKind::Message { redacted: false }, events::room::redaction::RedactionEventContent::TYPE, ); } } #[cfg(test)] mod test { use std::{future, sync::Arc}; use matrix_sdk_test::{EventBuilder, EventsJson}; use ruma::{ events::{room::member::MemberEventContent, StrippedStateEvent, SyncStateEvent}, room_id, }; use serde_json::json; use crate::{room, Client}; #[tokio::test] async fn event_handler() -> crate::Result<()> { use std::sync::atomic::{AtomicU8, Ordering::SeqCst}; let client = crate::client::test::logged_in_client().await; let member_count = Arc::new(AtomicU8::new(0)); let typing_count = Arc::new(AtomicU8::new(0)); let power_levels_count = Arc::new(AtomicU8::new(0)); let invited_member_count = Arc::new(AtomicU8::new(0)); client .register_event_handler({ let member_count = member_count.clone(); move |_ev: SyncStateEvent, _room: room::Room| { member_count.fetch_add(1, SeqCst); future::ready(()) } }) .await .register_event_handler({ let typing_count = typing_count.clone(); move |_ev: SyncStateEvent| { typing_count.fetch_add(1, SeqCst); future::ready(()) } }) .await .register_event_handler({ let power_levels_count = power_levels_count.clone(); move |_ev: SyncStateEvent, _client: Client, _room: room::Room| { power_levels_count.fetch_add(1, SeqCst); future::ready(()) } }) .await .register_event_handler({ let invited_member_count = invited_member_count.clone(); move |_ev: StrippedStateEvent| { invited_member_count.fetch_add(1, SeqCst); future::ready(()) } }) .await; let response = EventBuilder::default() .add_room_event(EventsJson::Member) .add_ephemeral(EventsJson::Typing) .add_state_event(EventsJson::PowerLevels) .add_custom_invited_event( &room_id!("!test_invited:example.org"), json!({ "content": { "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF", "displayname": "Alice", "membership": "invite", }, "event_id": "$143273582443PhrSn:example.org", "origin_server_ts": 1432735824653u64, "room_id": "!jEsUZKDJdhlrceRyVU:example.org", "sender": "@example:example.org", "state_key": "@alice:example.org", "type": "m.room.member", "unsigned": { "age": 1234, "invite_room_state": [ { "content": { "name": "Example Room" }, "sender": "@bob:example.org", "state_key": "", "type": "m.room.name" }, { "content": { "join_rule": "invite" }, "sender": "@bob:example.org", "state_key": "", "type": "m.room.join_rules" } ] } }), ) .build_sync_response(); client.process_sync(response).await?; assert_eq!(member_count.load(SeqCst), 1); assert_eq!(typing_count.load(SeqCst), 1); assert_eq!(power_levels_count.load(SeqCst), 1); assert_eq!(invited_member_count.load(SeqCst), 1); Ok(()) } }