holy cow it works
This commit is contained in:
parent
c3cf86f6e9
commit
15ad3dfb94
28 changed files with 490 additions and 158 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1 +1 @@
|
||||||
.env.local
|
.env
|
||||||
|
|
|
@ -3,7 +3,9 @@
|
||||||
<option name="buildProfileId" value="dev" />
|
<option name="buildProfileId" value="dev" />
|
||||||
<option name="command" value="run" />
|
<option name="command" value="run" />
|
||||||
<option name="workingDirectory" value="file://$PROJECT_DIR$/holycow_backend" />
|
<option name="workingDirectory" value="file://$PROJECT_DIR$/holycow_backend" />
|
||||||
<envs />
|
<envs>
|
||||||
|
<env name="RUST_LOG" value="trace" />
|
||||||
|
</envs>
|
||||||
<option name="emulateTerminal" value="true" />
|
<option name="emulateTerminal" value="true" />
|
||||||
<option name="channel" value="DEFAULT" />
|
<option name="channel" value="DEFAULT" />
|
||||||
<option name="requiredFeatures" value="true" />
|
<option name="requiredFeatures" value="true" />
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
:30002 {
|
:30002 {
|
||||||
reverse_proxy http://localhost:30000
|
reverse_proxy http://localhost:30000
|
||||||
reverse_proxy /api http://localhost:30001
|
reverse_proxy /api/* http://localhost:30001
|
||||||
|
reverse_proxy /telegram/webhook http://localhost:30001
|
||||||
}
|
}
|
||||||
|
|
1
holycow_backend/Cargo.lock
generated
1
holycow_backend/Cargo.lock
generated
|
@ -664,6 +664,7 @@ dependencies = [
|
||||||
"micronfig",
|
"micronfig",
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"skillratings",
|
"skillratings",
|
||||||
"teloxide",
|
"teloxide",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
|
@ -17,3 +17,4 @@ tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread", "net"] }
|
||||||
serde = { version = "1.0.215", features = ["derive"] }
|
serde = { version = "1.0.215", features = ["derive"] }
|
||||||
chrono = { version = "0.4.38", features = ["serde"] }
|
chrono = { version = "0.4.38", features = ["serde"] }
|
||||||
url = { version = "2.5.4", features = ["serde"] }
|
url = { version = "2.5.4", features = ["serde"] }
|
||||||
|
serde_json = "1.0.133"
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE players DROP COLUMN IF EXISTS username;
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE players ADD COLUMN username BPCHAR UNIQUE NOT NULL;
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE matches ADD CONSTRAINT match_unique_name UNIQUE (name);
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE matches DROP CONSTRAINT match_unique_name;
|
|
@ -3,4 +3,6 @@ micronfig::config! {
|
||||||
BACKEND_BIND_ADDRESS: String > std::net::SocketAddr,
|
BACKEND_BIND_ADDRESS: String > std::net::SocketAddr,
|
||||||
TELEGRAM_API_KEY: String,
|
TELEGRAM_API_KEY: String,
|
||||||
TELEGRAM_WEBHOOK_URL: String > url::Url,
|
TELEGRAM_WEBHOOK_URL: String > url::Url,
|
||||||
|
TELEGRAM_NOTIFICATION_CHAT_ID: String > i64,
|
||||||
|
TELEGRAM_NOTIFICATION_TOPIC_ID?: String > i32,
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ use diesel::backend::Backend;
|
||||||
use diesel::deserialize::FromSql;
|
use diesel::deserialize::FromSql;
|
||||||
use diesel::dsl::insert_into;
|
use diesel::dsl::insert_into;
|
||||||
use diesel::pg::Pg;
|
use diesel::pg::Pg;
|
||||||
use diesel::serialize::ToSql;
|
use diesel::serialize::{IsNull, ToSql};
|
||||||
use diesel::sql_types as sql;
|
use diesel::sql_types as sql;
|
||||||
use diesel::serialize::Output as DieselOutput;
|
use diesel::serialize::Output as DieselOutput;
|
||||||
use diesel::ExpressionMethods;
|
use diesel::ExpressionMethods;
|
||||||
|
@ -32,6 +32,7 @@ pub struct Player {
|
||||||
pub wenglin: WengLinRating,
|
pub wenglin: WengLinRating,
|
||||||
pub telegram_id: Option<TelegramId>,
|
pub telegram_id: Option<TelegramId>,
|
||||||
pub competitive: bool,
|
pub competitive: bool,
|
||||||
|
pub username: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Insertable, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Insertable, Serialize, Deserialize)]
|
||||||
|
@ -39,8 +40,9 @@ pub struct Player {
|
||||||
#[diesel(check_for_backend(Pg))]
|
#[diesel(check_for_backend(Pg))]
|
||||||
pub struct PlayerI {
|
pub struct PlayerI {
|
||||||
pub wenglin: WengLinRating,
|
pub wenglin: WengLinRating,
|
||||||
pub telegram_id: TelegramId,
|
pub telegram_id: Option<TelegramId>,
|
||||||
pub competitive: bool,
|
pub competitive: bool,
|
||||||
|
pub username: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromSqlRow, AsExpression, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromSqlRow, AsExpression, Serialize, Deserialize)]
|
||||||
|
@ -71,7 +73,6 @@ pub struct Match {
|
||||||
#[diesel(table_name = schema::matches)]
|
#[diesel(table_name = schema::matches)]
|
||||||
#[diesel(check_for_backend(Pg))]
|
#[diesel(check_for_backend(Pg))]
|
||||||
pub struct MatchI {
|
pub struct MatchI {
|
||||||
pub instant: DateTime<Utc>,
|
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub player_a_id: i32,
|
pub player_a_id: i32,
|
||||||
pub player_a_wenglin_before: WengLinRating,
|
pub player_a_wenglin_before: WengLinRating,
|
||||||
|
@ -92,8 +93,7 @@ impl FromSql<sql::BigInt, Pg> for TelegramId {
|
||||||
|
|
||||||
impl FromSql<schema::sql_types::WenglinT, Pg> for WengLinRating {
|
impl FromSql<schema::sql_types::WenglinT, Pg> for WengLinRating {
|
||||||
fn from_sql(bytes: <Pg as Backend>::RawValue<'_>) -> diesel::deserialize::Result<Self> {
|
fn from_sql(bytes: <Pg as Backend>::RawValue<'_>) -> diesel::deserialize::Result<Self> {
|
||||||
let rating = <f64 as FromSql<sql::Double, Pg>>::from_sql(bytes)?;
|
let (rating, uncertainty) = <(f64, f64) as FromSql<sql::Record<(sql::Double, sql::Double)>, Pg>>::from_sql(bytes)?;
|
||||||
let uncertainty = <f64 as FromSql<sql::Double, Pg>>::from_sql(bytes)?;
|
|
||||||
|
|
||||||
let rating = skillratings::weng_lin::WengLinRating::from((rating, uncertainty));
|
let rating = skillratings::weng_lin::WengLinRating::from((rating, uncertainty));
|
||||||
Ok(Self(rating))
|
Ok(Self(rating))
|
||||||
|
@ -121,10 +121,10 @@ impl ToSql<sql::BigInt, Pg> for TelegramId {
|
||||||
|
|
||||||
impl ToSql<schema::sql_types::WenglinT, Pg> for WengLinRating {
|
impl ToSql<schema::sql_types::WenglinT, Pg> for WengLinRating {
|
||||||
fn to_sql<'b>(&'b self, out: &mut DieselOutput<'b, '_, Pg>) -> diesel::serialize::Result {
|
fn to_sql<'b>(&'b self, out: &mut DieselOutput<'b, '_, Pg>) -> diesel::serialize::Result {
|
||||||
<f64 as ToSql<sql::Double, Pg>>::to_sql(&self.0.rating, out)?;
|
diesel::serialize::WriteTuple::<(sql::Double, sql::Double)>::write_tuple(
|
||||||
<f64 as ToSql<sql::Double, Pg>>::to_sql(&self.0.uncertainty, out)?;
|
&(self.0.rating, self.0.uncertainty),
|
||||||
|
out
|
||||||
Ok(diesel::serialize::IsNull::No)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,24 +138,54 @@ impl ToSql<schema::sql_types::OutcomeT, Pg> for Outcome {
|
||||||
}
|
}
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(diesel::serialize::IsNull::No)
|
Ok(IsNull::No)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Outcome> for skillratings::Outcomes {
|
||||||
|
fn from(value: Outcome) -> Self {
|
||||||
|
match value {
|
||||||
|
Outcome::AWins => Self::WIN,
|
||||||
|
Outcome::BWins => Self::LOSS,
|
||||||
|
Outcome::Tie => Self::DRAW,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WengLinRating {
|
||||||
|
pub fn human_score(&self) -> i64 {
|
||||||
|
let rating = self.0.rating;
|
||||||
|
let uncertainty = self.0.uncertainty;
|
||||||
|
log::debug!("Getting human score for: {rating:?}±{uncertainty:?}");
|
||||||
|
let uncertain = self.0.rating - self.0.uncertainty;
|
||||||
|
log::trace!("Minimum score is: {uncertain:?}");
|
||||||
|
let multiplied = uncertain * 100.0;
|
||||||
|
log::trace!("Multiplied score is: {multiplied:?}");
|
||||||
|
let floored: f64 = multiplied.floor();
|
||||||
|
log::trace!("Floored score is: {floored:?}");
|
||||||
|
let converted: i64 = floored as i64;
|
||||||
|
log::debug!("Human score for {rating:?}±{uncertainty:?} is {converted:?}");
|
||||||
|
converted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Player {
|
impl Player {
|
||||||
pub fn total(conn: &mut PgConnection) -> QueryResult<i64> {
|
pub fn total(conn: &mut PgConnection) -> QueryResult<i64> {
|
||||||
|
log::debug!("Querying total amount of players...");
|
||||||
schema::players::table
|
schema::players::table
|
||||||
.select(diesel::dsl::count_star())
|
.select(diesel::dsl::count_star())
|
||||||
.get_result::<i64>(conn)
|
.get_result::<i64>(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn all(conn: &mut PgConnection) -> QueryResult<Vec<Self>> {
|
pub fn all(conn: &mut PgConnection) -> QueryResult<Vec<Self>> {
|
||||||
|
log::debug!("Querying all players...");
|
||||||
schema::players::table
|
schema::players::table
|
||||||
.select(Self::as_select())
|
.select(Self::as_select())
|
||||||
.get_results::<Self>(conn)
|
.get_results::<Self>(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_by_id(conn: &mut PgConnection, player_id: i32) -> QueryResult<Option<Self>> {
|
pub fn get_by_id(conn: &mut PgConnection, player_id: i32) -> QueryResult<Option<Self>> {
|
||||||
|
log::debug!("Querying player with id: {player_id:?}");
|
||||||
schema::players::table
|
schema::players::table
|
||||||
.select(Self::as_select())
|
.select(Self::as_select())
|
||||||
.filter(schema::players::id.eq(player_id))
|
.filter(schema::players::id.eq(player_id))
|
||||||
|
@ -164,6 +194,7 @@ impl Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_by_telegram_id(conn: &mut PgConnection, telegram_id: TelegramId) -> QueryResult<Option<Self>> {
|
pub fn get_by_telegram_id(conn: &mut PgConnection, telegram_id: TelegramId) -> QueryResult<Option<Self>> {
|
||||||
|
log::debug!("Querying player with telegram id: {telegram_id:?}");
|
||||||
schema::players::table
|
schema::players::table
|
||||||
.select(Self::as_select())
|
.select(Self::as_select())
|
||||||
.filter(schema::players::telegram_id.eq(telegram_id))
|
.filter(schema::players::telegram_id.eq(telegram_id))
|
||||||
|
@ -178,6 +209,12 @@ impl Player {
|
||||||
pub fn won_count(&self, conn: &mut PgConnection) -> QueryResult<i64> {
|
pub fn won_count(&self, conn: &mut PgConnection) -> QueryResult<i64> {
|
||||||
Match::won_by_count(conn, self.id)
|
Match::won_by_count(conn, self.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_wenglin(self, conn: &mut PgConnection, value: &WengLinRating) -> QueryResult<Self> {
|
||||||
|
diesel::update(schema::players::table.find(self.id))
|
||||||
|
.set(schema::players::wenglin.eq(value))
|
||||||
|
.get_result(conn)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Match {
|
impl Match {
|
||||||
|
@ -214,9 +251,9 @@ impl Match {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlayerI {
|
impl PlayerI {
|
||||||
pub fn insert(self, conn: &mut PgConnection) -> QueryResult<Player> {
|
pub fn insert(&self, conn: &mut PgConnection) -> QueryResult<Player> {
|
||||||
insert_into(schema::players::table)
|
insert_into(schema::players::table)
|
||||||
.values(&[self])
|
.values(self)
|
||||||
.get_result::<Player>(conn)
|
.get_result::<Player>(conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -224,7 +261,7 @@ impl PlayerI {
|
||||||
impl MatchI {
|
impl MatchI {
|
||||||
pub fn insert(self, conn: &mut PgConnection) -> QueryResult<Match> {
|
pub fn insert(self, conn: &mut PgConnection) -> QueryResult<Match> {
|
||||||
insert_into(schema::matches::table)
|
insert_into(schema::matches::table)
|
||||||
.values(&[self])
|
.values(self)
|
||||||
.get_result::<Match>(conn)
|
.get_result::<Match>(conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@ diesel::table! {
|
||||||
wenglin -> WenglinT,
|
wenglin -> WenglinT,
|
||||||
telegram_id -> Nullable<Int8>,
|
telegram_id -> Nullable<Int8>,
|
||||||
competitive -> Bool,
|
competitive -> Bool,
|
||||||
|
username -> Bpchar,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
use axum::Extension;
|
||||||
use diesel::{Connection, PgConnection};
|
use diesel::{Connection, PgConnection};
|
||||||
use diesel_migrations::MigrationHarness;
|
use diesel_migrations::MigrationHarness;
|
||||||
use teloxide::dispatching::{DefaultKey, MessageFilterExt};
|
use teloxide::dispatching::DefaultKey;
|
||||||
use teloxide::error_handlers::LoggingErrorHandler;
|
use teloxide::error_handlers::LoggingErrorHandler;
|
||||||
use teloxide::types::Message;
|
|
||||||
use teloxide::update_listeners::webhooks::Options;
|
use teloxide::update_listeners::webhooks::Options;
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
|
@ -72,19 +72,22 @@ async fn main() -> anyhow::Result<Infallible> {
|
||||||
.route("/api/matches/",
|
.route("/api/matches/",
|
||||||
axum::routing::get(routes::matches::get_all)
|
axum::routing::get(routes::matches::get_all)
|
||||||
)
|
)
|
||||||
|
.route("/api/matches/",
|
||||||
|
axum::routing::post(routes::matches::post_match)
|
||||||
|
)
|
||||||
.nest("/telegram/webhook",
|
.nest("/telegram/webhook",
|
||||||
telegram_router
|
telegram_router
|
||||||
)
|
)
|
||||||
|
.layer(Extension(bot.clone()))
|
||||||
;
|
;
|
||||||
|
|
||||||
log::trace!("Setting up Telegram dispatcher...");
|
log::trace!("Setting up Telegram dispatcher...");
|
||||||
let mut telegram_dispatcher = teloxide::dispatching::Dispatcher::<teloxide::Bot, anyhow::Error, DefaultKey>::builder(
|
let mut telegram_dispatcher = teloxide::dispatching::Dispatcher::<teloxide::Bot, anyhow::Error, DefaultKey>::builder(
|
||||||
bot.clone(),
|
bot.clone(),
|
||||||
Message::filter_web_app_data()
|
teloxide::dptree::entry()
|
||||||
.endpoint(telegram::webapp::process_data)
|
|
||||||
)
|
)
|
||||||
.default_handler(|u| async move {
|
.default_handler(|u| async move {
|
||||||
log::trace!("Unhandled update: {u:#?}")
|
log::trace!("Unhandled update: {u:?}")
|
||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::Json;
|
use axum::{Extension, Json};
|
||||||
use diesel::{Connection, PgConnection};
|
use diesel::{Connection, PgConnection};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use skillratings::weng_lin::WengLinConfig;
|
||||||
|
use teloxide::Bot;
|
||||||
|
use teloxide::requests::Requester;
|
||||||
|
use teloxide::types::{ChatId, MessageId, ThreadId};
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use crate::database::model::Match;
|
use crate::database::model::{Match, MatchI, Outcome, Player, WengLinRating};
|
||||||
|
|
||||||
#[axum::debug_handler]
|
#[axum::debug_handler]
|
||||||
pub async fn get_all()
|
pub async fn get_all()
|
||||||
|
@ -16,3 +21,136 @@ pub async fn get_all()
|
||||||
|
|
||||||
Ok(Json(matches))
|
Ok(Json(matches))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct MatchII {
|
||||||
|
name: Option<String>,
|
||||||
|
player_a: i32,
|
||||||
|
player_b: i32,
|
||||||
|
outcome: Outcome,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn player_to_text(player: &Player, before: &WengLinRating, after: &WengLinRating) -> String {
|
||||||
|
let name = &player.username;
|
||||||
|
let competitive = &player.competitive;
|
||||||
|
|
||||||
|
match competitive {
|
||||||
|
false => {
|
||||||
|
format!("{name}")
|
||||||
|
},
|
||||||
|
true => {
|
||||||
|
let before = before.human_score();
|
||||||
|
let after = after.human_score();
|
||||||
|
let change = after - before;
|
||||||
|
|
||||||
|
format!("<b>{name}</b> <i>({change})</i>")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_to_text(r#match: &Match, player_a: &Player, player_b: &Player) -> String {
|
||||||
|
let player_a = player_to_text(player_a, &r#match.player_a_wenglin_before, &r#match.player_a_wenglin_after);
|
||||||
|
let player_b = player_to_text(player_b, &r#match.player_b_wenglin_before, &r#match.player_b_wenglin_after);
|
||||||
|
|
||||||
|
match r#match.outcome {
|
||||||
|
Outcome::AWins => match &r#match.name {
|
||||||
|
Some(name) => format!("🔵 {player_a} ha trionfato su {player_b} in <b>{name}</b>!"),
|
||||||
|
None => format!("🔵 {player_a} ha trionfato su {player_b}!"),
|
||||||
|
},
|
||||||
|
Outcome::BWins => match &r#match.name {
|
||||||
|
Some(name) => format!("⚪️ {player_a} è stato sconfitto da {player_b} in <b>{name}</b>!"),
|
||||||
|
None => format!("⚪️ {player_a} è stato sconfitto da {player_b}!"),
|
||||||
|
},
|
||||||
|
Outcome::Tie => match &r#match.name {
|
||||||
|
Some(name) => format!("🟠 {player_a} e {player_b} hanno pareggiato in <b>{name}</b>!"),
|
||||||
|
None => format!("🟠 {player_a} e {player_b} hanno pareggiato!"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn post_match(
|
||||||
|
Extension(bot): Extension<Bot>,
|
||||||
|
Json(matchii): Json<MatchII>,
|
||||||
|
)
|
||||||
|
-> Result<Json<Match>, StatusCode>
|
||||||
|
{
|
||||||
|
log::debug!("New MatchII just dropped: {matchii:#?}");
|
||||||
|
let name = matchii.name;
|
||||||
|
let outcome = matchii.outcome;
|
||||||
|
|
||||||
|
log::trace!("Establishing database connection...");
|
||||||
|
let mut conn = PgConnection::establish(config::DATABASE_URL())
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
log::trace!("Finding player A's info...");
|
||||||
|
let player_a = Player::get_by_id(&mut conn, matchii.player_a)
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||||
|
.ok_or_else(|| StatusCode::NOT_FOUND)?;
|
||||||
|
let player_a_id = player_a.id;
|
||||||
|
let player_a_wenglin_before = player_a.wenglin.clone();
|
||||||
|
|
||||||
|
log::trace!("Finding player B's info...");
|
||||||
|
let player_b = Player::get_by_id(&mut conn, matchii.player_b)
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||||
|
.ok_or_else(|| StatusCode::NOT_FOUND)?;
|
||||||
|
let player_b_id = player_b.id;
|
||||||
|
let player_b_wenglin_before = player_b.wenglin.clone();
|
||||||
|
|
||||||
|
log::trace!("Calculating rating changes...");
|
||||||
|
let (player_a_wenglin_after, player_b_wenglin_after) = skillratings::weng_lin::weng_lin(
|
||||||
|
&player_a_wenglin_before.0,
|
||||||
|
&player_b_wenglin_before.0,
|
||||||
|
&outcome.into(),
|
||||||
|
&WengLinConfig::default(),
|
||||||
|
);
|
||||||
|
let player_a_wenglin_after = WengLinRating(player_a_wenglin_after);
|
||||||
|
log::trace!("A's new rating is: {player_a_wenglin_after:?}");
|
||||||
|
let player_b_wenglin_after = WengLinRating(player_b_wenglin_after);
|
||||||
|
log::trace!("B's new rating is: {player_b_wenglin_after:?}");
|
||||||
|
|
||||||
|
log::trace!("Starting database transaction...");
|
||||||
|
let (r#match, player_a, player_b) = conn.transaction(|tx| {
|
||||||
|
log::trace!("Updating A's rating...");
|
||||||
|
let player_a = player_a.update_wenglin(tx, &player_a_wenglin_after)
|
||||||
|
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
|
||||||
|
log::trace!("Updating B's rating...");
|
||||||
|
let player_b = player_b.update_wenglin(tx, &player_b_wenglin_after)
|
||||||
|
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
|
||||||
|
|
||||||
|
log::trace!("Inserting match...");
|
||||||
|
let matchi = MatchI {
|
||||||
|
name,
|
||||||
|
player_a_id,
|
||||||
|
player_a_wenglin_before,
|
||||||
|
player_a_wenglin_after,
|
||||||
|
player_b_id,
|
||||||
|
player_b_wenglin_before,
|
||||||
|
player_b_wenglin_after,
|
||||||
|
outcome,
|
||||||
|
};
|
||||||
|
let r#match = matchi.insert(tx)
|
||||||
|
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
|
||||||
|
|
||||||
|
Ok::<(Match, Player, Player), anyhow::Error>((r#match, player_a, player_b))
|
||||||
|
})
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
log::trace!("Preparing send message future...");
|
||||||
|
|
||||||
|
let chat = config::TELEGRAM_NOTIFICATION_CHAT_ID();
|
||||||
|
let chat = ChatId(*chat);
|
||||||
|
let mut send_message_future = bot.send_message(chat, match_to_text(&r#match, &player_a, &player_b));
|
||||||
|
|
||||||
|
let topic = config::TELEGRAM_NOTIFICATION_TOPIC_ID();
|
||||||
|
if let Some(topic) = topic {
|
||||||
|
let topic = MessageId(*topic);
|
||||||
|
let topic = ThreadId(topic);
|
||||||
|
send_message_future.message_thread_id = Some(topic);
|
||||||
|
}
|
||||||
|
|
||||||
|
log::trace!("Sending message...");
|
||||||
|
let _message = send_message_future.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
Ok(Json(r#match))
|
||||||
|
}
|
||||||
|
|
|
@ -2,14 +2,37 @@ use axum::extract::Path;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
use diesel::{Connection, PgConnection};
|
use diesel::{Connection, PgConnection};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use model::Player;
|
use model::Player;
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use crate::database::model;
|
use crate::database::model;
|
||||||
use crate::database::model::TelegramId;
|
use crate::database::model::TelegramId;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct PlayerO {
|
||||||
|
id: i32,
|
||||||
|
telegram_id: Option<TelegramId>,
|
||||||
|
username: String,
|
||||||
|
human_score: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Player> for PlayerO {
|
||||||
|
fn from(value: Player) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.id,
|
||||||
|
telegram_id: value.telegram_id,
|
||||||
|
username: value.username,
|
||||||
|
human_score: match value.competitive {
|
||||||
|
true => Some(value.wenglin.human_score()),
|
||||||
|
false => None
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[axum::debug_handler]
|
#[axum::debug_handler]
|
||||||
pub async fn get_all()
|
pub async fn get_all()
|
||||||
-> Result<Json<Vec<Player>>, StatusCode>
|
-> Result<Json<Vec<PlayerO>>, StatusCode>
|
||||||
{
|
{
|
||||||
let mut conn = PgConnection::establish(config::DATABASE_URL())
|
let mut conn = PgConnection::establish(config::DATABASE_URL())
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
@ -17,14 +40,14 @@ pub async fn get_all()
|
||||||
let players = Player::all(&mut conn)
|
let players = Player::all(&mut conn)
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
Ok(Json(players))
|
Ok(Json(players.into_iter().map(Into::into).collect()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[axum::debug_handler]
|
#[axum::debug_handler]
|
||||||
pub async fn get_by_id(
|
pub async fn get_by_id(
|
||||||
Path(player_id): Path<i32>,
|
Path(player_id): Path<i32>,
|
||||||
)
|
)
|
||||||
-> Result<Json<Player>, StatusCode>
|
-> Result<Json<PlayerO>, StatusCode>
|
||||||
{
|
{
|
||||||
let mut conn = PgConnection::establish(config::DATABASE_URL())
|
let mut conn = PgConnection::establish(config::DATABASE_URL())
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
@ -33,14 +56,14 @@ pub async fn get_by_id(
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||||
.ok_or(StatusCode::NOT_FOUND)?;
|
.ok_or(StatusCode::NOT_FOUND)?;
|
||||||
|
|
||||||
Ok(Json(player))
|
Ok(Json(player.into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[axum::debug_handler]
|
#[axum::debug_handler]
|
||||||
pub async fn get_by_telegram_id(
|
pub async fn get_by_telegram_id(
|
||||||
Path(telegram_id): Path<TelegramId>,
|
Path(telegram_id): Path<TelegramId>,
|
||||||
)
|
)
|
||||||
-> Result<Json<Player>, StatusCode>
|
-> Result<Json<PlayerO>, StatusCode>
|
||||||
{
|
{
|
||||||
let mut conn = PgConnection::establish(config::DATABASE_URL())
|
let mut conn = PgConnection::establish(config::DATABASE_URL())
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
@ -49,5 +72,5 @@ pub async fn get_by_telegram_id(
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||||
.ok_or(StatusCode::NOT_FOUND)?;
|
.ok_or(StatusCode::NOT_FOUND)?;
|
||||||
|
|
||||||
Ok(Json(player))
|
Ok(Json(player.into()))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
pub mod webapp;
|
|
|
@ -1,8 +0,0 @@
|
||||||
use teloxide::types::WebAppData;
|
|
||||||
|
|
||||||
pub async fn process_data(
|
|
||||||
web_app_data: WebAppData,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
log::trace!("{web_app_data:#?}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,7 +1,5 @@
|
||||||
import {NextConfig} from "next"
|
import {NextConfig} from "next"
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {};
|
||||||
output: "export",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
3
holycow_frontend/src/app/[telegramId]/profile/page.tsx
Normal file
3
holycow_frontend/src/app/[telegramId]/profile/page.tsx
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export default async function Page() {
|
||||||
|
|
||||||
|
}
|
16
holycow_frontend/src/app/[telegramId]/report/page.tsx
Normal file
16
holycow_frontend/src/app/[telegramId]/report/page.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import {ReportBox} from "@/components/ReportBox"
|
||||||
|
import {ReportBoxInteractive} from "@/components/ReportBoxInteractive"
|
||||||
|
import {PlayerO} from "@/holycow"
|
||||||
|
|
||||||
|
export default async function Page({params: {telegramId}}) {
|
||||||
|
const playersResponse = await fetch(`${process.env.BASE_URL}/api/results/`)
|
||||||
|
const players: PlayerO[] = await playersResponse.json()
|
||||||
|
const playerA: PlayerO = players.find(p => p.telegram_id == telegramId)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReportBoxInteractive
|
||||||
|
players={players}
|
||||||
|
playerA={playerA}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,18 +1,38 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useTelegram } from "@/components/useTelegram";
|
import {LoadingBox} from "@/components/LoadingBox"
|
||||||
import {useEffect, useMemo} from "react"
|
import {useTelegram} from "@/components/useTelegram"
|
||||||
|
import {useEffect} from "react"
|
||||||
|
import {useRouter} from "next/navigation"
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
|
const router = useRouter()
|
||||||
const telegram = useTelegram()
|
const telegram = useTelegram()
|
||||||
const telegramData = telegram?.WebApp?.initDataUnsafe
|
const data = telegram?.WebApp?.initDataUnsafe
|
||||||
const userId = telegramData?.user?.id
|
const startParam = data?.start_param
|
||||||
const userName = telegramData?.user?.first_name
|
|
||||||
const startParam = telegramData?.start_param
|
|
||||||
|
|
||||||
const resultsData = undefined
|
useEffect(
|
||||||
const resultsError = undefined
|
() => {
|
||||||
|
switch(startParam) {
|
||||||
return <main>
|
case undefined:
|
||||||
</main>
|
return
|
||||||
|
case "profile":
|
||||||
|
router.replace(`/${data.user.id}/profile`)
|
||||||
|
return
|
||||||
|
case "report":
|
||||||
|
router.replace(`/${data.user.id}/report`)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
router.replace(`/error404`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[startParam]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LoadingBox>
|
||||||
|
Connecting to Telegram...
|
||||||
|
</LoadingBox>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,107 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useTelegram } from "@/components/useTelegram";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import {FormEvent, useCallback, useMemo, useState} from "react"
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
const telegram = useTelegram()
|
|
||||||
const telegramData = telegram?.WebApp?.initDataUnsafe
|
|
||||||
const userId = telegramData?.user?.id
|
|
||||||
const userName = telegramData?.user?.first_name ?? "???"
|
|
||||||
|
|
||||||
const [opponent, setOpponent] = useState("")
|
|
||||||
const [result, setResult] = useState(null)
|
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
|
||||||
(e: FormEvent) => {
|
|
||||||
telegram?.WebApp?.sendData?.(`${result} ${opponent}`)
|
|
||||||
},
|
|
||||||
[telegram, result, opponent]
|
|
||||||
)
|
|
||||||
|
|
||||||
const contents = useMemo(() => {
|
|
||||||
return (
|
|
||||||
<div className={"chapter-1"}>
|
|
||||||
<form
|
|
||||||
className={classNames({
|
|
||||||
"panel": true,
|
|
||||||
"box": true,
|
|
||||||
"form-flex": true,
|
|
||||||
"red": result === "L",
|
|
||||||
"yellow": result === "T",
|
|
||||||
"green": result === "W",
|
|
||||||
})}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
>
|
|
||||||
<h3>
|
|
||||||
Registra risultato
|
|
||||||
</h3>
|
|
||||||
<label>
|
|
||||||
<span>Tu</span>
|
|
||||||
<div>
|
|
||||||
{userName}
|
|
||||||
</div>
|
|
||||||
<span></span>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>Avversario</span>
|
|
||||||
<select onChange={(e) => setOpponent(e.target.value)} value={opponent}>
|
|
||||||
<option value={""}></option>
|
|
||||||
<option value={"34053709"}>@AleCose</option>
|
|
||||||
<option value={"843330513"}>@Alleander</option>
|
|
||||||
<option value={"200821462"}>@catstolker</option>
|
|
||||||
<option value={"454281712"}>@CookieSin</option>
|
|
||||||
<option value={"524944901"}>@druidsfluid</option>
|
|
||||||
<option value={"48371848"}>@Francesco_Cuoghi</option>
|
|
||||||
<option value={"148374774"}>@GioOmbra</option>
|
|
||||||
<option value={"19611986"}>@GoodBalu</option>
|
|
||||||
<option value={"131057096"}>@Malbyx</option>
|
|
||||||
<option value={"488463576"}>@Mallllco</option>
|
|
||||||
<option value={"33523022"}>@MaxBubblegum</option>
|
|
||||||
<option value={"139079908"}>@SnowyCoder</option>
|
|
||||||
<option value={"165792255"}>@Spaggia</option>
|
|
||||||
<option value={"25167391"}>@Steffo</option>
|
|
||||||
<option value={"890339572"}>@xZefyr</option>
|
|
||||||
<option value={"19097832"}>@zezelda</option>
|
|
||||||
</select>
|
|
||||||
<span></span>
|
|
||||||
</label>
|
|
||||||
<div className={"form-flex-choice"}>
|
|
||||||
<span>Risultato</span>
|
|
||||||
<div>
|
|
||||||
<label>
|
|
||||||
<input type={"radio"} name={"result"} value={"W"} onChange={(e) => setResult(e.target.value)} checked={result === "W"}/> Vittoria
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type={"radio"} name={"result"} value={"T"} onChange={(e) => setResult(e.target.value)} checked={result === "T"}/> Pareggio
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type={"radio"} name={"result"} value={"L"} onChange={(e) => setResult(e.target.value)} checked={result === "L"}/> Sconfitta
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<span></span>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type={"submit"}
|
|
||||||
value={"Invia"}
|
|
||||||
className={classNames({
|
|
||||||
// TODO: "fade": result === null || opponent === ""
|
|
||||||
"fade": true,
|
|
||||||
})}
|
|
||||||
disabled={
|
|
||||||
// TODO: result === null || opponent === ""
|
|
||||||
true
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}, [onSubmit, userName, opponent, result])
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<main>
|
|
||||||
{contents}
|
|
||||||
</main>
|
|
||||||
</>
|
|
||||||
}
|
|
13
holycow_frontend/src/components/LoadingBox.module.css
Normal file
13
holycow_frontend/src/components/LoadingBox.module.css
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
@keyframes loading {
|
||||||
|
0% {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1.00;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
animation: loading 0.5s infinite alternate ease-in-out;
|
||||||
|
}
|
19
holycow_frontend/src/components/LoadingBox.tsx
Normal file
19
holycow_frontend/src/components/LoadingBox.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import classNames from "classnames"
|
||||||
|
import {ReactNode} from "react"
|
||||||
|
import style from "./LoadingBox.module.css"
|
||||||
|
|
||||||
|
export type LoadingBoxProps = {
|
||||||
|
children?: ReactNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoadingBox({children = "Loading..."}: LoadingBoxProps) {
|
||||||
|
return (
|
||||||
|
<div className={classNames({
|
||||||
|
"panel": true,
|
||||||
|
"box": true,
|
||||||
|
[style.loading]: true
|
||||||
|
})}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
97
holycow_frontend/src/components/ReportBox.tsx
Normal file
97
holycow_frontend/src/components/ReportBox.tsx
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import {Outcome, PlayerO} from "@/holycow"
|
||||||
|
import classNames from "classnames"
|
||||||
|
import {FormEvent} from "react"
|
||||||
|
|
||||||
|
export type ReportBoxProps = {
|
||||||
|
players: PlayerO[],
|
||||||
|
|
||||||
|
playerA: PlayerO,
|
||||||
|
playerB?: PlayerO,
|
||||||
|
setPlayerB: (player?: PlayerO) => void,
|
||||||
|
|
||||||
|
outcome: Outcome,
|
||||||
|
setOutcome: (outcome: Outcome) => void,
|
||||||
|
|
||||||
|
name: string,
|
||||||
|
setName: (name: string) => void,
|
||||||
|
|
||||||
|
onSubmit: (e: FormEvent) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
export function ReportBox({players, playerA, playerB, setPlayerB, outcome, setOutcome, name, setName, onSubmit}: ReportBoxProps) {
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
className={classNames({
|
||||||
|
"panel": true,
|
||||||
|
"box": true,
|
||||||
|
"form-flex": true,
|
||||||
|
"green": outcome === Outcome.AWins,
|
||||||
|
"red": outcome === Outcome.BWins,
|
||||||
|
"yellow": outcome === Outcome.Tie,
|
||||||
|
})}
|
||||||
|
onSubmit={e => {
|
||||||
|
e.preventDefault()
|
||||||
|
onSubmit(e)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h3>
|
||||||
|
Registra risultato
|
||||||
|
</h3>
|
||||||
|
<p></p>
|
||||||
|
<label>
|
||||||
|
<span>Tu</span>
|
||||||
|
<div>
|
||||||
|
{playerA.username}
|
||||||
|
</div>
|
||||||
|
<span>{playerA.human_score && `★ ${playerA.human_score}`}</span>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Avversario</span>
|
||||||
|
<select onChange={e => setPlayerB(players.find(p => p.id == Number.parseInt(e.target.value)))} value={playerB?.id}>
|
||||||
|
<option value={undefined}></option>
|
||||||
|
{players
|
||||||
|
.filter(player => player.id !== playerA.id)
|
||||||
|
.map(player => (
|
||||||
|
<option key={player.id} value={player.id}>{player.username}</option>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<span>{playerB && playerB.human_score && `★ ${playerB.human_score}`}</span>
|
||||||
|
</label>
|
||||||
|
<p></p>
|
||||||
|
<div className={"form-flex-choice"}>
|
||||||
|
<span>Risultato</span>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
<input type={"radio"} name={"result"} value={"AWins"} checked={outcome === Outcome.AWins} onChange={e => e.target.checked && setOutcome(Outcome.AWins)}/> Vittoria
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type={"radio"} name={"result"} value={"Tie"} checked={outcome === Outcome.Tie} onChange={e => e.target.checked && setOutcome(Outcome.Tie)}/> Pareggio
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type={"radio"} name={"result"} value={"BWins"} checked={outcome === Outcome.BWins} onChange={e => e.target.checked && setOutcome(Outcome.BWins)}/> Sconfitta
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
<p></p>
|
||||||
|
<label>
|
||||||
|
<span>Titolo sfida</span>
|
||||||
|
<input type={"text"} onChange={e => setName(e.target.value)} value={name}/>
|
||||||
|
<small>(opzionale)</small>
|
||||||
|
</label>
|
||||||
|
<p></p>
|
||||||
|
<input
|
||||||
|
type={"submit"}
|
||||||
|
value={"Invia"}
|
||||||
|
className={classNames({
|
||||||
|
"fade": outcome === undefined || playerB === undefined,
|
||||||
|
})}
|
||||||
|
disabled={
|
||||||
|
outcome === undefined || playerB === undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
56
holycow_frontend/src/components/ReportBoxInteractive.tsx
Normal file
56
holycow_frontend/src/components/ReportBoxInteractive.tsx
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import {ReportBox} from "@/components/ReportBox"
|
||||||
|
import {useTelegram} from "@/components/useTelegram"
|
||||||
|
import {Outcome, PlayerO} from "@/holycow"
|
||||||
|
import {useCallback, useState} from "react"
|
||||||
|
|
||||||
|
export type ReportBoxInteractiveProps = {
|
||||||
|
players: PlayerO[],
|
||||||
|
playerA: PlayerO,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReportBoxInteractive({players, playerA}: ReportBoxInteractiveProps) {
|
||||||
|
const [playerB, setPlayerB] = useState<PlayerO | undefined>(undefined)
|
||||||
|
const [outcome, setOutcome] = useState<Outcome | undefined>(undefined)
|
||||||
|
const [name, setName] = useState<string>("")
|
||||||
|
const [running, setRunning] = useState<boolean>(false)
|
||||||
|
const telegram = useTelegram()
|
||||||
|
|
||||||
|
const onSubmit = useCallback(
|
||||||
|
() => {
|
||||||
|
if(!telegram) return
|
||||||
|
setRunning(true)
|
||||||
|
const body = JSON.stringify({
|
||||||
|
name: name === "" ? null : name,
|
||||||
|
player_a: playerA.id,
|
||||||
|
player_b: playerB.id,
|
||||||
|
outcome: outcome.toString(),
|
||||||
|
})
|
||||||
|
fetch("/api/matches/", {
|
||||||
|
method: "POST",
|
||||||
|
body,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
telegram?.WebApp?.close?.()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[telegram, name, playerA, playerB, outcome]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReportBox
|
||||||
|
players={players}
|
||||||
|
playerA={playerA}
|
||||||
|
playerB={playerB}
|
||||||
|
setPlayerB={setPlayerB}
|
||||||
|
outcome={outcome}
|
||||||
|
setOutcome={setOutcome}
|
||||||
|
name={name}
|
||||||
|
setName={setName}
|
||||||
|
onSubmit={running ? () => {} : onSubmit}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
12
holycow_frontend/src/holycow.ts
Normal file
12
holycow_frontend/src/holycow.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
export enum Outcome {
|
||||||
|
AWins = "AWins",
|
||||||
|
BWins = "BWins",
|
||||||
|
Tie = "Tie",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PlayerO = {
|
||||||
|
id: number,
|
||||||
|
telegram_id: number,
|
||||||
|
username: string,
|
||||||
|
human_score: null | number,
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ interface Telegram {
|
||||||
interface TelegramWebApp {
|
interface TelegramWebApp {
|
||||||
initDataUnsafe?: TelegramWebAppInitData
|
initDataUnsafe?: TelegramWebAppInitData
|
||||||
sendData?: (data: string) => void,
|
sendData?: (data: string) => void,
|
||||||
|
close?: () => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TelegramWebAppInitData {
|
interface TelegramWebAppInitData {
|
||||||
|
|
Loading…
Reference in a new issue