use actix_web::http::header;
use actix_web::{web, HttpRequest, HttpResponse};
use uuid::Uuid;
use crate::pages::templates::SLOW_GAME;
use crate::pages::{insert_security_headers, naive_to_text, request_to_jar, CommonAuthInfo};
use crate::{DataBaseRo, DataBaseRw, WebData};
#[derive(serde_derive::Serialize)]
struct PendingDelegateInfo {
csrf: Uuid,
name: String,
accept: bool,
}
#[derive(serde_derive::Serialize)]
struct PlayerInfo {
player_name: String,
team_id: i32,
create_ts: Option<String>,
delegate_name: Option<String>,
player_itself: bool,
delegate_itself: bool,
revoke_delegate_data: Option<JoinLeaveData>,
query_delegate_data: Option<JoinLeaveData>,
pending_delegates: Option<Vec<PendingDelegateInfo>>,
}
#[derive(serde_derive::Deserialize, serde_derive::Serialize)]
pub struct JoinLeaveData {
csrf: Uuid,
}
#[derive(serde_derive::Deserialize)]
pub struct QueryDelegationData {
csrf: Uuid,
delegate_name: String,
}
#[derive(serde_derive::Deserialize)]
pub struct DelegationData {
csrf: Uuid,
submit: String,
}
#[derive(serde_derive::Serialize)]
struct GameData<'a> {
common_auth_info: CommonAuthInfo<'a>,
gameuid: &'a str,
gameuidenc: String,
status: Option<&'a str>,
notes_html: Option<&'a str>,
fo_forum_url: Option<&'a str>,
players: i64,
min_turn_ts: Option<String>,
max_turn_ts: Option<String>,
turns: i64,
player_list: Option<Vec<PlayerInfo>>,
join_data: Option<JoinLeaveData>,
leave_data: Option<JoinLeaveData>,
is_teamed: bool,
}
fn naive_to_diff_utc(
naive: &chrono::NaiveDateTime,
now: chrono::DateTime<chrono::Utc>,
) -> (chrono::DateTime<chrono::Utc>, chrono::Duration) {
use chrono::TimeZone;
let ts = chrono::Utc::from_utc_datetime(&chrono::Utc, naive);
(ts, now - ts)
}
fn diff_utc_to_string((ts, diff): (chrono::DateTime<chrono::Utc>, chrono::Duration)) -> String {
let (part, part_diff) = if diff.num_weeks() != 0 {
("weeks", diff.num_weeks())
} else if diff.num_days() != 0 {
("days", diff.num_days())
} else if diff.num_hours() != 0 {
("hours", diff.num_hours())
} else if diff.num_minutes() != 0 {
("minutes", diff.num_minutes())
} else {
("seconds", diff.num_seconds())
};
format!(
"{} ({} {})",
ts.format("%Y %b %d %H:%M UTC"),
part_diff,
part
)
}
#[actix_web::get("slow-game-{path}.html")]
pub async fn slow_game(
request: HttpRequest,
path: web::Path<String>,
data: web::Data<WebData<'_>>,
data_ro: web::Data<DataBaseRo>,
) -> HttpResponse {
let gameuid = path.into_inner();
let gameuid = match gameuid.char_indices().nth(128) {
None => gameuid,
Some((idx, _)) => gameuid[..idx].to_string(),
};
let dbclient = match data_ro.0.get().await {
Ok(client) => client,
Err(e) => {
log::error!("{}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
let stmt = match dbclient.prepare("select g.status::text, g.notes_html, g.fo_forum_url, g.is_teamed, (select count(*) from games.players p where p.game_uid = g.game_uid and p.client_type = 'p'), MIN(t.turn_ts), MAX(t.turn_ts), COUNT(t.turn) from games.games g left join games.turns t on t.game_uid = g.game_uid where g.game_uid = $1 group by g.game_uid;").await {
Ok(stmt) => stmt,
Err(e) => {
log::error!("{}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
let rows = match dbclient.query_opt(&stmt, &[&gameuid]).await {
Ok(rows) => rows,
Err(e) => {
log::error!("Pool RO query error {}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
let row = match rows {
Some(r) => r,
None => {
return HttpResponse::NotFound().body("Not found");
}
};
let jar = request_to_jar(request);
let user = jar
.private(&data.cookies_key)
.get("auth")
.map(|x| x.value().to_string());
let now = chrono::Utc::now();
let min_turn_ts = row
.get::<_, Option<chrono::NaiveDateTime>>(5)
.map(|x| naive_to_diff_utc(&x, now));
let max_turn_ts = row
.get::<_, Option<chrono::NaiveDateTime>>(6)
.map(|x| naive_to_diff_utc(&x, now));
let status = row.get::<_, Option<&str>>(0);
let (player_list, join_data, leave_data) = if let Some(ref user) = user {
let stmt = match dbclient.prepare("select q.player_name, q.player_name = $2, q.delegate_name, q.delegate_name = $2 from games.query_delegation q where (q.player_name = $2 or q.delegate_name = $2) and q.game_uid = $1;").await {
Ok(stmt) => stmt,
Err(e) => {
log::error!("{}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
let rows = match dbclient.query(&stmt, &[&gameuid, user]).await {
Ok(rows) => rows,
Err(e) => {
log::error!("{}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
let mut delegations_from = std::collections::HashSet::new();
let mut delegations_to = std::collections::HashSet::new();
for row in rows {
if row.get::<_, bool>(1) {
delegations_from.insert(row.get::<_, String>(2));
} else if row.get::<_, bool>(3) {
delegations_to.insert(row.get::<_, &str>(0).to_lowercase());
}
}
let stmt = match dbclient.prepare("select p.player_name, p.team_id, p.create_ts, p.delegate_name, p.player_name = $2, p.delegate_name = $2 from games.players p where p.game_uid = $1 and p.client_type = 'p';").await {
Ok(stmt) => stmt,
Err(e) => {
log::error!("{}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
let rows = match dbclient.query(&stmt, &[&gameuid, user]).await {
Ok(rows) => rows,
Err(e) => {
log::error!("{}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
let mut users = Vec::with_capacity(rows.len());
let mut has_itself = false;
for row in rows {
let player_name = row.get::<_, String>(0);
let team_id = row.get::<_, i32>(1);
let create_ts = row
.get::<_, Option<chrono::NaiveDateTime>>(2)
.as_ref()
.map(naive_to_text);
let delegate_name = row.get::<_, Option<String>>(3);
let player_itself = row.get::<_, bool>(4);
let delegate_itself = row.get::<_, Option<bool>>(5).unwrap_or(false);
let pending_delegates = if status.map_or(true, |x| x == "started")
&& player_itself
&& !delegations_from.is_empty()
{
let mut cache = data.cache_delegation_game.lock().await;
Some(
delegations_from
.iter()
.map(|x| {
let csrf = Uuid::new_v4();
cache.insert(
csrf,
(gameuid.clone(), user.to_lowercase(), x.to_lowercase()),
std::time::Duration::from_secs(data.cache_duration_sec),
);
PendingDelegateInfo {
csrf,
name: x.clone(),
accept: false,
}
})
.collect(),
)
} else if status.map_or(true, |x| x == "started")
&& delegations_to.contains(&player_name.to_lowercase())
{
let csrf = Uuid::new_v4();
let mut cache = data.cache_delegation_game.lock().await;
cache.insert(
csrf,
(
gameuid.clone(),
player_name.to_lowercase(),
user.to_lowercase(),
),
std::time::Duration::from_secs(data.cache_duration_sec),
);
Some(
[PendingDelegateInfo {
csrf,
name: user.clone(),
accept: true,
}]
.into_iter()
.collect(),
)
} else {
None
};
has_itself = has_itself || player_itself;
let revoke_delegate_data = if status.map_or(true, |x| x == "started") && delegate_itself
{
let csrf = Uuid::new_v4();
let mut cache = data.cache_revoke_delegation_game.lock().await;
cache.insert(
csrf,
(
gameuid.clone(),
player_name.to_lowercase(),
user.to_lowercase(),
),
std::time::Duration::from_secs(data.cache_duration_sec),
);
Some(JoinLeaveData { csrf })
} else if status.map_or(true, |x| x == "started") && player_itself {
if let Some(ref delegate_name) = delegate_name {
let csrf = Uuid::new_v4();
let mut cache = data.cache_revoke_delegation_game.lock().await;
cache.insert(
csrf,
(
gameuid.clone(),
user.to_lowercase(),
delegate_name.to_lowercase(),
),
std::time::Duration::from_secs(data.cache_duration_sec),
);
Some(JoinLeaveData { csrf })
} else {
None
}
} else {
None
};
let query_delegate_data = if status.map_or(true, |x| x == "started")
&& player_itself
&& delegate_name.is_none()
{
let csrf = Uuid::new_v4();
let mut cache = data.cache_query_delegation_game.lock().await;
cache.insert(
csrf,
(gameuid.clone(), user.to_lowercase()),
std::time::Duration::from_secs(data.cache_duration_sec),
);
Some(JoinLeaveData { csrf })
} else {
None
};
users.push(PlayerInfo {
player_name,
team_id,
delegate_name,
player_itself,
delegate_itself,
revoke_delegate_data,
query_delegate_data,
pending_delegates,
create_ts,
});
}
let (join_data, leave_data) = if !has_itself && status.is_none() {
let csrf = Uuid::new_v4();
let mut cache = data.cache_join_game.lock().await;
cache.insert(
csrf,
(gameuid.clone(), user.to_lowercase()),
std::time::Duration::from_secs(data.cache_duration_sec),
);
(Some(JoinLeaveData { csrf }), None)
} else if has_itself && status.is_none() {
let csrf = Uuid::new_v4();
let mut cache = data.cache_leave_game.lock().await;
cache.insert(
csrf,
(gameuid.clone(), user.to_lowercase()),
std::time::Duration::from_secs(data.cache_duration_sec),
);
(None, Some(JoinLeaveData { csrf }))
} else {
(None, None)
};
(Some(users), join_data, leave_data)
} else if status.is_none() {
let stmt = match dbclient.prepare("select p.player_name, p.team_id from games.players p where p.game_uid = $1 and p.client_type = 'p';").await {
Ok(stmt) => stmt,
Err(e) => {
log::error!("{}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
let rows = match dbclient.query(&stmt, &[&gameuid]).await {
Ok(rows) => rows,
Err(e) => {
log::error!("{}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
let mut users = Vec::with_capacity(rows.len());
for row in rows {
users.push(PlayerInfo {
player_name: row.get::<_, String>(0),
team_id: row.get::<_, i32>(1),
delegate_name: None,
player_itself: false,
delegate_itself: false,
revoke_delegate_data: None,
query_delegate_data: None,
pending_delegates: None,
create_ts: None,
});
}
(Some(users), None, None)
} else {
(None, None, None)
};
let body = match data.handlebars_xml.render(
SLOW_GAME,
&GameData {
common_auth_info: CommonAuthInfo {
user: user.map(Into::into),
},
gameuid: &gameuid,
gameuidenc: pct_str::PctString::encode(gameuid.chars(), pct_str::URIReserved)
.into_string(),
status,
notes_html: row.get::<_, Option<&str>>(1),
fo_forum_url: row.get::<_, Option<&str>>(2),
is_teamed: row.get::<_, bool>(3),
players: row.get::<_, i64>(4),
min_turn_ts: min_turn_ts.map(diff_utc_to_string),
max_turn_ts: max_turn_ts.map(diff_utc_to_string),
turns: row.get::<_, i64>(7),
player_list,
join_data,
leave_data,
},
) {
Ok(b) => b,
Err(e) => {
log::error!("{}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
insert_security_headers(HttpResponse::Ok()).body(body)
}
pub async fn post_join_game(
request: HttpRequest,
form: web::Form<JoinLeaveData>,
data: web::Data<WebData<'_>>,
data_rw: web::Data<DataBaseRw>,
) -> HttpResponse {
let cached_data = {
let mut cache = data.cache_join_game.lock().await;
cache.remove(&form.csrf)
};
let cached_data = if let Some(cd) = cached_data {
cd
} else {
log::warn!("Unknown data for CSRF: {}", form.csrf);
return HttpResponse::BadRequest().body("Incorrect");
};
let jar = request_to_jar(request);
if jar
.private(&data.cookies_key)
.get("auth")
.map_or(true, |x| x.value().to_lowercase() != cached_data.1)
{
log::warn!("Incorrect user");
return HttpResponse::BadRequest().body("Incorrect");
}
let dbclient_rw = match data_rw.0.get().await {
Ok(c) => c,
Err(e) => {
log::error!("Pool RW error {}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
let stmt = match dbclient_rw
.prepare("insert into games.players (game_uid, player_name, is_confirmed, species, client_type, create_ts) values ($1, $2, true, 'RANDOM', 'p', NOW());")
.await
{
Ok(stmt) => stmt,
Err(e) => {
log::error!("Pool RW statement error {}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
let inserted = match dbclient_rw
.execute(&stmt, &[&cached_data.0, &cached_data.1])
.await
{
Ok(c) => c,
Err(e) => {
log::error!("Pool RW execute insert error {}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
if inserted == 0 {
log::error!("Pool RW execute insert row error");
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
HttpResponse::Found()
.append_header((
header::LOCATION,
format!("slow-game-{}.html", cached_data.0),
))
.finish()
}
pub async fn post_leave_game(
request: HttpRequest,
form: web::Form<JoinLeaveData>,
data: web::Data<WebData<'_>>,
data_rw: web::Data<DataBaseRw>,
) -> HttpResponse {
let cached_data = {
let mut cache = data.cache_leave_game.lock().await;
cache.remove(&form.csrf)
};
let cached_data = if let Some(cd) = cached_data {
cd
} else {
log::warn!("Unknown data for CSRF: {}", form.csrf);
return HttpResponse::BadRequest().body("Incorrect");
};
let jar = request_to_jar(request);
if jar
.private(&data.cookies_key)
.get("auth")
.map_or(true, |x| x.value().to_lowercase() != cached_data.1)
{
log::warn!("Incorrect user");
return HttpResponse::BadRequest().body("Incorrect");
}
let dbclient_rw = match data_rw.0.get().await {
Ok(c) => c,
Err(e) => {
log::error!("Pool RW error {}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
let stmt = match dbclient_rw
.prepare("delete from games.players where game_uid = $1 and player_name = $2;")
.await
{
Ok(stmt) => stmt,
Err(e) => {
log::error!("Pool RW statement error {}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
let inserted = match dbclient_rw
.execute(&stmt, &[&cached_data.0, &cached_data.1])
.await
{
Ok(c) => c,
Err(e) => {
log::error!("Pool RW execute insert error {}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
if inserted == 0 {
log::error!("Pool RW execute insert row error");
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
HttpResponse::Found()
.append_header((
header::LOCATION,
format!("slow-game-{}.html", cached_data.0),
))
.finish()
}
pub async fn post_revoke_delegate(
request: HttpRequest,
form: web::Form<JoinLeaveData>,
data: web::Data<WebData<'_>>,
data_rw: web::Data<DataBaseRw>,
) -> HttpResponse {
let cached_data = {
let mut cache = data.cache_revoke_delegation_game.lock().await;
cache.remove(&form.csrf)
};
let cached_data = if let Some(cd) = cached_data {
cd
} else {
log::warn!("Unknown data for CSRF: {}", form.csrf);
return HttpResponse::BadRequest().body("Incorrect");
};
let jar = request_to_jar(request);
if jar
.private(&data.cookies_key)
.get("auth")
.map(|x| x.value().to_lowercase())
.map_or(true, |x| x != cached_data.1 && x != cached_data.2)
{
log::warn!("Incorrect user");
return HttpResponse::BadRequest().body("Incorrect");
}
let dbclient_rw = match data_rw.0.get().await {
Ok(c) => c,
Err(e) => {
log::error!("Pool RW error {}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
let stmt = match dbclient_rw
.prepare("update games.players set delegate_name = null where game_uid = $1 and player_name = $2 and delegate_name = $3;")
.await
{
Ok(stmt) => stmt,
Err(e) => {
log::error!("Pool RW statement error {}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
let inserted = match dbclient_rw
.execute(&stmt, &[&cached_data.0, &cached_data.1, &cached_data.2])
.await
{
Ok(c) => c,
Err(e) => {
log::error!("Pool RW execute insert error {}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
if inserted == 0 {
log::error!("Pool RW execute insert row error");
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
HttpResponse::Found()
.append_header((
header::LOCATION,
format!("slow-game-{}.html", cached_data.0),
))
.finish()
}
pub async fn post_query_delegate(
request: HttpRequest,
form: web::Form<QueryDelegationData>,
data: web::Data<WebData<'_>>,
data_rw: web::Data<DataBaseRw>,
) -> HttpResponse {
let cached_data = {
let mut cache = data.cache_query_delegation_game.lock().await;
cache.remove(&form.csrf)
};
let cached_data = if let Some(cd) = cached_data {
cd
} else {
log::warn!("Unknown data for CSRF: {}", form.csrf);
return HttpResponse::BadRequest().body("Incorrect");
};
let jar = request_to_jar(request);
if jar
.private(&data.cookies_key)
.get("auth")
.map_or(true, |x| x.value().to_lowercase() != cached_data.1)
{
log::warn!("Incorrect user");
return HttpResponse::BadRequest().body("Incorrect");
}
let dbclient_rw = match data_rw.0.get().await {
Ok(c) => c,
Err(e) => {
log::error!("Pool RW error {}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
let stmt = match dbclient_rw
.prepare("insert into games.query_delegation (game_uid, player_name, delegate_name) values ($1, $2, $3) on conflict do nothing;")
.await
{
Ok(stmt) => stmt,
Err(e) => {
log::error!("Pool RW statement error {}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
match dbclient_rw
.execute(
&stmt,
&[&cached_data.0, &cached_data.1, &form.delegate_name],
)
.await
{
Ok(_) => (),
Err(e) => {
log::error!("Pool RW execute insert error {}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
HttpResponse::Found()
.append_header((
header::LOCATION,
format!("slow-game-{}.html", cached_data.0),
))
.finish()
}
pub async fn post_delegate(
request: HttpRequest,
form: web::Form<DelegationData>,
data: web::Data<WebData<'_>>,
data_rw: web::Data<DataBaseRw>,
) -> HttpResponse {
let cached_data = {
let mut cache = data.cache_delegation_game.lock().await;
cache.remove(&form.csrf)
};
let cached_data = if let Some(cd) = cached_data {
cd
} else {
log::warn!("Unknown data for CSRF: {}", form.csrf);
return HttpResponse::BadRequest().body("Incorrect");
};
let jar = request_to_jar(request);
let user = if let Some(u) = jar
.private(&data.cookies_key)
.get("auth")
.map(|x| x.value().to_lowercase())
{
u
} else {
log::warn!("Incorrect user");
return HttpResponse::BadRequest().body("Incorrect");
};
if form.submit == "Accept Delegate" && user == cached_data.2 {
let dbclient_rw = match data_rw.0.get().await {
Ok(c) => c,
Err(e) => {
log::error!("Pool RW error {}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
match dbclient_rw
.execute(
"delete from games.query_delegation where game_uid = $1 and player_name = $2;",
&[&cached_data.0, &cached_data.1],
)
.await
{
Ok(_) => (),
Err(e) => {
log::error!("Pool RW statement error {}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
match dbclient_rw
.execute("update games.players set delegate_name = $3 where game_uid = $1 and player_name = $2;", &[&cached_data.0, &cached_data.1, &cached_data.2])
.await
{
Ok(_) => (),
Err(e) => {
log::error!("Pool RW statement error {}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
} else if form.submit == "Reject Delegate" && (user == cached_data.1 || user == cached_data.2) {
let dbclient_rw = match data_rw.0.get().await {
Ok(c) => c,
Err(e) => {
log::error!("Pool RW error {}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
let stmt = match dbclient_rw
.prepare("delete from games.query_delegation where game_uid = $1 and player_name = $2 and delegate_name = $3;")
.await
{
Ok(stmt) => stmt,
Err(e) => {
log::error!("Pool RW statement error {}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
match dbclient_rw
.execute(&stmt, &[&cached_data.0, &cached_data.1, &cached_data.2])
.await
{
Ok(_) => (),
Err(e) => {
log::error!("Pool RW execute insert error {}", e);
return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
}
};
} else {
log::warn!("Incorrect user submit {}", form.submit);
return HttpResponse::BadRequest().body("Incorrect");
}
HttpResponse::Found()
.append_header((
header::LOCATION,
format!("slow-game-{}.html", cached_data.0),
))
.finish()
}