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