Skip to main content

manabrew_server/
state.rs

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