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    pub avatar_url: Option<String>,
31    /// What the client reported at authentication. Read when deciding which
32    /// wire features this seat can be sent.
33    pub client: ClientBuild,
34}
35
36impl ConnectedPlayer {
37    pub fn verified(&self) -> bool {
38        self.name_verified
39            && self.identity.iter().any(|identity| {
40                matches!(identity, SessionIdentity::Account(sub) if !sub.starts_with(GUEST_SUBJECT_PREFIX))
41            })
42    }
43}
44
45pub struct UsernameSession {
46    pub player_id: String,
47    pub room_id: Option<String>,
48    pub generation: u64,
49    pub connected: bool,
50    pub sender_closed: bool,
51    pub identity: Vec<SessionIdentity>,
52    pub name_verified: bool,
53}
54
55impl From<&ConnectedPlayer> for UsernameSession {
56    fn from(player: &ConnectedPlayer) -> Self {
57        UsernameSession {
58            player_id: player.player_id.clone(),
59            room_id: player.room_id.clone(),
60            generation: player.generation,
61            connected: player.connected,
62            sender_closed: player.sender.is_closed(),
63            identity: player.identity.clone(),
64            name_verified: player.name_verified,
65        }
66    }
67}
68
69pub struct ServerState {
70    pub players: DashMap<String, ConnectedPlayer>,
71    pub rooms: DashMap<String, Room>,
72    pub server_key: String,
73    pub max_rooms: usize,
74    pub official_key: Option<String>,
75    pub analytics: AnalyticsHandle,
76    pub deck_play_events: DeckPlayEventHandle,
77    pub identity: IdentityVerifier,
78}
79
80impl ServerState {
81    pub fn new(
82        server_key: String,
83        max_rooms: usize,
84        official_key: Option<String>,
85        analytics: AnalyticsHandle,
86        deck_play_events: DeckPlayEventHandle,
87        hub_jwks_url: Option<String>,
88    ) -> Self {
89        ServerState {
90            players: DashMap::new(),
91            rooms: DashMap::new(),
92            server_key,
93            max_rooms,
94            official_key,
95            analytics,
96            deck_play_events,
97            identity: IdentityVerifier::new(hub_jwks_url),
98        }
99    }
100
101    pub fn session_by_username(&self, username: &str) -> Option<UsernameSession> {
102        let mut connected = None;
103        for entry in self.players.iter() {
104            let player = entry.value();
105            if player.username != username {
106                continue;
107            }
108            if !player.connected {
109                return Some(UsernameSession::from(player));
110            }
111            connected.get_or_insert_with(|| UsernameSession::from(player));
112        }
113        connected
114    }
115}