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 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 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}