Skip to main content

mas_handlers/activity_tracker/
bound.rs

1// Copyright 2026 Element Creations Ltd.
2// Copyright 2024, 2025 New Vector Ltd.
3// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
4//
5// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
6// Please see LICENSE files in the repository root for full details.
7
8use std::{convert::Infallible, net::IpAddr};
9
10use axum::extract::FromRequestParts;
11use mas_data_model::{
12    BrowserSession, Clock, CompatSession, Session, personal::session::PersonalSession,
13};
14
15use crate::{ClientIp, activity_tracker::ActivityTracker};
16
17/// An activity tracker with an IP address bound to it.
18#[derive(Clone)]
19pub struct Bound {
20    tracker: ActivityTracker,
21    ip: Option<IpAddr>,
22}
23
24impl Bound {
25    /// Create a new bound activity tracker.
26    #[must_use]
27    pub fn new(tracker: ActivityTracker, ip: Option<IpAddr>) -> Self {
28        Self { tracker, ip }
29    }
30
31    /// Get the IP address bound to this activity tracker.
32    #[must_use]
33    pub fn ip(&self) -> Option<IpAddr> {
34        self.ip
35    }
36
37    /// Record activity in an OAuth 2.0 session.
38    pub async fn record_oauth2_session(&self, clock: &dyn Clock, session: &Session) {
39        self.tracker
40            .record_oauth2_session(clock, session, self.ip)
41            .await;
42    }
43
44    /// Record activity in a personal session.
45    pub async fn record_personal_session(&self, clock: &dyn Clock, session: &PersonalSession) {
46        self.tracker
47            .record_personal_session(clock, session, self.ip)
48            .await;
49    }
50
51    /// Record activity in a compatibility session.
52    pub async fn record_compat_session(&self, clock: &dyn Clock, session: &CompatSession) {
53        self.tracker
54            .record_compat_session(clock, session, self.ip)
55            .await;
56    }
57
58    /// Record activity in a browser session.
59    pub async fn record_browser_session(&self, clock: &dyn Clock, session: &BrowserSession) {
60        self.tracker
61            .record_browser_session(clock, session, self.ip)
62            .await;
63    }
64}
65
66/// Extracts a [`Bound`] activity tracker for any state that can provide an
67/// [`ActivityTracker`]. The client IP is read from the [`ClientIp`] request
68/// extension (set by the IP-detection middleware), defaulting to `None` when it
69/// is absent.
70impl<S> FromRequestParts<S> for Bound
71where
72    S: Send + Sync,
73    ActivityTracker: FromRequestParts<S, Rejection = Infallible>,
74{
75    type Rejection = Infallible;
76
77    async fn from_request_parts(
78        parts: &mut axum::http::request::Parts,
79        state: &S,
80    ) -> Result<Self, Self::Rejection> {
81        let ip = parts.extensions.get::<ClientIp>().and_then(|ip| ip.0);
82        let tracker = match ActivityTracker::from_request_parts(parts, state).await {
83            Ok(tracker) => tracker,
84            Err(infallible) => match infallible {},
85        };
86        Ok(tracker.bind(ip))
87    }
88}