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::protocol::LocalGameKind;
12use crate::room::Room;
13
14pub struct ConnectedPlayer {
15    pub player_id: String,
16    pub username: String,
17    pub room_id: Option<String>,
18    pub sender: mpsc::UnboundedSender<Message>,
19    /// Most recent heartbeat round trip for this connection. The aggregate
20    /// histogram cannot answer whether one slow decision sat behind one bad
21    /// link, because it drops the association at the moment it records.
22    pub last_client_rtt_ms: Option<u32>,
23    pub connected: bool,
24    pub generation: u64,
25    pub last_seen: Instant,
26    pub disconnected_at: Option<Instant>,
27    pub is_service: bool,
28    pub identity: Vec<SessionIdentity>,
29    pub name_verified: bool,
30    pub qualification: Option<String>,
31    pub avatar_url: Option<String>,
32    /// What the client reported at authentication. Read when deciding which
33    /// wire features this seat can be sent.
34    pub client: ClientBuild,
35    /// Playing on their own machine, reported by the client. Only meaningful
36    /// while `connected`: a dropped socket stops asserting anything.
37    pub local_game: Option<LocalGameKind>,
38}
39
40impl ConnectedPlayer {
41    pub fn verified(&self) -> bool {
42        self.name_verified
43            && self.identity.iter().any(|identity| {
44                matches!(identity, SessionIdentity::Account(sub) if !sub.starts_with(GUEST_SUBJECT_PREFIX))
45            })
46    }
47}
48
49pub struct UsernameSession {
50    pub player_id: String,
51    pub room_id: Option<String>,
52    pub generation: u64,
53    pub connected: bool,
54    pub sender_closed: bool,
55    pub identity: Vec<SessionIdentity>,
56    pub name_verified: bool,
57}
58
59impl From<&ConnectedPlayer> for UsernameSession {
60    fn from(player: &ConnectedPlayer) -> Self {
61        UsernameSession {
62            player_id: player.player_id.clone(),
63            room_id: player.room_id.clone(),
64            generation: player.generation,
65            connected: player.connected,
66            sender_closed: player.sender.is_closed(),
67            identity: player.identity.clone(),
68            name_verified: player.name_verified,
69        }
70    }
71}
72
73pub struct ServerState {
74    pub players: DashMap<String, ConnectedPlayer>,
75    pub rooms: DashMap<String, Room>,
76    pub server_key: String,
77    pub max_rooms: usize,
78    pub official_key: Option<String>,
79    pub analytics: AnalyticsHandle,
80    pub deck_play_events: DeckPlayEventHandle,
81    pub identity: IdentityVerifier,
82    pub art_base_url: Option<String>,
83}
84
85impl ServerState {
86    pub fn new(
87        server_key: String,
88        max_rooms: usize,
89        official_key: Option<String>,
90        analytics: AnalyticsHandle,
91        deck_play_events: DeckPlayEventHandle,
92        hub_jwks_url: Option<String>,
93    ) -> Self {
94        ServerState {
95            players: DashMap::new(),
96            rooms: DashMap::new(),
97            server_key,
98            max_rooms,
99            official_key,
100            analytics,
101            deck_play_events,
102            identity: IdentityVerifier::new(hub_jwks_url),
103            art_base_url: None,
104        }
105    }
106
107    /// Where this relay serves card art, handed to every client at auth. A
108    /// self-hosted box holding the images is the reason to run one, and the
109    /// client cannot guess the port.
110    pub fn with_art_base_url(mut self, url: Option<String>) -> Self {
111        self.art_base_url = url;
112        self
113    }
114
115    pub fn session_by_username(&self, username: &str) -> Option<UsernameSession> {
116        let mut connected = None;
117        for entry in self.players.iter() {
118            let player = entry.value();
119            if player.username != username {
120                continue;
121            }
122            if !player.connected {
123                return Some(UsernameSession::from(player));
124            }
125            connected.get_or_insert_with(|| UsernameSession::from(player));
126        }
127        connected
128    }
129}