2024-10-05 12:57:13 -07:00

997 lines
44 KiB
Rust

use bus::Bus;
use game::history::Play;
use crate::{
config,
app::{
authentication::Authentication,
connection::Connection,
user::{User, UserStatus},
App,
session::Session,
},
protocol,
};
pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
{
use futures::SinkExt;
use protocol::*;
use ring::rand::{SecureRandom, SystemRandom};
let rng = SystemRandom::new();
let argon_config = argon2::Config::default();
let mut send_user_status = Vec::<u32>::new();
while let Some(packet) = bus.receive_wait() {
let qr = packet.data;
let mut user_id = None;
let mut session_id = None;
if let Some(conn) = app.connections.get(qr.id as usize) {
session_id = conn.session;
if let Some(auth_id) = conn.auth {
if let Some(auth) = app.auths.get(&auth_id) {
user_id = Some(auth.user);
}
}
}
match match qr.data {
QRPacketData::QConn(request) => {
let id = app.connections.add(Connection {
bus: request.bus_id,
stream: request.stream,
auth: None,
session: None,
prev:0,
next:0,
});
if let Some(conn) = app.connections.get_mut(id) {
conn.prev = id as u32;
conn.next = id as u32;
}
println!("Connect: {}", id);
bus.send(
packet.from,
QRPacket::new(id as u32, QRPacketData::RConn)
).ok();
Some(QRPacket::new(0, QRPacketData::None))
}
QRPacketData::QDisconn => {
println!("Disconnect: {}", qr.id);
// Uninitialize connection
if if let Some(conn) = app.connections.get(qr.id as usize).cloned() {
// Disassociate session if present
if let Some(session_token) = conn.session {
if let Some(session) = app.sessions.get_mut(&session_token) {
if user_id == Some(session.p_dawn.user) { session.remove_connection(0, qr.id); }
else if user_id == Some(session.p_dusk.user) { session.remove_connection(1, qr.id); }
else { session.remove_connection(2, qr.id); }
}
}
// Remove connection from chain.
if let Some(auth_id) = conn.auth {
if let Some(auth) = app.auths.get(&auth_id).cloned() {
// Update user connection reference.
if let Some(user) = app.get_user_by_id_mut(auth.user) {
if Some(qr.id) == user.connection {
// Set connection to next if exists.
if conn.next != qr.id {
user.connection = Some(conn.next);
} else {
user.connection = None;
}
}
}
// Link prev and next connections.
if conn.next != qr.id {
if let Some(prev_conn) = app.connections.get_mut(conn.prev as usize) {
prev_conn.next = conn.next;
}
if let Some(next_conn) = app.connections.get_mut(conn.next as usize) {
next_conn.prev = conn.prev;
}
}
}
}
// Close socket
let mut socket = conn.stream.write().await;
socket.close().await.ok();
true
} else { false } {
app.connections.remove(qr.id as usize).ok();
}
Some(QRPacket::new(0, QRPacketData::None))
}
QRPacketData::QHello => {
let response = PacketHelloResponse {
version:config::VERSION.to_string(),
};
Some(QRPacket::new(qr.id, QRPacketData::RHello(response)))
}
QRPacketData::QRegister(request) => {
let mut response = PacketRegisterResponse::new();
response.status = STATUS_SERVER_ERROR;
println!("Request: Register");
let mut is_valid = true;
if request.code != crate::config::REGISTER_CODE.as_bytes() { response.status = STATUS_BAD_CODE; is_valid = false; }
if is_valid && request.handle.len() == 0 { response.status = STATUS_BAD_HANDLE; is_valid = false; }
if is_valid && request.secret.len() == 0 { response.status = STATUS_BAD_SECRET; is_valid = false; }
if is_valid {
let handle = request.handle.to_lowercase();
let display_name = request.handle.clone();
match app.user_handle.get(handle.as_bytes()) {
None => {
let mut salt = [0u8; 16];
match rng.fill(&mut salt) {
Ok(_) => {
let salt_id = app.filesystem.salt_create(salt).unwrap();
app.salts.set(salt_id as isize, salt);
if let Ok(secret) = argon2::hash_raw(&request.secret, &salt, &argon_config) {
let user_id = app.filesystem.user_count().unwrap() as u32;
// Register user pool id and handle
app.filesystem.handle_create(&handle, user_id).ok();
app.user_handle.set(handle.as_bytes(), user_id);
let user_data = User {
id:user_id,
flags:0,
handle:display_name,
secret,
na_key:salt_id,
connection:Some(qr.id),
status:UserStatus::new(),
challenges:Vec::new(),
};
app.filesystem.user_create(&user_data).ok();
// Create user entry
let user_pos = app.users.add(user_data);
app.user_id.set(user_id as isize, user_pos);
println!("Registered user '{}' @ {} with id {}", request.handle, user_pos, user_id);
// Generate authentication token and secret
response.status = STATUS_OK;
rng.fill(&mut response.secret).ok();
loop {
rng.fill(&mut response.token).ok();
if app.auths.get(&response.token).is_none() {
app.auths.set(&response.token, Authentication {
key:response.token,
secret:response.secret,
user:user_id,
});
break;
}
}
// Attach authentication to connection
if let Some(conn) = app.connections.get_mut(qr.id as usize) {
conn.auth = Some(response.token);
}
}
}
Err(_) => { println!("error: failed to generate salt.") }
}
}
Some(_) => {
response.status = STATUS_BAD_HANDLE;
println!("notice: attempt to register existing handle: '{}'", request.handle);
}
}
}
Some(QRPacket::new(qr.id, QRPacketData::RRegister(response)))
}
QRPacketData::QAuth(request) => {
let mut response = PacketAuthResponse::new();
response.status = STATUS_ERROR;
println!("Request: Auth");
let mut is_valid = true;
if is_valid && request.handle.len() == 0 { response.status = STATUS_BAD_HANDLE; is_valid = false; }
if is_valid && request.secret.len() == 0 { response.status = STATUS_BAD_SECRET; is_valid = false; }
if is_valid {
// Get user data from handle
match app.user_handle.get(request.handle.to_lowercase().as_bytes()).cloned() {
Some(uid) => {
if let Some(tuid) = app.user_id.get(uid as isize).cloned() {
if let Some(mut user) = app.users.get(tuid).cloned() {
// Get user salt
if let Some(salt) = app.salts.get(user.na_key as isize).cloned() {
// [TEMPORARY] WORKAROUND FOR PASSWORD RESET
if user.secret.is_empty() {
println!("Password reset: {}", user.handle);
if let Ok(secret) = argon2::hash_raw(&request.secret.as_bytes(), &salt, &argon_config) {
user.secret = secret;
if if let Some(app_user) = app.users.get_mut(tuid) {
app_user.secret = user.secret.clone();
true
} else { false } {
app.filesystem.user_update(uid, &user).ok();
}
}
} else {
// Verify salted secret against user data
if argon2::verify_raw(&request.secret.as_bytes(), &salt, &user.secret, &argon_config).unwrap_or(false) {
println!("Authenticated user '{}' id {}", user.handle, uid);
// Generate authentication token and secret
response.status = STATUS_OK;
rng.fill(&mut response.secret).ok();
loop {
rng.fill(&mut response.token).ok();
if app.auths.get(&response.token).is_none() {
app.auths.set(&response.token, Authentication {
key:response.token,
secret:response.secret,
user:uid,
});
break;
}
}
// Mark send status.
send_user_status.push(uid);
// Attach authentication to connection.
if let Some(conn) = app.connections.get_mut(qr.id as usize) {
conn.auth = Some(response.token);
if let Some(cid) = user.connection {
conn.prev = cid;
}
}
// Add connection to chain.
if let Some(user_cid) = user.connection {
if let Some(existing) = app.connections.get(user_cid as usize).cloned() {
if let Some(conn) = app.connections.get_mut(qr.id as usize) {
conn.next = existing.next;
}
}
} else {
if let Some(user) = app.users.get_mut(tuid) {
user.connection = Some(qr.id);
}
}
} else {
println!("notice: password verification failed.");
}
}
} else {
println!("error: user salt id '{}' not found.", user.na_key);
}
} else {
println!("error: user with id '{}' not found.", uid);
}
} else {
println!("error: user with id '{}' not found.", uid);
}
}
None => { }
}
}
Some(QRPacket::new(qr.id, QRPacketData::RAuth(response)))
}
QRPacketData::QAuthResume(request) => {
println!("Request: Auth Resume");
let mut response = PacketAuthResumeResponse::new();
response.status = STATUS_ERROR;
if let Some(auth) = app.auths.get(&request.token).cloned() {
// Compare full secret length to reduce time-based attacks.
let mut valid = true;
for i in 0..16 {
valid |= auth.secret[i] == request.secret[i];
}
if valid {
if if let Some(conn) = app.connections.get_mut(qr.id as usize) {
conn.auth = Some(request.token);
response.status = STATUS_OK;
true
} else {
response.status = STATUS_SERVER_ERROR;
false
} {
// Add connection to chain.
if let Some(user) = app.get_user_by_id(auth.user).cloned() {
if let Some(user_cid) = user.connection {
if let Some(existing) = app.connections.get(user_cid as usize).cloned() {
if let Some(conn) = app.connections.get_mut(qr.id as usize) {
conn.next = existing.next;
}
}
} else {
if let Some(user) = app.get_user_by_id_mut(auth.user) {
user.connection = Some(qr.id);
}
}
// Mark send user status.
send_user_status.push(auth.user);
}
}
}
}
Some(QRPacket::new(qr.id, QRPacketData::RAuthResume(response)))
}
QRPacketData::QAuthRevoke => {
println!("Request: Auth Revoke");
// Remove connection from chain.
if let Some(conn) = app.connections.get(qr.id as usize).cloned() {
if let Some(auth_id) = conn.auth {
if let Some(auth) = app.auths.get(&auth_id).cloned() {
// Update user connection reference.
if let Some(user) = app.get_user_by_id_mut(auth.user) {
if Some(qr.id) == user.connection {
// Set connection to next if exists.
if conn.next != qr.id {
user.connection = Some(conn.next);
} else {
user.connection = None;
}
}
}
// Link prev and next connections.
if conn.next != qr.id {
if let Some(prev_conn) = app.connections.get_mut(conn.prev as usize) {
prev_conn.next = conn.next;
}
if let Some(next_conn) = app.connections.get_mut(conn.next as usize) {
next_conn.prev = conn.prev;
}
}
}
}
}
// Remove authentication from connection.
if let Some(conn) = app.connections.get_mut(qr.id as usize) {
match conn.auth {
Some(auth) => {
println!("Deauthenticated connection: {}", qr.id);
app.auths.unset(&auth);
}
None => { }
}
conn.auth = None;
}
Some(QRPacket::new(0, QRPacketData::None))
}
QRPacketData::QSessionList(request) => {
println!("Request: Session List");
let mut response = PacketSessionListResponse::new();
let mut count = 0;
let mut next_id = app.session_time.begin();
while let Some(id) = next_id {
let token = app.session_time.get(id).unwrap();
if let Some(session) = app.sessions.get(token) {
let mut valid = match request.game_state {
1 => !session.game.is_complete(),
2 => !session.game.is_complete(),
3 => session.game.is_complete(),
_ => true,
};
valid &= !request.is_player || Some(session.p_dawn.user) == user_id || Some(session.p_dusk.user) == user_id;
valid &= !request.is_live || (session.p_dawn.connections.len() > 0 && session.p_dusk.connections.len() > 0);
if valid {
let player :u8 = if user_id.is_some() {
if Some(session.p_dawn.user) == user_id { 1 }
else if Some(session.p_dusk.user) == user_id { 2 }
else { 0 }
} else { 0 };
let is_turn = player != 0 && (session.game.turn & 1) == player as u16 - 1;
let is_complete = (session.game.is_complete() as u8) * (((session.game.turn & 1) == 0) as u8 + 1);
let dawn_handle = if let Some(user) = app.get_user_by_id(session.p_dawn.user) {
user.handle.clone()
} else { String::new() };
let dusk_handle = if let Some(user) = app.get_user_by_id(session.p_dusk.user) {
user.handle.clone()
} else { String::new() };
response.records.push(PacketSessionListResponseRecord {
token:session.token,
handles:[
dawn_handle,
dusk_handle,
],
turn:session.game.turn,
last_move:[0; 3],
viewers:session.connections.len() as u32,
player,
is_turn,
is_complete,
});
count += 1;
}
}
if count >= 60 { break; }
next_id = app.session_time.next(id);
}
Some(QRPacket::new(qr.id, QRPacketData::RSessionList(response)))
}
QRPacketData::QSessionView(request) => {
println!("Request: Session Join");
let mut packets = Vec::<QRPacket>::new();
let mut response = PacketSessionViewResponse::new();
response.status = STATUS_ERROR;
response.token = request.token;
// Verify that session exists
if let Some(session) = app.sessions.get_mut(&request.token) {
response.is_complete = session.game.is_complete();
if user_id == Some(session.p_dawn.user) {
response.player = 1;
} else if user_id == Some(session.p_dusk.user) {
response.player = 2;
}
// Join game as player
if if request.join {
// Verify client is authenticated
if user_id.is_some() {
// Resume session if user is player
if Some(session.p_dawn.user) == user_id || Some(session.p_dusk.user) == user_id {
println!("User resumes session.");
response.status = STATUS_OK;
for (cid, _) in session.get_connections() {
packets.push(QRPacket::new(
cid,
QRPacketData::GameMessage(PacketGameMessage {
data: GameMessageData::Online(1 + (Some(session.p_dusk.user) == user_id) as u8, 1),
}),
));
}
true
} else { false }
} else { response.status = STATUS_NOAUTH; false }
}
// Join game as spectator.
else {
println!("User spectates session.");
response.status = STATUS_OK;
// Add user online packets.
for (cid, _) in session.get_connections() {
packets.push(QRPacket::new(
cid,
QRPacketData::GameMessage(PacketGameMessage {
data: GameMessageData::Online(0, 1 + session.spectators() as u32),
}),
));
}
true
} {
// Associate session and connection on join
if let Some(conn) = app.connections.get_mut(qr.id as usize) {
conn.session = Some(session.token);
}
let mode = if user_id == Some(session.p_dawn.user) {
0
} else if user_id == Some(session.p_dusk.user) {
1
} else {
2
};
session.add_connection(mode, qr.id);
}
}
// Send notification packets
for packet in packets {
app.send_response(packet).await;
}
Some(QRPacket::new(qr.id, QRPacketData::RSessionView(response)))
}
// SessionResign
QRPacketData::QSessionResign(request) => {
use game::history::Play;
println!("Request: Session Resign");
let mut packets = Vec::<QRPacket>::new();
let play = Play {
source: 0xF,
from: 0,
to: 0,
};
if let Some(session) = app.sessions.get_mut(&request.token) {
if !session.game.is_complete() {
if (user_id == Some(session.p_dawn.user) && session.game.turn & 1 == 0)
|| (user_id == Some(session.p_dusk.user) && session.game.turn & 1 == 1) {
session.game.process(&play).ok();
app.filesystem.session_history_push(session.id, play).ok();
send_user_status.push(session.p_dawn.user);
send_user_status.push(session.p_dusk.user);
for (cid, _) in session.get_connections() {
packets.push(QRPacket::new(
cid,
QRPacketData::GameMessage(PacketGameMessage {
data: GameMessageData::Resign,
}),
));
}
}
}
}
for packet in packets {
app.send_response(packet).await;
}
Some(QRPacket::new(0, QRPacketData::None))
}
// SessionLeave
QRPacketData::QSessionLeave => {
println!("Request: Session Leave");
let mut packets = Vec::<QRPacket>::new();
// Verify that session exists.
if let Some(session_token) = session_id {
if let Some(session) = app.sessions.get_mut(&session_token) {
if user_id == Some(session.p_dawn.user) {
session.remove_connection(0, qr.id);
if session.p_dawn.connections.len() == 0 {
for (cid, _) in session.get_connections() {
packets.push(QRPacket::new(
cid,
QRPacketData::GameMessage(PacketGameMessage {
data: GameMessageData::Online(1, 0),
}),
));
}
}
}
else if user_id == Some(session.p_dusk.user) {
session.remove_connection(1, qr.id);
if session.p_dusk.connections.len() == 0 {
for (cid, _) in session.get_connections() {
packets.push(QRPacket::new(
cid,
QRPacketData::GameMessage(PacketGameMessage {
data: GameMessageData::Online(2, 0),
}),
));
}
}
}
else {
session.remove_connection(2, qr.id);
// Add user online packets.
for (cid, _) in session.get_connections() {
packets.push(QRPacket::new(
cid,
QRPacketData::GameMessage(PacketGameMessage {
data: GameMessageData::Online(0, session.spectators() as u32),
}),
));
}
}
}
}
for packet in packets {
app.send_response(packet).await;
}
Some(QRPacket::new(0, QRPacketData::None))
}
// GameState
QRPacketData::QGameState(request) => {
let mut response = PacketGameStateResponse::new();
println!("Request: Game State");
if let Some(session) = app.sessions.get(&request.token) {
response.status = STATUS_OK;
response = generate_game_state(&app, &session);
if user_id == Some(session.p_dawn.user) { response.player = 0; }
else if user_id == Some(session.p_dusk.user) { response.player = 1; }
} else {
response.status = STATUS_ERROR;
}
Some(QRPacket::new(qr.id, QRPacketData::RGameState(response)))
}
// GameMessage
QRPacketData::GameMessage(request) => {
println!("Request: Game Message");
let mut packets = Vec::<QRPacket>::new();
if let Some(sid) = session_id {
if let Some(session) = app.sessions.get_mut(&sid) {
match request.data {
GameMessageData::PlayMove(turn, from, to)
| GameMessageData::PlayDrop(turn, from, to)
| GameMessageData::PlayAlt(turn, from, to)
=> {
println!("HERE");
if !session.game.is_complete() {
if (user_id == Some(session.p_dawn.user) && session.game.turn & 1 == 0)
|| (user_id == Some(session.p_dusk.user) && session.game.turn & 1 == 1) {
if turn == session.game.turn {
let play = Play {
source: match request.data {
GameMessageData::PlayMove(..) => 0,
GameMessageData::PlayDrop(..) => 1,
GameMessageData::PlayAlt(..) => 2,
_ => 0,
},
from, to,
};
println!("play {} {} {}", play.source, play.from, play.to);
if session.game.process(&play).is_ok() {
// Commit play to history
app.filesystem.session_history_push(session.id, play).ok();
// Forward messsage to all clients
for (cid, _) in session.get_connections() {
packets.push(QRPacket::new(
cid,
QRPacketData::GameMessage(request.clone())
));
}
// Send status to players.
send_user_status.push(session.p_dawn.user);
send_user_status.push(session.p_dusk.user);
}
}
}
}
}
GameMessageData::Undo(turn, _) => {
if !session.game.is_complete() {
if (user_id == Some(session.p_dawn.user) && session.game.turn & 1 == 0)
|| (user_id == Some(session.p_dusk.user) && session.game.turn & 1 == 1) {
if turn == session.game.turn {
// Request or commit undo
}
}
}
}
GameMessageData::Resign => {
if !session.game.is_complete() {
if (user_id == Some(session.p_dawn.user) && session.game.turn & 1 == 0)
|| (user_id == Some(session.p_dusk.user) && session.game.turn & 1 == 1) {
// Forward messsage to all clients
for (cid, _) in session.get_connections() {
packets.push(QRPacket::new(
cid,
QRPacketData::GameMessage(request.clone())
));
}
}
}
}
GameMessageData::Reaction(_) => {
// Forward messsage to all clients
for (cid, _) in session.get_connections() {
packets.push(QRPacket::new(
cid,
QRPacketData::GameMessage(request.clone())
));
}
}
_ => { }
}
}
}
match request.data {
GameMessageData::Error => {
Some(QRPacket::new(qr.id, QRPacketData::GameMessage(request)))
}
_ => {
for packet in packets {
app.send_response(packet).await;
}
// Updates already sent; nothing to do here.
Some(QRPacket::new(qr.id, QRPacketData::None))
}
}
}
// Challenge
QRPacketData::QChallenge(request) => {
println!("Request: Challenge");
if let Some(user_id) = user_id {
if let Some(chal_id) = app.user_handle.get(request.handle.to_lowercase().as_bytes()).cloned() {
if let Some(chal_user) = app.get_user_by_id_mut(chal_id) {
let mut find = false;
for ch in &chal_user.challenges {
if *ch == user_id { find = true; }
}
if !find {
chal_user.challenges.push(user_id);
send_user_status.push(chal_id);
} else {
println!("notice: duplicate challenge.");
}
}
}
}
None
}
// ChallengeAnswer
QRPacketData::QChallengeAnswer(request) => {
println!("Request: Challenge Answer");
use crate::app::session::{SessionToken, SessionSecret, Player};
let mut response = PacketChallengeAnswerResponse::new();
response.status = STATUS_ERROR;
if let Some(user_id) = user_id {
if let Some(chal_id) = app.user_handle.get(request.handle.to_lowercase().as_bytes()).copied() {
// Check if challenge exists.
if let Some(user) = app.get_user_by_id_mut(user_id) {
match user.challenges.binary_search(&chal_id) {
Ok(pos) => {
response.status = if request.answer { STATUS_OK } else { STATUS_REJECT };
user.challenges.remove(pos);
}
Err(_) => { }
}
}
// Create session if response was positive.
if response.status == STATUS_OK {
// Generate session token
let mut token = SessionToken::default();
let mut secret = SessionSecret::default();
loop {
rng.fill(&mut token).ok();
if app.sessions.get(&token).is_none() { break; }
}
rng.fill(&mut secret).ok();
// Add session to latest list.
let chain_id = app.session_time.add(token);
// Choose player seats.
let time = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_millis() as u64;
println!("Time {}", time);
// Build session.
let mut session = Session {
id:0,
token,
secret,
game:game::Game::new(),
p_dawn:Player {
user:if (time & 1) == 0 { user_id } else { chal_id },
connections:Vec::new(),
},
p_dusk:Player {
user:if (time & 1) == 0 { chal_id } else { user_id },
connections:Vec::new(),
},
connections:Vec::new(),
time:std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as u64,
chain_id,
undo:None,
};
session.game.init();
if let Some(conn) = app.connections.get_mut(qr.id as usize) {
conn.session = Some(session.token);
}
session.id = app.filesystem.session_create(&session).unwrap();
app.sessions.set(&token, session);
app.session_time.set(chain_id, token);
response.token = token;
send_user_status.push(chal_id);
}
send_user_status.push(user_id);
}
}
Some(QRPacket::new(qr.id, QRPacketData::RChallengeAnswer(response)))
}
// ChallengeList
QRPacketData::QChallengeList => {
println!("Request: Challenge List");
let mut response = PacketChallengeListResponse::new();
response.status = STATUS_NOAUTH;
if let Some(user_id) = user_id {
if let Some(user) = app.get_user_by_id(user_id) {
response.status = STATUS_OK;
for ch_id in &user.challenges {
if let Some(target) = app.get_user_by_id(*ch_id) {
response.challenges.push(PacketChallengeListData {
handle:target.handle.clone(),
});
}
}
}
}
Some(QRPacket::new(qr.id, QRPacketData::RChallengeList(response)))
}
// UserList
QRPacketData::QUserList => {
println!("Request: User List");
let mut response = PacketUserListResponse::new();
response.status = STATUS_NOAUTH;
if let Some(user_id) = user_id {
for (_, uid) in app.user_id.pairs() {
let uid = *uid as u32;
if uid != user_id {
if let Some(user) = app.get_user_by_id(uid) {
response.records.push(PacketUserListData {
handle:user.handle.clone(),
is_online:user.connection.is_some(),
});
}
}
}
}
response.records.sort_by(|a, b| {
b.is_online.cmp(&a.is_online)
.then(a.handle.to_lowercase().cmp(&b.handle.to_lowercase()))
});
Some(QRPacket::new(qr.id, QRPacketData::RUserList(response)))
}
_ => { Some(QRPacket::new(0, QRPacketData::None)) }
} {
Some(response) => {
app.send_response(response).await;
}
None => { }
}
for user_id in &send_user_status {
app.send_status(*user_id).await;
}
send_user_status.clear();
}
}
fn generate_game_state(app:&App, session:&Session) -> protocol::PacketGameStateResponse
{
use protocol::PacketGameStateResponse;
let mut response = PacketGameStateResponse::new();
response.token = session.token;
response.player = 2;
// Get Dawn handle
if let Some(user) = app.get_user_by_id(session.p_dawn.user) {
response.dawn_handle = user.handle.clone();
}
// Get Dusk handle
if let Some(user) = app.get_user_by_id(session.p_dusk.user) {
response.dusk_handle = user.handle.clone();
}
response.dawn_online = session.p_dawn.connections.len() > 0;
response.dusk_online = session.p_dusk.connections.len() > 0;
response.spectators = session.connections.len() as u32;
// Get history
response.history = session.game.history.clone();
response
}