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 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 pub client: ClientBuild,
38 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 direct_transport: bool,
90 pub ice_servers: Vec<crate::protocol::IceServer>,
92 pub lobby_chat: Mutex<ChatHistory>,
93 pub seal: Option<MessageSealer>,
94 pub art_base_url: Option<String>,
95}
96
97impl ServerState {
98 #[allow(clippy::too_many_arguments)]
99 pub fn new(
100 server_key: String,
101 max_rooms: usize,
102 official_key: Option<String>,
103 analytics: AnalyticsHandle,
104 deck_play_events: DeckPlayEventHandle,
105 hub_jwks_url: Option<String>,
106 seal: Option<MessageSealer>,
107 ) -> Self {
108 ServerState {
109 players: DashMap::new(),
110 rooms: DashMap::new(),
111 server_key,
112 max_rooms,
113 official_key,
114 analytics,
115 deck_play_events,
116 identity: IdentityVerifier::new(hub_jwks_url),
117 direct_transport: false,
118 ice_servers: Vec::new(),
119 lobby_chat: Mutex::new(ChatHistory::default()),
120 seal,
121 art_base_url: None,
122 }
123 }
124
125 pub fn with_direct_transport(
126 mut self,
127 enabled: bool,
128 ice_servers: Vec<crate::protocol::IceServer>,
129 ) -> Self {
130 self.direct_transport = enabled;
131 self.ice_servers = ice_servers;
132 self
133 }
134
135 pub fn with_art_base_url(mut self, url: Option<String>) -> Self {
139 self.art_base_url = url;
140 self
141 }
142
143 pub fn session_by_username(&self, username: &str) -> Option<UsernameSession> {
144 let mut connected = None;
145 for entry in self.players.iter() {
146 let player = entry.value();
147 if player.username != username {
148 continue;
149 }
150 if !player.connected {
151 return Some(UsernameSession::from(player));
152 }
153 connected.get_or_insert_with(|| UsernameSession::from(player));
154 }
155 connected
156 }
157}