Progress
This commit is contained in:
parent
65d04be7f3
commit
14c28762bf
12 changed files with 223 additions and 38 deletions
7
holycow_backend/Cargo.lock
generated
7
holycow_backend/Cargo.lock
generated
|
@ -1,6 +1,6 @@
|
||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
|
@ -221,6 +221,7 @@ dependencies = [
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"serde",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
@ -666,6 +667,7 @@ dependencies = [
|
||||||
"skillratings",
|
"skillratings",
|
||||||
"teloxide",
|
"teloxide",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1674,6 +1676,9 @@ name = "skillratings"
|
||||||
version = "0.27.1"
|
version = "0.27.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "53c8196a815d27d6dbd2439058a2cbf6597a549a68ca6368611df240df7c2987"
|
checksum = "53c8196a815d27d6dbd2439058a2cbf6597a549a68ca6368611df240df7c2987"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
|
|
|
@ -11,8 +11,9 @@ diesel_migrations = { version = "2.2.0", features = ["postgres"] }
|
||||||
log = "0.4.22"
|
log = "0.4.22"
|
||||||
micronfig = "0.3.0"
|
micronfig = "0.3.0"
|
||||||
pretty_env_logger = "0.5.0"
|
pretty_env_logger = "0.5.0"
|
||||||
skillratings = "0.27.1"
|
skillratings = { version = "0.27.1", features = ["serde"] }
|
||||||
teloxide = { version = "0.13.0", features = ["webhooks-axum"] }
|
teloxide = { version = "0.13.0", features = ["webhooks-axum"] }
|
||||||
tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread", "net"] }
|
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 = "0.4.38"
|
chrono = { version = "0.4.38", features = ["serde"] }
|
||||||
|
url = { version = "2.5.4", features = ["serde"] }
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE players DROP COLUMN IF EXISTS competitive;
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE players ADD COLUMN competitive BOOLEAN NOT NULL DEFAULT FALSE;
|
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE matches ALTER COLUMN player_a_id TYPE BIGINT;
|
||||||
|
ALTER TABLE matches ALTER COLUMN player_b_id TYPE BIGINT;
|
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE matches ALTER COLUMN player_a_id TYPE INTEGER;
|
||||||
|
ALTER TABLE matches ALTER COLUMN player_b_id TYPE INTEGER;
|
|
@ -1,4 +1,6 @@
|
||||||
micronfig::config! {
|
micronfig::config! {
|
||||||
DATABASE_URL,
|
DATABASE_URL: String,
|
||||||
BIND_ADDRESS,
|
BIND_ADDRESS: String > std::net::SocketAddr,
|
||||||
}
|
TELEGRAM_API_KEY: String,
|
||||||
|
TELEGRAM_WEBHOOK_URL: String > url::Url,
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,18 @@
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
use axum::extract::Path;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
|
use axum::Json;
|
||||||
use diesel::{Connection, PgConnection};
|
use diesel::{Connection, PgConnection};
|
||||||
use diesel_migrations::MigrationHarness;
|
use diesel_migrations::MigrationHarness;
|
||||||
|
use serde::Serialize;
|
||||||
|
use teloxide::{dptree};
|
||||||
|
use teloxide::dispatching::{DefaultKey, MessageFilterExt, UpdateFilterExt};
|
||||||
|
use teloxide::error_handlers::LoggingErrorHandler;
|
||||||
|
use teloxide::types::{Message, WebAppData};
|
||||||
|
use teloxide::update_listeners::webhooks::Options;
|
||||||
|
use crate::types::TelegramId;
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
mod schema;
|
mod schema;
|
||||||
|
@ -14,12 +23,17 @@ pub const MIGRATIONS: diesel_migrations::EmbeddedMigrations = diesel_migrations:
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<Infallible> {
|
async fn main() -> anyhow::Result<Infallible> {
|
||||||
pretty_env_logger::init();
|
pretty_env_logger::init();
|
||||||
log::debug!("Logging initialized!");
|
log::trace!("Logging initialized!");
|
||||||
|
|
||||||
log::trace!("Determining database URL...");
|
log::trace!("Determining database URL...");
|
||||||
let db = config::DATABASE_URL();
|
let db = config::DATABASE_URL();
|
||||||
|
log::trace!("Database URL is: {db:?}");
|
||||||
|
|
||||||
log::debug!("Connecting to: {db:?}");
|
log::trace!("Determining bind address...");
|
||||||
|
let bind_address = config::BIND_ADDRESS();
|
||||||
|
log::trace!("Bind address is: {bind_address:?}");
|
||||||
|
|
||||||
|
log::trace!("Connecting to: {db:?}");
|
||||||
let mut db = match PgConnection::establish(db) {
|
let mut db = match PgConnection::establish(db) {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to connect to the PostgreSQL database: {e:#?}");
|
log::error!("Failed to connect to the PostgreSQL database: {e:#?}");
|
||||||
|
@ -28,35 +42,129 @@ async fn main() -> anyhow::Result<Infallible> {
|
||||||
Ok(db) => db,
|
Ok(db) => db,
|
||||||
};
|
};
|
||||||
|
|
||||||
log::debug!("Running migrations...");
|
log::trace!("Running migrations...");
|
||||||
if let Err(e) = db.run_pending_migrations(MIGRATIONS) {
|
if let Err(e) = db.run_pending_migrations(MIGRATIONS) {
|
||||||
log::error!("Failed to perform migration: {e:#?}");
|
log::error!("Failed to perform migration: {e:#?}");
|
||||||
exit(2);
|
exit(2);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
log::trace!("Creating Telegram bot...");
|
||||||
|
let bot = teloxide::Bot::new(config::TELEGRAM_API_KEY());
|
||||||
|
|
||||||
|
log::trace!("Setting up webhooks...");
|
||||||
|
let (telegram_listener, _telegram_stop, telegram_router) = teloxide::update_listeners::webhooks::axum_to_router(
|
||||||
|
bot.clone(),
|
||||||
|
Options {
|
||||||
|
address: config::BIND_ADDRESS().clone(),
|
||||||
|
url: config::TELEGRAM_WEBHOOK_URL().clone(),
|
||||||
|
path: "/".to_string(),
|
||||||
|
certificate: None,
|
||||||
|
max_connections: None,
|
||||||
|
drop_pending_updates: false,
|
||||||
|
secret_token: None,
|
||||||
|
}
|
||||||
|
).await?;
|
||||||
|
|
||||||
log::trace!("Creating Axum router...");
|
log::trace!("Creating Axum router...");
|
||||||
let app = axum::Router::new()
|
let app = axum::Router::new()
|
||||||
.route("/results/:user", axum::routing::get(results_handler));
|
.route("/players/holycow/:player_id/results", axum::routing::get(results_by_id_handler))
|
||||||
log::trace!("Axum router created successfully!");
|
.route("/players/telegram/:telegram_id/results", axum::routing::get(results_by_telegram_id_handler))
|
||||||
|
.nest("/telegram/webhook", telegram_router)
|
||||||
|
;
|
||||||
|
|
||||||
|
log::trace!("Setting up Telegram dispatcher...");
|
||||||
|
let mut telegram_dispatcher = teloxide::dispatching::Dispatcher::<teloxide::Bot, anyhow::Error, DefaultKey>::builder(
|
||||||
|
bot.clone(),
|
||||||
|
Message::filter_web_app_data()
|
||||||
|
.endpoint(telegram_web_app_handler)
|
||||||
|
)
|
||||||
|
.default_handler(|u| async move {
|
||||||
|
log::trace!("Unhandled update: {u:#?}")
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
log::trace!("Creating Telegram dispatcher future...");
|
||||||
|
let telegram_future = telegram_dispatcher.dispatch_with_listener(telegram_listener, LoggingErrorHandler::new());
|
||||||
|
|
||||||
log::trace!("Creating Tokio listener...");
|
log::trace!("Creating Tokio listener...");
|
||||||
let bind_address = config::BIND_ADDRESS();
|
let tokio_listener = tokio::net::TcpListener::bind(bind_address)
|
||||||
let listener = tokio::net::TcpListener::bind(bind_address)
|
|
||||||
.await
|
.await
|
||||||
.context("failed to bind listener to address")?;
|
.context("failed to bind listener to address")?;
|
||||||
log::trace!("Tokio listener bound to: {bind_address}");
|
|
||||||
|
|
||||||
log::debug!("Starting server...");
|
log::trace!("Creating Axum server future...");
|
||||||
axum::serve(listener, app)
|
let axum_future = axum::serve(tokio_listener, app);
|
||||||
.await
|
|
||||||
.context("server exited with error")?;
|
|
||||||
|
|
||||||
log::error!("Server exited with no error, exiting.");
|
log::info!("Running Axum server future and Telegram dispatcher future!");
|
||||||
|
let _ = tokio::join!(axum_future, telegram_future);
|
||||||
|
|
||||||
|
log::error!("Server exited!");
|
||||||
exit(1)
|
exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize)]
|
||||||
|
struct ResultsResponse {
|
||||||
|
played: i64,
|
||||||
|
won: i64,
|
||||||
|
rating: f64,
|
||||||
|
uncertainty: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn results(
|
||||||
|
conn: &mut PgConnection,
|
||||||
|
player: types::Player,
|
||||||
|
) -> Result<Json<ResultsResponse>, StatusCode> {
|
||||||
|
let played = player.played_count(conn)
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
let won = player.won_count(conn)
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
let rating = match player.competitive {
|
||||||
|
false => 0.0,
|
||||||
|
true => player.wenglin.0.rating,
|
||||||
|
};
|
||||||
|
|
||||||
|
let uncertainty = match player.competitive {
|
||||||
|
false => 0.0,
|
||||||
|
true => player.wenglin.0.uncertainty,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Json(ResultsResponse {
|
||||||
|
played, won, rating, uncertainty
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
#[axum::debug_handler]
|
#[axum::debug_handler]
|
||||||
async fn results_handler() -> Result<String, StatusCode> {
|
async fn results_by_id_handler(
|
||||||
todo!()
|
Path(player_id): Path<i32>,
|
||||||
|
) -> Result<Json<ResultsResponse>, StatusCode> {
|
||||||
|
let mut conn = PgConnection::establish(config::DATABASE_URL())
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
let player = types::Player::get_by_id(&mut conn, player_id)
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||||
|
.ok_or(StatusCode::NOT_FOUND)?;
|
||||||
|
|
||||||
|
results(&mut conn, player)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[axum::debug_handler]
|
||||||
|
async fn results_by_telegram_id_handler(
|
||||||
|
Path(telegram_id): Path<TelegramId>,
|
||||||
|
) -> Result<Json<ResultsResponse>, StatusCode> {
|
||||||
|
let mut conn = PgConnection::establish(config::DATABASE_URL())
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
let player = types::Player::get_by_telegram_id(&mut conn, telegram_id)
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||||
|
.ok_or(StatusCode::NOT_FOUND)?;
|
||||||
|
|
||||||
|
results(&mut conn, player)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn telegram_web_app_handler(
|
||||||
|
web_app_data: WebAppData,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
log::trace!("{web_app_data:#?}");
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -19,10 +19,10 @@ diesel::table! {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
instant -> Timestamptz,
|
instant -> Timestamptz,
|
||||||
name -> Nullable<Varchar>,
|
name -> Nullable<Varchar>,
|
||||||
player_a_id -> Int8,
|
player_a_id -> Int4,
|
||||||
player_a_wenglin_before -> WenglinT,
|
player_a_wenglin_before -> WenglinT,
|
||||||
player_a_wenglin_after -> WenglinT,
|
player_a_wenglin_after -> WenglinT,
|
||||||
player_b_id -> Int8,
|
player_b_id -> Int4,
|
||||||
player_b_wenglin_before -> WenglinT,
|
player_b_wenglin_before -> WenglinT,
|
||||||
player_b_wenglin_after -> WenglinT,
|
player_b_wenglin_after -> WenglinT,
|
||||||
outcome -> OutcomeT,
|
outcome -> OutcomeT,
|
||||||
|
@ -37,6 +37,7 @@ diesel::table! {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
wenglin -> WenglinT,
|
wenglin -> WenglinT,
|
||||||
telegram_id -> Nullable<Int8>,
|
telegram_id -> Nullable<Int8>,
|
||||||
|
competitive -> Bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,44 +1,48 @@
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use diesel::{AsExpression, FromSqlRow, Identifiable, Insertable, Queryable, QueryableByName, Selectable};
|
use diesel::{AsExpression, BoolExpressionMethods, FromSqlRow, Identifiable, Insertable, OptionalExtension, PgConnection, QueryDsl, QueryResult, Queryable, QueryableByName, RunQueryDsl, Selectable, SelectableHelper};
|
||||||
use diesel::backend::Backend;
|
use diesel::backend::Backend;
|
||||||
use diesel::deserialize::FromSql;
|
use diesel::deserialize::FromSql;
|
||||||
use diesel::pg::Pg;
|
use diesel::pg::Pg;
|
||||||
use diesel::serialize::ToSql;
|
use diesel::serialize::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 serde::{Deserialize, Serialize};
|
||||||
use crate::schema;
|
use crate::schema;
|
||||||
|
|
||||||
#[derive(Debug, Clone, FromSqlRow, AsExpression)]
|
#[derive(Debug, Clone, FromSqlRow, AsExpression, Serialize, Deserialize)]
|
||||||
#[diesel(sql_type = sql::BigInt)]
|
#[diesel(sql_type = sql::BigInt)]
|
||||||
#[diesel(check_for_backend(Pg))]
|
#[diesel(check_for_backend(Pg))]
|
||||||
#[repr(transparent)]
|
#[repr(transparent)]
|
||||||
pub struct TelegramId(pub teloxide::types::ChatId);
|
pub struct TelegramId(pub teloxide::types::ChatId);
|
||||||
|
|
||||||
#[derive(Debug, Clone, FromSqlRow, AsExpression)]
|
#[derive(Debug, Clone, FromSqlRow, AsExpression, Serialize, Deserialize)]
|
||||||
#[diesel(sql_type = schema::sql_types::WenglinT)]
|
#[diesel(sql_type = schema::sql_types::WenglinT)]
|
||||||
#[diesel(check_for_backend(Pg))]
|
#[diesel(check_for_backend(Pg))]
|
||||||
#[repr(transparent)]
|
#[repr(transparent)]
|
||||||
pub struct WengLinRating(pub skillratings::weng_lin::WengLinRating);
|
pub struct WengLinRating(pub skillratings::weng_lin::WengLinRating);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Queryable, QueryableByName, Identifiable, Selectable)]
|
#[derive(Debug, Clone, Queryable, QueryableByName, Identifiable, Selectable, Serialize, Deserialize)]
|
||||||
#[diesel(table_name = schema::players)]
|
#[diesel(table_name = schema::players)]
|
||||||
#[diesel(check_for_backend(Pg))]
|
#[diesel(check_for_backend(Pg))]
|
||||||
pub struct Player {
|
pub struct Player {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub wenglin: WengLinRating,
|
pub wenglin: WengLinRating,
|
||||||
pub telegram_id: Option<TelegramId>,
|
pub telegram_id: Option<TelegramId>,
|
||||||
|
pub competitive: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Insertable)]
|
#[derive(Debug, Clone, Insertable, Serialize, Deserialize)]
|
||||||
#[diesel(table_name = schema::players)]
|
#[diesel(table_name = schema::players)]
|
||||||
#[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: TelegramId,
|
||||||
|
pub competitive: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromSqlRow, AsExpression)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromSqlRow, AsExpression, Serialize, Deserialize)]
|
||||||
#[diesel(sql_type = schema::sql_types::OutcomeT)]
|
#[diesel(sql_type = schema::sql_types::OutcomeT)]
|
||||||
pub enum Outcome {
|
pub enum Outcome {
|
||||||
AWins,
|
AWins,
|
||||||
|
@ -46,38 +50,37 @@ pub enum Outcome {
|
||||||
Tie,
|
Tie,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Queryable, QueryableByName, Identifiable, Selectable)]
|
#[derive(Debug, Clone, Queryable, QueryableByName, Identifiable, Selectable, Serialize, Deserialize)]
|
||||||
#[diesel(table_name = schema::matches)]
|
#[diesel(table_name = schema::matches)]
|
||||||
#[diesel(check_for_backend(Pg))]
|
#[diesel(check_for_backend(Pg))]
|
||||||
pub struct Match {
|
pub struct Match {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub instant: DateTime<Utc>,
|
pub instant: DateTime<Utc>,
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub player_a_id: TelegramId,
|
pub player_a_id: i32,
|
||||||
pub player_a_wenglin_before: WengLinRating,
|
pub player_a_wenglin_before: WengLinRating,
|
||||||
pub player_a_wenglin_after: WengLinRating,
|
pub player_a_wenglin_after: WengLinRating,
|
||||||
pub player_b_id: TelegramId,
|
pub player_b_id: i32,
|
||||||
pub player_b_wenglin_before: WengLinRating,
|
pub player_b_wenglin_before: WengLinRating,
|
||||||
pub player_b_wenglin_after: WengLinRating,
|
pub player_b_wenglin_after: WengLinRating,
|
||||||
pub outcome: Outcome,
|
pub outcome: Outcome,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Insertable)]
|
#[derive(Debug, Clone, Insertable, Serialize, Deserialize)]
|
||||||
#[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 instant: DateTime<Utc>,
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub player_a_id: TelegramId,
|
pub player_a_id: i32,
|
||||||
pub player_a_wenglin_before: WengLinRating,
|
pub player_a_wenglin_before: WengLinRating,
|
||||||
pub player_a_wenglin_after: WengLinRating,
|
pub player_a_wenglin_after: WengLinRating,
|
||||||
pub player_b_id: TelegramId,
|
pub player_b_id: i32,
|
||||||
pub player_b_wenglin_before: WengLinRating,
|
pub player_b_wenglin_before: WengLinRating,
|
||||||
pub player_b_wenglin_after: WengLinRating,
|
pub player_b_wenglin_after: WengLinRating,
|
||||||
pub outcome: Outcome,
|
pub outcome: Outcome,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl FromSql<sql::BigInt, Pg> for TelegramId {
|
impl FromSql<sql::BigInt, Pg> for TelegramId {
|
||||||
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 s = <i64 as FromSql<sql::BigInt, Pg>>::from_sql(bytes)?;
|
let s = <i64 as FromSql<sql::BigInt, Pg>>::from_sql(bytes)?;
|
||||||
|
@ -136,4 +139,56 @@ impl ToSql<schema::sql_types::OutcomeT, Pg> for Outcome {
|
||||||
|
|
||||||
Ok(diesel::serialize::IsNull::No)
|
Ok(diesel::serialize::IsNull::No)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Player {
|
||||||
|
pub fn get_by_id(conn: &mut PgConnection, player_id: i32) -> QueryResult<Option<Self>> {
|
||||||
|
schema::players::table
|
||||||
|
.select(Self::as_select())
|
||||||
|
.filter(schema::players::id.eq(player_id))
|
||||||
|
.get_result(conn)
|
||||||
|
.optional()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_by_telegram_id(conn: &mut PgConnection, telegram_id: TelegramId) -> QueryResult<Option<Self>> {
|
||||||
|
schema::players::table
|
||||||
|
.select(Self::as_select())
|
||||||
|
.filter(schema::players::telegram_id.eq(telegram_id))
|
||||||
|
.get_result(conn)
|
||||||
|
.optional()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn played_count(&self, conn: &mut PgConnection) -> QueryResult<i64> {
|
||||||
|
Match::played_by_count(conn, self.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn won_count(&self, conn: &mut PgConnection) -> QueryResult<i64> {
|
||||||
|
Match::won_by_count(conn, self.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Match {
|
||||||
|
pub fn total(conn: &mut PgConnection) -> QueryResult<i64> {
|
||||||
|
schema::matches::table
|
||||||
|
.select(diesel::dsl::count_star())
|
||||||
|
.get_result::<i64>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn played_by_count(conn: &mut PgConnection, player_id: i32) -> QueryResult<i64> {
|
||||||
|
schema::matches::table
|
||||||
|
.select(diesel::dsl::count_star())
|
||||||
|
.or_filter(schema::matches::player_a_id.eq(player_id))
|
||||||
|
.or_filter(schema::matches::player_b_id.eq(player_id))
|
||||||
|
.get_result::<i64>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn won_by_count(conn: &mut PgConnection, player_id: i32) -> QueryResult<i64> {
|
||||||
|
schema::matches::table
|
||||||
|
.select(diesel::dsl::count_star())
|
||||||
|
.or_filter(schema::matches::player_a_id.eq(player_id)
|
||||||
|
.and(schema::matches::outcome.eq(Outcome::AWins)))
|
||||||
|
.or_filter(schema::matches::player_b_id.eq(player_id)
|
||||||
|
.and(schema::matches::outcome.eq(Outcome::BWins)))
|
||||||
|
.get_result::<i64>(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { StatPanel } from "@/components/StatPanel";
|
import { StatPanel } from "@/components/StatPanel";
|
||||||
import { useTelegram } from "@/components/useTelegram";
|
import { useTelegram } from "@/components/useTelegram";
|
||||||
import { useMemo } from "react";
|
import {useEffect, useMemo} from "react"
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
|
@ -14,6 +14,12 @@ export default function Page() {
|
||||||
const resultsData = undefined
|
const resultsData = undefined
|
||||||
const resultsError = undefined
|
const resultsError = undefined
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(telegramData.start_param === "report") {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
}, [telegramData])
|
||||||
|
|
||||||
const contents = useMemo(() => {
|
const contents = useMemo(() => {
|
||||||
if(resultsError) {
|
if(resultsError) {
|
||||||
return resultsError.toString()
|
return resultsError.toString()
|
||||||
|
|
|
@ -10,6 +10,7 @@ interface TelegramWebApp {
|
||||||
interface TelegramWebAppInitData {
|
interface TelegramWebAppInitData {
|
||||||
user: TelegramWebAppUser,
|
user: TelegramWebAppUser,
|
||||||
receiver: TelegramWebAppUser,
|
receiver: TelegramWebAppUser,
|
||||||
|
start_param?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TelegramWebAppUser {
|
interface TelegramWebAppUser {
|
||||||
|
|
Loading…
Reference in a new issue