fix(timeline): validate beacon_info content for session matching

This commit is contained in:
ganfra
2026-03-23 20:37:03 +01:00
committed by Ivan Enderlin
parent cad9c42a7a
commit e335cac8f7
6 changed files with 276 additions and 74 deletions
@@ -57,6 +57,7 @@ use super::{ObservableItemsTransaction, rfind_event_by_item_id};
use crate::timeline::{
BeaconInfo, EventTimelineItem, LiveLocationState, MsgLikeContent, MsgLikeKind, PollState,
ReactionInfo, ReactionStatus, TimelineEventItemId, TimelineItem, TimelineItemContent,
event_item::beacon_info_matches,
};
#[derive(Clone)]
@@ -425,17 +426,10 @@ pub(crate) struct Aggregations {
/// A pending beacon-stop aggregation received before the corresponding live
/// `beacon_info` start item has arrived.
///
/// Keyed by the sender's user ID (the state key of both the start and stop
/// `beacon_info` events). At most one stop can be pending per sender at any
/// time: `beacon_info` is a state event whose state key is the sender's
/// user ID, so there is only ever one live session per sender, and
/// therefore only one meaningful pending stop. A later stop simply
/// replaces an earlier one.
///
/// When the live start item is eventually inserted via `add_item`, the
/// entry here is promoted into [`Self::related_events`] under the
/// now-known start event ID so that [`Self::apply_all`] can apply the
/// stop immediately.
/// Keyed by the sender's user ID. When a live start item is eventually
/// inserted via `add_item`, we check if the pending stop matches and
/// promote it into [`Self::related_events`] so that [`Self::apply_all`]
/// can apply it immediately.
pending_beacon_stops: HashMap<OwnedUserId, Aggregation>,
}
@@ -452,22 +446,42 @@ impl Aggregations {
/// [`Self::related_events`] (and thus picked up by [`Self::apply_all`])
/// when the live item is inserted via
/// [`Self::promote_pending_beacon_stop`].
///
/// If a stop was already stashed for this sender, the new one replaces it:
/// the later state event is always authoritative.
pub fn add_pending_beacon_stop(&mut self, sender: OwnedUserId, aggregation: Aggregation) {
self.pending_beacon_stops.insert(sender, aggregation);
}
/// Promote any stashed beacon-stop aggregations for `sender` into the
/// Promote a matching stashed beacon-stop aggregation for `sender` into the
/// regular aggregation map, now that the live start item's
/// `target_event_id` is known.
///
/// The pending stop's content must match the start event's content (except
/// for the `live` field) for promotion to occur. If they don't match, the
/// pending stop is discarded because it belongs to a different session.
///
/// Should be called from `add_item` just before `apply_all`, when inserting
/// a live `beacon_info` item.
fn promote_pending_beacon_stop(&mut self, sender: &OwnedUserId, target_event_id: OwnedEventId) {
fn promote_pending_beacon_stop(
&mut self,
sender: &OwnedUserId,
target_event_id: OwnedEventId,
start_content: &BeaconInfoEventContent,
) {
if !start_content.live {
return;
}
let Some(stop) = self.pending_beacon_stops.remove(sender) else { return };
let AggregationKind::BeaconStop { content: stop_content } = &stop.kind else {
warn!("pending beacon stop has unexpected aggregation kind");
return;
};
if !beacon_info_matches(start_content, stop_content) {
trace!("discarding stale pending beacon stop (content mismatch)");
return;
}
let target = TimelineEventItemId::EventId(target_event_id);
self.add(target, stop);
}
@@ -620,8 +634,14 @@ impl Aggregations {
// in `pending_beacon_stops` keyed by sender. Promote it into
// `related_events` under the now-known start event ID so the loop below
// applies it together with any other pending aggregations.
if let TimelineEventItemId::EventId(event_id) = item_id {
self.promote_pending_beacon_stop(sender, event_id.clone());
//
// The promotion verifies that the pending stop's content matches the
// start event's content to ensure we don't apply an old stop to a new
// session.
if let TimelineEventItemId::EventId(event_id) = item_id
&& let Some(live_location) = event.content().as_live_location_state()
{
self.promote_pending_beacon_stop(sender, event_id.clone(), &live_location.beacon_info);
}
let Some(aggregations) = self.related_events.get(item_id) else {
@@ -338,6 +338,8 @@ impl TimelineAction {
// existing live item from the same sender rather than creating a
// new timeline item.
Self::HandleAggregation {
// There is no explicit relates_to on a beacon_info state event;
// the target is identified by sender in handle_beacon_stop.
related_event: ev.event_id,
kind: HandleAggregationKind::BeaconStop { content: ev.content },
}
@@ -772,27 +774,30 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
/// Handle a stop `beacon_info` state event by finding the existing live
/// `LiveLocation` timeline item from the same sender and updating it via
/// the aggregation system.
///
/// The stop event's content must match the start item's content (except for
/// the `live` field) to ensure we apply the stop to the correct session.
#[instrument(skip(self, content))]
fn handle_beacon_stop(&mut self, content: BeaconInfoEventContent) {
let sender = &self.ctx.sender;
// Find the live start item by sender and matching content.
let target_event_id = super::algorithms::rfind_event_item(self.items, |item| {
item.sender() == sender
&& item.content().as_live_location_state().is_some_and(|s| s.matches_stop(&content))
})
.and_then(|(_, event_item)| event_item.inner.event_id().map(ToOwned::to_owned));
let aggregation = Aggregation::new(
self.ctx.flow.timeline_item_id(),
AggregationKind::BeaconStop { content },
);
// The stop beacon_info has no explicit `relates_to`; find the target
// live item by matching sender and liveness, then extract its event ID
// so we can address the aggregation correctly.
let Some(target_event_id) = super::algorithms::rfind_event_item(self.items, |item| {
item.sender() == sender
&& item.content().as_live_location_state().is_some_and(|s| s.is_live())
})
.and_then(|(_, event_item)| event_item.inner.event_id().map(ToOwned::to_owned)) else {
// The live start item hasn't arrived yet. Stash the stop so it can
// be applied when the start item is eventually inserted.
let Some(target_event_id) = target_event_id else {
// The live start item hasn't arrived yet (or the content doesn't match).
// Stash the stop so it can be applied when the matching start item arrives.
trace!(
"no live beacon_info item found for {sender}; \
"no matching live beacon_info item found for {sender}; \
stashing stop event to apply when the start item arrives"
);
self.meta.aggregations.add_pending_beacon_stop(sender.clone(), aggregation);
@@ -175,4 +175,21 @@ impl LiveLocationState {
assert!(!beacon_info.is_live(), "A stop `beacon_info` event must not be live.");
self.beacon_info = beacon_info;
}
/// Check if a stop `beacon_info` matches this session.
///
/// Returns `true` if all fields except `live` match and this session is
/// still live. This is used to verify that a stop event belongs to the
/// same session as this start event.
pub(in crate::timeline) fn matches_stop(&self, stop: &BeaconInfoEventContent) -> bool {
self.beacon_info.live && beacon_info_matches(&self.beacon_info, stop)
}
}
/// Check if two `BeaconInfoEventContent` values belong to the same session.
///
/// Compares all fields except `live`, which differs between start and stop
/// events. Returns `true` if all other fields match.
pub fn beacon_info_matches(a: &BeaconInfoEventContent, b: &BeaconInfoEventContent) -> bool {
a.ts == b.ts && a.timeout == b.timeout && a.description == b.description && a.asset == b.asset
}
@@ -62,8 +62,11 @@ mod reply;
pub use pinned_events::RoomPinnedEventsChange;
pub(in crate::timeline) use self::message::{
extract_bundled_edit_event_json, extract_poll_edit_content, extract_room_msg_edit_content,
pub(in crate::timeline) use self::{
live_location::beacon_info_matches,
message::{
extract_bundled_edit_event_json, extract_poll_edit_content, extract_room_msg_edit_content,
},
};
pub use self::{
live_location::{BeaconInfo, LiveLocationState},
@@ -49,7 +49,8 @@ pub use self::{
};
pub(super) use self::{
content::{
extract_bundled_edit_event_json, extract_poll_edit_content, extract_room_msg_edit_content,
beacon_info_matches, extract_bundled_edit_event_json, extract_poll_edit_content,
extract_room_msg_edit_content,
},
local::LocalEventTimelineItem,
remote::{RemoteEventOrigin, RemoteEventTimelineItem},
@@ -21,12 +21,14 @@ use eyeball_im::VectorDiff;
use matrix_sdk_test::{ALICE, BOB, async_test};
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, event_id,
events::beacon_info::RedactedBeaconInfoEventContent, owned_event_id, uint,
events::beacon_info::{BeaconInfoEventContent, RedactedBeaconInfoEventContent},
owned_event_id, uint,
};
use stream_assert::{assert_next_matches, assert_pending};
use crate::timeline::{
EventTimelineItem, ReactionStatus, TimelineEventItemId, tests::TestTimeline,
EventTimelineItem, ReactionStatus, TimelineEventItemId, event_item::beacon_info_matches,
tests::TestTimeline,
};
/// A `beacon_info` state event creates a `MsgLikeKind::LiveLocation`
@@ -44,6 +46,7 @@ async fn test_beacon_info_creates_timeline_item() {
Some("Alice's walk".to_owned()),
Duration::from_secs(3600),
true,
None,
)
.await;
@@ -70,16 +73,7 @@ async fn test_beacon_info_with_expired_timeout_still_creates_item() {
let past_ts = MilliSecondsSinceUnixEpoch(uint!(1_000)); // Very early timestamp
let short_duration = Duration::from_millis(1); // Effectively expired immediately
timeline
.send_beacon_info_with_ts(
&ALICE,
beacon_id,
None,
short_duration,
true,
Some(past_ts),
)
.await;
timeline.send_beacon_info(&ALICE, beacon_id, None, short_duration, true, Some(past_ts)).await;
// The item should still be created even though the timeout has expired.
let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
@@ -101,7 +95,7 @@ async fn test_beacon_info_stopped_without_start_produces_no_item() {
let mut stream = timeline.subscribe_events().await;
let beacon_id = event_id!("$beacon_stop:example.org");
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(60), false).await;
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(60), false, None).await;
assert_pending!(stream);
@@ -119,7 +113,7 @@ async fn test_beacon_update_aggregates_onto_beacon_info() {
let mut stream = timeline.subscribe_events().await;
let beacon_id = event_id!("$beacon_info:example.org");
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true).await;
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true, None).await;
let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
let state = item.content().as_live_location_state().unwrap();
@@ -152,7 +146,7 @@ async fn test_multiple_beacon_updates_accumulate_in_order() {
let mut stream = timeline.subscribe_events().await;
let beacon_id = event_id!("$beacon_info:example.org");
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true).await;
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true, None).await;
let _item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
// Send in non-chronological order.
@@ -204,7 +198,9 @@ async fn test_beacon_update_before_beacon_info_is_applied_when_parent_arrives()
);
// Now the beacon_info arrives.
timeline.send_beacon_info(&ALICE, &beacon_id, None, Duration::from_secs(3600), true).await;
timeline
.send_beacon_info(&ALICE, &beacon_id, None, Duration::from_secs(3600), true, None)
.await;
// A single PushBack with the stashed location already applied.
let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
@@ -232,6 +228,7 @@ async fn test_multiple_users_sharing_produce_independent_items() {
Some("Alice".to_owned()),
Duration::from_secs(3600),
true,
None,
)
.await;
@@ -246,6 +243,7 @@ async fn test_multiple_users_sharing_produce_independent_items() {
Some("Bob".to_owned()),
Duration::from_secs(3600),
true,
None,
)
.await;
@@ -318,7 +316,12 @@ async fn test_beacon_stop_updates_existing_item() {
let start_id = event_id!("$beacon_start:example.org");
let stop_id = event_id!("$beacon_stop:example.org");
timeline.send_beacon_info(&ALICE, start_id, None, Duration::from_secs(3600), true).await;
// Both start and stop use the same session timestamp.
let session_ts = MilliSecondsSinceUnixEpoch::now();
timeline
.send_beacon_info(&ALICE, start_id, None, Duration::from_secs(3600), true, Some(session_ts))
.await;
let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
assert!(item.content().as_live_location_state().unwrap().is_live());
@@ -326,7 +329,9 @@ async fn test_beacon_stop_updates_existing_item() {
assert_pending!(stream);
timeline.send_beacon_info(&ALICE, stop_id, None, Duration::from_secs(3600), false).await;
timeline
.send_beacon_info(&ALICE, stop_id, None, Duration::from_secs(3600), false, Some(session_ts))
.await;
// The existing item is updated — a Set diff, not a PushBack.
let item = assert_next_matches!(stream, VectorDiff::Set { index: 0, value } => value);
@@ -349,23 +354,30 @@ async fn test_beacon_stop_preserves_locations() {
let start_id = event_id!("$beacon_start:example.org");
let stop_id = event_id!("$beacon_stop:example.org");
timeline.send_beacon_info(&ALICE, start_id, None, Duration::from_secs(3600), true).await;
// Both start and stop use the same session timestamp.
let session_ts = MilliSecondsSinceUnixEpoch::now();
timeline
.send_beacon_info(&ALICE, start_id, None, Duration::from_secs(3600), true, Some(session_ts))
.await;
let _item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
let ts = MilliSecondsSinceUnixEpoch(uint!(1_000));
timeline.send_beacon_location(&ALICE, start_id, 51.5, 0.1, ts).await;
let location_ts = MilliSecondsSinceUnixEpoch(uint!(1_000));
timeline.send_beacon_location(&ALICE, start_id, 51.5, 0.1, location_ts).await;
let item = assert_next_matches!(stream, VectorDiff::Set { index: 0, value } => value);
assert_eq!(item.content().as_live_location_state().unwrap().locations().len(), 1);
// Stop the session.
timeline.send_beacon_info(&ALICE, stop_id, None, Duration::from_secs(3600), false).await;
timeline
.send_beacon_info(&ALICE, stop_id, None, Duration::from_secs(3600), false, Some(session_ts))
.await;
let item = assert_next_matches!(stream, VectorDiff::Set { index: 0, value } => value);
let state = item.content().as_live_location_state().unwrap();
assert!(!state.is_live(), "should be stopped");
assert_eq!(state.locations().len(), 1, "location updates should be preserved after stop");
assert_eq!(state.latest_location().unwrap().ts(), ts);
assert_eq!(state.latest_location().unwrap().ts(), location_ts);
assert_pending!(stream);
}
@@ -380,8 +392,13 @@ async fn test_beacon_stop_before_start_is_applied_later() {
let start_id = event_id!("$beacon_start:example.org");
let stop_id = event_id!("$beacon_stop:example.org");
// Both start and stop use the same session timestamp.
let session_ts = MilliSecondsSinceUnixEpoch::now();
// Send the stop event first — the live start item doesn't exist yet.
timeline.send_beacon_info(&ALICE, stop_id, None, Duration::from_secs(3600), false).await;
timeline
.send_beacon_info(&ALICE, stop_id, None, Duration::from_secs(3600), false, Some(session_ts))
.await;
// No item should have been added to the timeline yet.
assert_pending!(stream);
@@ -391,7 +408,9 @@ async fn test_beacon_stop_before_start_is_applied_later() {
);
// Now send the live start event.
timeline.send_beacon_info(&ALICE, start_id, None, Duration::from_secs(3600), true).await;
timeline
.send_beacon_info(&ALICE, start_id, None, Duration::from_secs(3600), true, Some(session_ts))
.await;
// The item should appear already stopped — a single PushBack, no follow-up Set.
let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
@@ -404,6 +423,66 @@ async fn test_beacon_stop_before_start_is_applied_later() {
assert_pending!(stream);
}
/// A pending beacon stop from an OLD session should NOT be applied to a NEW
/// session. This tests the scenario where:
/// 1. User starts session A
/// 2. Stop for session A arrives out-of-order and is stashed
/// 3. User starts session B — the stashed stop should NOT apply
#[async_test]
async fn test_pending_beacon_stop_not_applied_to_different_session() {
let timeline = TestTimeline::new();
let mut stream = timeline.subscribe_events().await;
let old_stop_id = event_id!("$old_stop:example.org");
let new_start_id = event_id!("$new_start:example.org");
// Use the current time as base for session B.
// Session A's timestamp doesn't matter since its stop will be discarded.
// Session B needs a recent timestamp so is_live() doesn't fail due to timeout.
let old_session_ts = MilliSecondsSinceUnixEpoch(uint!(1)); // Old session (past)
let new_session_ts = MilliSecondsSinceUnixEpoch::now(); // New session (now)
// A stop event from the OLD session arrives first (out-of-order).
// Its corresponding start event is missing (maybe it's from long ago).
timeline
.send_beacon_info(
&ALICE,
old_stop_id,
None,
Duration::from_secs(3600),
false,
Some(old_session_ts),
)
.await;
// No item should appear — there's no start item for this stop.
assert_pending!(stream);
// Now a NEW session starts with a DIFFERENT timestamp.
timeline
.send_beacon_info(
&ALICE,
new_start_id,
Some("New session".to_owned()),
Duration::from_secs(3600),
true,
Some(new_session_ts),
)
.await;
// The new session should appear and remain LIVE because the pending stop
// belongs to a different session (different ts).
let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
let state = item.content().as_live_location_state().expect("should be a live location item");
assert!(
state.is_live(),
"the new session should be live; the old session's stop should NOT apply"
);
assert_eq!(state.description(), Some("New session"));
assert_pending!(stream);
}
/// Duplicate beacon location updates (same timestamp) are de-duplicated.
#[async_test]
async fn test_duplicate_beacon_location_is_deduplicated() {
@@ -411,7 +490,7 @@ async fn test_duplicate_beacon_location_is_deduplicated() {
let mut stream = timeline.subscribe_events().await;
let beacon_id = event_id!("$beacon_info:example.org");
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true).await;
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true, None).await;
let _item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
let ts = MilliSecondsSinceUnixEpoch(uint!(1_000));
@@ -466,7 +545,7 @@ async fn test_reaction_on_live_location_item() {
let mut stream = timeline.subscribe_events().await;
let beacon_id = event_id!("$beacon_info:example.org");
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true).await;
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true, None).await;
let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
assert!(item.content().as_live_location_state().is_some());
@@ -495,7 +574,7 @@ async fn test_multiple_reactions_on_live_location_item() {
let mut stream = timeline.subscribe_events().await;
let beacon_id = event_id!("$beacon_info:example.org");
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true).await;
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true, None).await;
let _item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
// ALICE and BOB both react, with different keys.
@@ -531,7 +610,7 @@ async fn test_reaction_before_live_location_item_is_applied_when_parent_arrives(
assert_pending!(stream);
// Now the beacon_info arrives.
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true).await;
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true, None).await;
// The item is inserted with the reaction already applied.
let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
@@ -552,7 +631,7 @@ async fn test_local_reaction_on_live_location_item() {
let mut stream = timeline.subscribe_events().await;
let beacon_id = event_id!("$beacon_info:example.org");
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true).await;
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true, None).await;
let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
let item_id = TimelineEventItemId::EventId(item.event_id().unwrap().to_owned());
@@ -583,6 +662,95 @@ async fn test_local_reaction_on_live_location_item() {
assert_pending!(stream);
}
/// `beacon_info_matches` returns true for identical content.
#[test]
fn test_beacon_info_matches_identical_content() {
let ts = MilliSecondsSinceUnixEpoch(uint!(1_000_000));
let a = BeaconInfoEventContent::new(
Some("Test".to_owned()),
Duration::from_secs(3600),
true,
Some(ts),
);
let b = BeaconInfoEventContent::new(
Some("Test".to_owned()),
Duration::from_secs(3600),
true,
Some(ts),
);
assert!(beacon_info_matches(&a, &b));
}
/// `beacon_info_matches` ignores the `live` field (start vs stop).
#[test]
fn test_beacon_info_matches_ignores_live_field() {
let ts = MilliSecondsSinceUnixEpoch(uint!(1_000_000));
let start = BeaconInfoEventContent::new(
Some("Test".to_owned()),
Duration::from_secs(3600),
true,
Some(ts),
);
let stop = BeaconInfoEventContent::new(
Some("Test".to_owned()),
Duration::from_secs(3600),
false,
Some(ts),
);
assert!(beacon_info_matches(&start, &stop), "should match even though live differs");
}
/// `beacon_info_matches` returns false for different timestamps.
#[test]
fn test_beacon_info_matches_different_ts() {
let a = BeaconInfoEventContent::new(
None,
Duration::from_secs(3600),
true,
Some(MilliSecondsSinceUnixEpoch(uint!(1_000))),
);
let b = BeaconInfoEventContent::new(
None,
Duration::from_secs(3600),
true,
Some(MilliSecondsSinceUnixEpoch(uint!(2_000))),
);
assert!(!beacon_info_matches(&a, &b), "different ts should not match");
}
/// `beacon_info_matches` returns false for different timeouts.
#[test]
fn test_beacon_info_matches_different_timeout() {
let ts = MilliSecondsSinceUnixEpoch(uint!(1_000_000));
let a = BeaconInfoEventContent::new(None, Duration::from_secs(3600), true, Some(ts));
let b = BeaconInfoEventContent::new(None, Duration::from_secs(7200), true, Some(ts));
assert!(!beacon_info_matches(&a, &b), "different timeout should not match");
}
/// `beacon_info_matches` returns false for different descriptions.
#[test]
fn test_beacon_info_matches_different_description() {
let ts = MilliSecondsSinceUnixEpoch(uint!(1_000_000));
let a = BeaconInfoEventContent::new(
Some("Session A".to_owned()),
Duration::from_secs(3600),
true,
Some(ts),
);
let b = BeaconInfoEventContent::new(
Some("Session B".to_owned()),
Duration::from_secs(3600),
true,
Some(ts),
);
assert!(!beacon_info_matches(&a, &b), "different description should not match");
}
impl TestTimeline {
/// Collect every event timeline item (no virtual items).
async fn live_location_event_items(&self) -> Vec<EventTimelineItem> {
@@ -597,18 +765,6 @@ impl TestTimeline {
description: Option<String>,
duration: Duration,
live: bool,
) {
self.send_beacon_info_with_ts(sender, event_id, description, duration, live, None).await;
}
/// Convenience: send a `beacon_info` state event with a custom timestamp.
async fn send_beacon_info_with_ts(
&self,
sender: &ruma::UserId,
event_id: &EventId,
description: Option<String>,
duration: Duration,
live: bool,
ts: Option<MilliSecondsSinceUnixEpoch>,
) {
let event = self