matrix-rust-sdk/crates/matrix-sdk/src/event_handler/mod.rs

858 lines
28 KiB
Rust

// Copyright 2021 Jonas Platte
// Copyright 2022 Famedly GmbH
//
// 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::add_event_handler`].
//!
//! ### How it works
//!
//! The `add_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`],
//! extracting 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.
#[cfg(any(feature = "anyhow", feature = "eyre"))]
use std::any::TypeId;
use std::{
borrow::Cow,
fmt,
future::Future,
pin::Pin,
sync::{
atomic::{AtomicU64, Ordering::SeqCst},
RwLock,
},
};
use anymap2::any::CloneAnySendSync;
use futures_util::stream::{FuturesUnordered, StreamExt};
use matrix_sdk_base::{
deserialized_responses::{EncryptionInfo, SyncTimelineEvent},
SendOutsideWasm, SyncOutsideWasm,
};
use ruma::{events::AnySyncStateEvent, push::Action, serde::Raw, OwnedRoomId};
use serde::{de::DeserializeOwned, Deserialize};
use serde_json::value::RawValue as RawJsonValue;
use tracing::{debug, error, field::debug, instrument, warn};
use self::maps::EventHandlerMaps;
use crate::{room, Client};
mod context;
mod maps;
mod static_events;
pub use self::context::{Ctx, EventHandlerContext, RawEvent};
#[cfg(not(target_arch = "wasm32"))]
type EventHandlerFut = Pin<Box<dyn Future<Output = ()> + Send>>;
#[cfg(target_arch = "wasm32")]
type EventHandlerFut = Pin<Box<dyn Future<Output = ()>>>;
#[cfg(not(target_arch = "wasm32"))]
type EventHandlerFn = dyn Fn(EventHandlerData<'_>) -> EventHandlerFut + Send + Sync;
#[cfg(target_arch = "wasm32")]
type EventHandlerFn = dyn Fn(EventHandlerData<'_>) -> EventHandlerFut;
type AnyMap = anymap2::Map<dyn CloneAnySendSync + Send + Sync>;
#[derive(Default)]
pub(crate) struct EventHandlerStore {
handlers: RwLock<EventHandlerMaps>,
context: RwLock<AnyMap>,
counter: AtomicU64,
}
impl EventHandlerStore {
pub fn add_handler(&self, handle: EventHandlerHandle, handler_fn: Box<EventHandlerFn>) {
self.handlers.write().unwrap().add(handle, handler_fn);
}
pub fn add_context<T>(&self, ctx: T)
where
T: Clone + Send + Sync + 'static,
{
self.context.write().unwrap().insert(ctx);
}
pub fn remove(&self, handle: EventHandlerHandle) {
self.handlers.write().unwrap().remove(handle);
}
#[cfg(test)]
fn len(&self) -> usize {
self.handlers.read().unwrap().len()
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum HandlerKind {
GlobalAccountData,
RoomAccountData,
EphemeralRoomData,
Timeline,
MessageLike,
OriginalMessageLike,
RedactedMessageLike,
State,
OriginalState,
RedactedState,
StrippedState,
ToDevice,
Presence,
}
impl HandlerKind {
fn message_like_redacted(redacted: bool) -> Self {
if redacted {
Self::RedactedMessageLike
} else {
Self::OriginalMessageLike
}
}
fn state_redacted(redacted: bool) -> Self {
if redacted {
Self::RedactedState
} else {
Self::OriginalState
}
}
}
/// A statically-known event kind/type that can be retrieved from an event sync.
pub trait SyncEvent {
#[doc(hidden)]
const KIND: HandlerKind;
#[doc(hidden)]
const TYPE: Option<&'static str>;
}
pub(crate) struct EventHandlerWrapper {
handler_fn: Box<EventHandlerFn>,
pub handler_id: u64,
}
/// Handle to remove a registered event handler by passing it to
/// [`Client::remove_event_handler`].
#[derive(Clone, Debug)]
pub struct EventHandlerHandle {
pub(crate) ev_kind: HandlerKind,
pub(crate) ev_type: Option<&'static str>,
pub(crate) room_id: Option<OwnedRoomId>,
pub(crate) handler_id: u64,
}
/// 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 Display + Debug
/// + 'static>` (if you are using `anyhow::Result` or `eyre::Result` you can
/// additionally enable the `anyhow` / `eyre` feature to get the verbose
/// `Debug` output printed on error)
///
/// ### 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::add_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<Ev, Ctx>: SendOutsideWasm + SyncOutsideWasm + 'static {
/// The future returned by `handle_event`.
#[doc(hidden)]
type Future: EventHandlerFuture;
/// 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<Self::Future>;
}
#[doc(hidden)]
pub trait EventHandlerFuture:
Future<Output = <Self as EventHandlerFuture>::Output> + SendOutsideWasm + 'static
{
type Output: EventHandlerResult;
}
impl<T> EventHandlerFuture for T
where
T: Future + SendOutsideWasm + 'static,
<T as Future>::Output: EventHandlerResult,
{
type Output = <T as Future>::Output;
}
#[doc(hidden)]
#[derive(Debug)]
pub struct EventHandlerData<'a> {
client: Client,
room: Option<room::Room>,
raw: &'a RawJsonValue,
encryption_info: Option<&'a EncryptionInfo>,
push_actions: &'a [Action],
handle: EventHandlerHandle,
}
/// 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: Option<&str>);
}
impl EventHandlerResult for () {
fn print_error(&self, _event_type: Option<&str>) {}
}
impl<E: fmt::Debug + fmt::Display + 'static> EventHandlerResult for Result<(), E> {
fn print_error(&self, event_type: Option<&str>) {
let msg_fragment = match event_type {
Some(event_type) => format!(" for `{event_type}`"),
None => "".to_owned(),
};
match self {
#[cfg(feature = "anyhow")]
Err(e) if TypeId::of::<E>() == TypeId::of::<anyhow::Error>() => {
error!("Event handler{msg_fragment} failed: {e:?}");
}
#[cfg(feature = "eyre")]
Err(e) if TypeId::of::<E>() == TypeId::of::<eyre::Report>() => {
error!("Event handler{msg_fragment} failed: {e:?}");
}
Err(e) => {
error!("Event handler{msg_fragment} failed: {e}");
}
Ok(_) => {}
}
}
}
#[derive(Deserialize)]
struct UnsignedDetails {
redacted_because: Option<serde::de::IgnoredAny>,
}
/// Event handling internals.
impl Client {
pub(crate) fn add_event_handler_impl<Ev, Ctx, H>(
&self,
handler: H,
room_id: Option<OwnedRoomId>,
) -> EventHandlerHandle
where
Ev: SyncEvent + DeserializeOwned + Send + 'static,
H: EventHandler<Ev, Ctx>,
{
let handler_fn: Box<EventHandlerFn> = Box::new(move |data| {
let maybe_fut =
serde_json::from_str(data.raw.get()).map(|ev| handler.handle_event(ev, data));
Box::pin(async move {
match maybe_fut {
Ok(Some(fut)) => {
fut.await.print_error(Ev::TYPE);
}
Ok(None) => {
error!(
event_type = Ev::TYPE, event_kind = ?Ev::KIND,
"Event handler has an invalid context argument",
);
}
Err(e) => {
warn!(
event_type = Ev::TYPE, event_kind = ?Ev::KIND,
"Failed to deserialize event, skipping event handler.\n
Deserialization error: {e}",
);
}
}
})
});
let handler_id = self.inner.event_handlers.counter.fetch_add(1, SeqCst);
let handle =
EventHandlerHandle { ev_kind: Ev::KIND, ev_type: Ev::TYPE, room_id, handler_id };
self.inner.event_handlers.add_handler(handle.clone(), handler_fn);
handle
}
pub(crate) async fn handle_sync_events<T>(
&self,
kind: HandlerKind,
room: Option<&room::Room>,
events: &[Raw<T>],
) -> serde_json::Result<()> {
#[derive(Deserialize)]
struct ExtractType<'a> {
#[serde(borrow, rename = "type")]
event_type: Cow<'a, str>,
}
for raw_event in events {
let event_type = raw_event.deserialize_as::<ExtractType<'_>>()?.event_type;
self.call_event_handlers(room, raw_event.json(), kind, &event_type, None, &[]).await;
}
Ok(())
}
pub(crate) async fn handle_sync_state_events(
&self,
room: Option<&room::Room>,
state_events: &[Raw<AnySyncStateEvent>],
) -> serde_json::Result<()> {
#[derive(Deserialize)]
struct StateEventDetails<'a> {
#[serde(borrow, rename = "type")]
event_type: Cow<'a, str>,
unsigned: Option<UnsignedDetails>,
}
// Event handlers for possibly-redacted state events
self.handle_sync_events(HandlerKind::State, room, state_events).await?;
// Event handlers specifically for redacted OR unredacted state events
for raw_event in state_events {
let StateEventDetails { event_type, unsigned } = raw_event.deserialize_as()?;
let redacted = unsigned.and_then(|u| u.redacted_because).is_some();
let handler_kind = HandlerKind::state_redacted(redacted);
self.call_event_handlers(room, raw_event.json(), handler_kind, &event_type, None, &[])
.await;
}
Ok(())
}
pub(crate) async fn handle_sync_timeline_events(
&self,
room: Option<&room::Room>,
timeline_events: &[SyncTimelineEvent],
) -> serde_json::Result<()> {
#[derive(Deserialize)]
struct TimelineEventDetails<'a> {
#[serde(borrow, rename = "type")]
event_type: Cow<'a, str>,
state_key: Option<serde::de::IgnoredAny>,
unsigned: Option<UnsignedDetails>,
}
for item in timeline_events {
let TimelineEventDetails { event_type, state_key, unsigned } =
item.event.deserialize_as()?;
let redacted = unsigned.and_then(|u| u.redacted_because).is_some();
let (handler_kind_g, handler_kind_r) = match state_key {
Some(_) => (HandlerKind::State, HandlerKind::state_redacted(redacted)),
None => (HandlerKind::MessageLike, HandlerKind::message_like_redacted(redacted)),
};
let raw_event = item.event.json();
let encryption_info = item.encryption_info.as_ref();
let push_actions = &item.push_actions;
// Event handlers for possibly-redacted timeline events
self.call_event_handlers(
room,
raw_event,
handler_kind_g,
&event_type,
encryption_info,
push_actions,
)
.await;
// Event handlers specifically for redacted OR unredacted timeline events
self.call_event_handlers(
room,
raw_event,
handler_kind_r,
&event_type,
encryption_info,
push_actions,
)
.await;
// Event handlers for `AnySyncTimelineEvent`
let kind = HandlerKind::Timeline;
self.call_event_handlers(
room,
raw_event,
kind,
&event_type,
encryption_info,
push_actions,
)
.await;
}
Ok(())
}
#[instrument(skip_all, fields(?event_kind, ?event_type, room_id))]
async fn call_event_handlers(
&self,
room: Option<&room::Room>,
raw: &RawJsonValue,
event_kind: HandlerKind,
event_type: &str,
encryption_info: Option<&EncryptionInfo>,
push_actions: &[Action],
) {
let room_id = room.map(|r| r.room_id());
if let Some(room_id) = room_id {
tracing::Span::current().record("room_id", debug(room_id));
}
// Construct event handler futures
let mut futures: FuturesUnordered<_> = self
.inner
.event_handlers
.handlers
.read()
.unwrap()
.get_handlers(event_kind, event_type, room_id)
.map(|(handle, handler_fn)| {
let data = EventHandlerData {
client: self.clone(),
room: room.cloned(),
raw,
encryption_info,
push_actions,
handle,
};
(handler_fn)(data)
})
.collect();
if !futures.is_empty() {
debug!(amount = futures.len(), "Calling event handlers");
// Run the event handler futures with the `self.event_handlers.handlers`
// lock no longer being held.
while let Some(()) = futures.next().await {}
}
}
}
/// A guard type that removes an event handler when it drops (goes out of
/// scope).
///
/// Created with [`Client::event_handler_drop_guard`].
#[derive(Debug)]
pub struct EventHandlerDropGuard {
handle: EventHandlerHandle,
client: Client,
}
impl EventHandlerDropGuard {
pub(crate) fn new(handle: EventHandlerHandle, client: Client) -> Self {
Self { handle, client }
}
}
impl Drop for EventHandlerDropGuard {
fn drop(&mut self) {
self.client.remove_event_handler(self.handle.clone());
}
}
macro_rules! impl_event_handler {
($($ty:ident),* $(,)?) => {
impl<Ev, Fun, Fut, $($ty),*> EventHandler<Ev, ($($ty,)*)> for Fun
where
Ev: SyncEvent,
Fun: Fn(Ev, $($ty),*) -> Fut + SendOutsideWasm + SyncOutsideWasm + 'static,
Fut: EventHandlerFuture,
$($ty: EventHandlerContext),*
{
type Future = Fut;
fn handle_event(&self, ev: Ev, _d: EventHandlerData<'_>) -> Option<Self::Future> {
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);
#[cfg(test)]
mod tests {
use matrix_sdk_test::{
async_test, test_json::DEFAULT_SYNC_ROOM_ID, InvitedRoomBuilder, JoinedRoomBuilder,
};
#[cfg(target_arch = "wasm32")]
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
use std::{
future,
sync::{
atomic::{AtomicU8, Ordering::SeqCst},
Arc,
},
};
use matrix_sdk_test::{
EphemeralTestEvent, EventBuilder, StateTestEvent, StrippedStateTestEvent, TimelineTestEvent,
};
use ruma::{
events::{
room::{
member::{OriginalSyncRoomMemberEvent, StrippedRoomMemberEvent},
name::OriginalSyncRoomNameEvent,
power_levels::OriginalSyncRoomPowerLevelsEvent,
},
typing::SyncTypingEvent,
AnySyncStateEvent,
},
room_id,
serde::Raw,
};
use serde_json::json;
use crate::{
event_handler::Ctx,
room::Room,
test_utils::{logged_in_client, no_retry_test_client},
Client,
};
#[async_test]
async fn add_event_handler() -> crate::Result<()> {
let client = logged_in_client(None).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.add_event_handler({
let member_count = member_count.clone();
move |_ev: OriginalSyncRoomMemberEvent, _room: Room| {
member_count.fetch_add(1, SeqCst);
future::ready(())
}
});
client.add_event_handler({
let typing_count = typing_count.clone();
move |_ev: SyncTypingEvent| {
typing_count.fetch_add(1, SeqCst);
future::ready(())
}
});
client.add_event_handler({
let power_levels_count = power_levels_count.clone();
move |_ev: OriginalSyncRoomPowerLevelsEvent, _client: Client, _room: Room| {
power_levels_count.fetch_add(1, SeqCst);
future::ready(())
}
});
client.add_event_handler({
let invited_member_count = invited_member_count.clone();
move |_ev: StrippedRoomMemberEvent| {
invited_member_count.fetch_add(1, SeqCst);
future::ready(())
}
});
let response = EventBuilder::default()
.add_joined_room(
JoinedRoomBuilder::default()
.add_timeline_event(TimelineTestEvent::Member)
.add_ephemeral_event(EphemeralTestEvent::Typing)
.add_state_event(StateTestEvent::PowerLevels),
)
.add_invited_room(
InvitedRoomBuilder::new(room_id!("!test_invited:example.org")).add_state_event(
StrippedStateTestEvent::Custom(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(())
}
#[async_test]
async fn add_room_event_handler() -> crate::Result<()> {
let client = logged_in_client(None).await;
let room_id_a = room_id!("!foo:example.org");
let room_id_b = room_id!("!bar:matrix.org");
let member_count = Arc::new(AtomicU8::new(0));
let power_levels_count = Arc::new(AtomicU8::new(0));
// Room event handlers for member events in both rooms
client.add_room_event_handler(room_id_a, {
let member_count = member_count.clone();
move |_ev: OriginalSyncRoomMemberEvent, _room: Room| {
member_count.fetch_add(1, SeqCst);
future::ready(())
}
});
client.add_room_event_handler(room_id_b, {
let member_count = member_count.clone();
move |_ev: OriginalSyncRoomMemberEvent, _room: Room| {
member_count.fetch_add(1, SeqCst);
future::ready(())
}
});
// Power levels event handlers for member events in room A
client.add_room_event_handler(room_id_a, {
let power_levels_count = power_levels_count.clone();
move |_ev: OriginalSyncRoomPowerLevelsEvent, _client: Client, _room: Room| {
power_levels_count.fetch_add(1, SeqCst);
future::ready(())
}
});
// Room name event handler for room name events in room B
client.add_room_event_handler(room_id_b, move |_ev: OriginalSyncRoomNameEvent| async {
unreachable!("No room event in room B")
});
let response = EventBuilder::default()
.add_joined_room(
JoinedRoomBuilder::new(room_id_a)
.add_timeline_event(TimelineTestEvent::Member)
.add_state_event(StateTestEvent::PowerLevels)
.add_state_event(StateTestEvent::RoomName),
)
.add_joined_room(
JoinedRoomBuilder::new(room_id_b)
.add_timeline_event(TimelineTestEvent::Member)
.add_state_event(StateTestEvent::PowerLevels),
)
.build_sync_response();
client.process_sync(response).await?;
assert_eq!(member_count.load(SeqCst), 2);
assert_eq!(power_levels_count.load(SeqCst), 1);
Ok(())
}
#[async_test]
async fn remove_event_handler() -> crate::Result<()> {
let client = logged_in_client(None).await;
let member_count = Arc::new(AtomicU8::new(0));
client.add_event_handler({
let member_count = member_count.clone();
move |_ev: OriginalSyncRoomMemberEvent| {
member_count.fetch_add(1, SeqCst);
future::ready(())
}
});
let handle_a = client.add_event_handler(move |_ev: OriginalSyncRoomMemberEvent| async {
panic!("handler should have been removed");
});
let handle_b = client.add_room_event_handler(
#[allow(unknown_lints, clippy::explicit_auto_deref)] // lint is buggy
*DEFAULT_SYNC_ROOM_ID,
move |_ev: OriginalSyncRoomMemberEvent| async {
panic!("handler should have been removed");
},
);
client.add_event_handler({
let member_count = member_count.clone();
move |_ev: OriginalSyncRoomMemberEvent| {
member_count.fetch_add(1, SeqCst);
future::ready(())
}
});
let response = EventBuilder::default()
.add_joined_room(
JoinedRoomBuilder::default().add_timeline_event(TimelineTestEvent::Member),
)
.build_sync_response();
client.remove_event_handler(handle_a);
client.remove_event_handler(handle_b);
client.process_sync(response).await?;
assert_eq!(member_count.load(SeqCst), 2);
Ok(())
}
#[async_test]
async fn event_handler_drop_guard() {
let client = no_retry_test_client(None).await;
let handle = client.add_event_handler(|_ev: OriginalSyncRoomMemberEvent| async {});
assert_eq!(client.inner.event_handlers.len(), 1);
{
let _guard = client.event_handler_drop_guard(handle);
assert_eq!(client.inner.event_handlers.len(), 1);
// guard dropped here
}
assert_eq!(client.inner.event_handlers.len(), 0);
}
#[async_test]
async fn use_client_in_handler() {
// This used to not work because we were requiring `Send` of event
// handler futures even on WASM, where practically all futures that do
// I/O aren't.
let client = no_retry_test_client(None).await;
client.add_event_handler(|_ev: OriginalSyncRoomMemberEvent, client: Client| async move {
// All of Client's async methods that do network requests (and
// possibly some that don't) are `!Send` on wasm. We obviously want
// to be able to use them in event handlers.
let _caps = client.get_capabilities().await?;
anyhow::Ok(())
});
}
#[async_test]
async fn raw_event_handler() -> crate::Result<()> {
let client = logged_in_client(None).await;
let counter = Arc::new(AtomicU8::new(0));
client.add_event_handler_context(counter.clone());
client.add_event_handler(
|_ev: Raw<OriginalSyncRoomMemberEvent>, counter: Ctx<Arc<AtomicU8>>| async move {
counter.fetch_add(1, SeqCst);
},
);
let response = EventBuilder::default()
.add_joined_room(
JoinedRoomBuilder::default().add_timeline_event(TimelineTestEvent::Member),
)
.build_sync_response();
client.process_sync(response).await?;
assert_eq!(counter.load(SeqCst), 1);
Ok(())
}
#[async_test]
async fn enum_event_handler() -> crate::Result<()> {
let client = logged_in_client(None).await;
let counter = Arc::new(AtomicU8::new(0));
client.add_event_handler_context(counter.clone());
client.add_event_handler(
|_ev: AnySyncStateEvent, counter: Ctx<Arc<AtomicU8>>| async move {
counter.fetch_add(1, SeqCst);
},
);
let response = EventBuilder::default()
.add_joined_room(
JoinedRoomBuilder::default().add_timeline_event(TimelineTestEvent::Member),
)
.build_sync_response();
client.process_sync(response).await?;
assert_eq!(counter.load(SeqCst), 1);
Ok(())
}
}