Skip to main content

manabrew_server/
state.rs

1use dashmap::DashMap;
2use std::time::Instant;
3use tokio::sync::mpsc;
4use tokio_tungstenite::tungstenite::Message;
5
6use crate::analytics::AnalyticsHandle;
7use crate::client_build::ClientBuild;
8use crate::deck_play_events::DeckPlayEventHandle;
9use crate::identity::{IdentityVerifier, SessionIdentity};
10use crate::protocol::identity_token::GUEST_SUBJECT_PREFIX;
11use crate::room::Room;
12
13pub struct ConnectedPlayer {
14    pub player_id: String,
15    pub username: String,
16    pub room_id: Option<String>,
17    pub sender: mpsc::UnboundedSender<Message>,
18    /// Most recent heartbeat round trip for this connection. The aggregate
19    /// histogram cannot answer whether one slow decision sat behind one bad
20    /// link, because it drops the association at the moment it records.
21    pub last_client_rtt_ms: Option<u32>,
22    pub connected: bool,
23    pub generation: u64,
24    pub last_seen: Instant,
25    pub disconnected_at: Option<Instant>,
26    pub is_service: bool,
27    pub identity: Vec<SessionIdentity>,
28    pub name_verified: bool,
29    pub qualification: Option<String>,
30    /// What the client reported at authentication. Read when deciding which
31    /// wire features this seat can be sent.
32    pub client: ClientBuild,
33}
34
35impl ConnectedPlayer {
36    pub fn verified(&self) -> bool {
37        self.name_verified
38            && self.identity.iter().any(|identity| {
39                matches!(identity, SessionIdentity::Account(sub) if !sub.starts_with(GUEST_SUBJECT_PREFIX))
40            })
41    }
42}
43
44pub struct UsernameSession {
45    pub player_id: String,
46    pub room_id: Option<String>,
47    pub generation: u64,
48    pub connected: bool,
49    pub sender_closed: bool,
50    pub identity: Vec<SessionIdentity>,
51    pub name_verified: bool,
52}
53
54impl From<&ConnectedPlayer> for UsernameSession {
55    fn from(player: &ConnectedPlayer) -> Self {
56        UsernameSession {
57            player_id: player.player_id.clone(),
58            room_id: player.room_id.clone(),
59            generation: player.generation,
60            connected: player.connected,
61            sender_closed: player.sender.is_closed(),
62            identity: player.identity.clone(),
63            name_verified: player.name_verified,
64        }
65    }
66}
67
68pub struct ServerState {
69    pub players: DashMap<String, ConnectedPlayer>,
70    pub rooms: DashMap<String, Room>,
71    pub server_key: String,
72    pub max_rooms: usize,
73    pub official_key: Option<String>,
74    pub analytics: AnalyticsHandle,
75    pub deck_play_events: DeckPlayEventHandle,
76    pub identity: IdentityVerifier,
77}
78
79impl ServerState {
80    pub fn new(
81        server_key: String,
82        max_rooms: usize,
83        official_key: Option<String>,
84        analytics: AnalyticsHandle,
85        deck_play_events: DeckPlayEventHandle,
86        hub_jwks_url: Option<String>,
87    ) -> Self {
88        ServerState {
89            players: DashMap::new(),
90            rooms: DashMap::new(),
91            server_key,
92            max_rooms,
93            official_key,
94            analytics,
95            deck_play_events,
96            identity: IdentityVerifier::new(hub_jwks_url),
97        }
98    }
99
100    pub fn session_by_username(&self, username: &str) -> Option<UsernameSession> {
101        let mut connected = None;
102        for entry in self.players.iter() {
103            let player = entry.value();
104            if player.username != username {
105                continue;
106            }
107            if !player.connected {
108                return Some(UsernameSession::from(player));
109            }
110            connected.get_or_insert_with(|| UsernameSession::from(player));
111        }
112        connected
113    }
114}