diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..aed68ba --- /dev/null +++ b/Caddyfile @@ -0,0 +1,4 @@ +:30002 { + reverse_proxy http://localhost:30000 + reverse_proxy /api http://localhost:30001 +} diff --git a/holycow_backend/diesel.toml b/holycow_backend/diesel.toml index 83d15a9..803c125 100644 --- a/holycow_backend/diesel.toml +++ b/holycow_backend/diesel.toml @@ -2,7 +2,7 @@ # see https://diesel.rs/guides/configuring-diesel-cli [print_schema] -file = "src/schema.rs" +file = "src/database/schema.rs" custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] [migrations_directory] diff --git a/holycow_backend/src/config.rs b/holycow_backend/src/config.rs index 92cde60..a66a9d6 100644 --- a/holycow_backend/src/config.rs +++ b/holycow_backend/src/config.rs @@ -1,6 +1,6 @@ micronfig::config! { DATABASE_URL: String, - BIND_ADDRESS: String > std::net::SocketAddr, + BACKEND_BIND_ADDRESS: String > std::net::SocketAddr, TELEGRAM_API_KEY: String, TELEGRAM_WEBHOOK_URL: String > url::Url, } diff --git a/holycow_backend/src/database/migrations.rs b/holycow_backend/src/database/migrations.rs new file mode 100644 index 0000000..047be11 --- /dev/null +++ b/holycow_backend/src/database/migrations.rs @@ -0,0 +1 @@ +pub const MIGRATIONS: diesel_migrations::EmbeddedMigrations = diesel_migrations::embed_migrations!(); diff --git a/holycow_backend/src/database/mod.rs b/holycow_backend/src/database/mod.rs new file mode 100644 index 0000000..701386a --- /dev/null +++ b/holycow_backend/src/database/mod.rs @@ -0,0 +1,3 @@ +pub mod model; +pub mod migrations; +mod schema; diff --git a/holycow_backend/src/types.rs b/holycow_backend/src/database/model.rs similarity index 87% rename from holycow_backend/src/types.rs rename to holycow_backend/src/database/model.rs index ad92b9e..f22fe98 100644 --- a/holycow_backend/src/types.rs +++ b/holycow_backend/src/database/model.rs @@ -3,13 +3,14 @@ use chrono::{DateTime, Utc}; use diesel::{AsExpression, BoolExpressionMethods, FromSqlRow, Identifiable, Insertable, OptionalExtension, PgConnection, QueryDsl, QueryResult, Queryable, QueryableByName, RunQueryDsl, Selectable, SelectableHelper}; use diesel::backend::Backend; use diesel::deserialize::FromSql; +use diesel::dsl::insert_into; use diesel::pg::Pg; use diesel::serialize::ToSql; use diesel::sql_types as sql; use diesel::serialize::Output as DieselOutput; use diesel::ExpressionMethods; use serde::{Deserialize, Serialize}; -use crate::schema; +use crate::database::schema; #[derive(Debug, Clone, FromSqlRow, AsExpression, Serialize, Deserialize)] #[diesel(sql_type = sql::BigInt)] @@ -142,6 +143,18 @@ impl ToSql for Outcome { } impl Player { + pub fn total(conn: &mut PgConnection) -> QueryResult { + schema::players::table + .select(diesel::dsl::count_star()) + .get_result::(conn) + } + + pub fn all(conn: &mut PgConnection) -> QueryResult> { + schema::players::table + .select(Self::as_select()) + .get_results::(conn) + } + pub fn get_by_id(conn: &mut PgConnection, player_id: i32) -> QueryResult> { schema::players::table .select(Self::as_select()) @@ -174,6 +187,13 @@ impl Match { .get_result::(conn) } + pub fn all(conn: &mut PgConnection) -> QueryResult> { + schema::matches::table + .select(Self::as_select()) + .order_by(schema::matches::instant) + .get_results::(conn) + } + pub fn played_by_count(conn: &mut PgConnection, player_id: i32) -> QueryResult { schema::matches::table .select(diesel::dsl::count_star()) @@ -192,3 +212,19 @@ impl Match { .get_result::(conn) } } + +impl PlayerI { + pub fn insert(self, conn: &mut PgConnection) -> QueryResult { + insert_into(schema::players::table) + .values(&[self]) + .get_result::(conn) + } +} + +impl MatchI { + pub fn insert(self, conn: &mut PgConnection) -> QueryResult { + insert_into(schema::matches::table) + .values(&[self]) + .get_result::(conn) + } +} diff --git a/holycow_backend/src/schema.rs b/holycow_backend/src/database/schema.rs similarity index 100% rename from holycow_backend/src/schema.rs rename to holycow_backend/src/database/schema.rs diff --git a/holycow_backend/src/main.rs b/holycow_backend/src/main.rs index eaded92..ead9de8 100644 --- a/holycow_backend/src/main.rs +++ b/holycow_backend/src/main.rs @@ -1,24 +1,17 @@ use std::convert::Infallible; use std::process::exit; use anyhow::Context; -use axum::extract::Path; -use axum::http::StatusCode; -use axum::Json; use diesel::{Connection, PgConnection}; use diesel_migrations::MigrationHarness; -use serde::Serialize; -use teloxide::{dptree}; -use teloxide::dispatching::{DefaultKey, MessageFilterExt, UpdateFilterExt}; +use teloxide::dispatching::{DefaultKey, MessageFilterExt}; use teloxide::error_handlers::LoggingErrorHandler; -use teloxide::types::{Message, WebAppData}; +use teloxide::types::Message; use teloxide::update_listeners::webhooks::Options; -use crate::types::TelegramId; mod config; -mod schema; -mod types; - -pub const MIGRATIONS: diesel_migrations::EmbeddedMigrations = diesel_migrations::embed_migrations!(); +mod database; +mod routes; +mod telegram; #[tokio::main] async fn main() -> anyhow::Result { @@ -30,7 +23,7 @@ async fn main() -> anyhow::Result { log::trace!("Database URL is: {db:?}"); log::trace!("Determining bind address..."); - let bind_address = config::BIND_ADDRESS(); + let bind_address = config::BACKEND_BIND_ADDRESS(); log::trace!("Bind address is: {bind_address:?}"); log::trace!("Connecting to: {db:?}"); @@ -43,7 +36,7 @@ async fn main() -> anyhow::Result { }; log::trace!("Running migrations..."); - if let Err(e) = db.run_pending_migrations(MIGRATIONS) { + if let Err(e) = db.run_pending_migrations(database::migrations::MIGRATIONS) { log::error!("Failed to perform migration: {e:#?}"); exit(2); }; @@ -55,7 +48,7 @@ async fn main() -> anyhow::Result { let (telegram_listener, _telegram_stop, telegram_router) = teloxide::update_listeners::webhooks::axum_to_router( bot.clone(), Options { - address: config::BIND_ADDRESS().clone(), + address: config::BACKEND_BIND_ADDRESS().clone(), url: config::TELEGRAM_WEBHOOK_URL().clone(), path: "/".to_string(), certificate: None, @@ -67,16 +60,28 @@ async fn main() -> anyhow::Result { log::trace!("Creating Axum router..."); let app = axum::Router::new() - .route("/players/holycow/:player_id/results", axum::routing::get(results_by_id_handler)) - .route("/players/telegram/:telegram_id/results", axum::routing::get(results_by_telegram_id_handler)) - .nest("/telegram/webhook", telegram_router) + .route("/api/results/", + axum::routing::get(routes::results::get_all) + ) + .route("/api/results/holycow/:player_id", + axum::routing::get(routes::results::get_by_id) + ) + .route("/api/results/telegram/:telegram_id", + axum::routing::get(routes::results::get_by_telegram_id) + ) + .route("/api/matches/", + axum::routing::get(routes::matches::get_all) + ) + .nest("/telegram/webhook", + telegram_router + ) ; log::trace!("Setting up Telegram dispatcher..."); let mut telegram_dispatcher = teloxide::dispatching::Dispatcher::::builder( bot.clone(), Message::filter_web_app_data() - .endpoint(telegram_web_app_handler) + .endpoint(telegram::webapp::process_data) ) .default_handler(|u| async move { log::trace!("Unhandled update: {u:#?}") @@ -100,71 +105,3 @@ async fn main() -> anyhow::Result { log::error!("Server exited!"); 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, 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] -async fn results_by_id_handler( - Path(player_id): Path, -) -> Result, 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, -) -> Result, 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(()) -} \ No newline at end of file diff --git a/holycow_backend/src/routes/matches.rs b/holycow_backend/src/routes/matches.rs new file mode 100644 index 0000000..a139095 --- /dev/null +++ b/holycow_backend/src/routes/matches.rs @@ -0,0 +1,18 @@ +use axum::http::StatusCode; +use axum::Json; +use diesel::{Connection, PgConnection}; +use crate::config; +use crate::database::model::Match; + +#[axum::debug_handler] +pub async fn get_all() + -> Result>, StatusCode> +{ + let mut conn = PgConnection::establish(config::DATABASE_URL()) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + let matches = Match::all(&mut conn) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(Json(matches)) +} diff --git a/holycow_backend/src/routes/mod.rs b/holycow_backend/src/routes/mod.rs new file mode 100644 index 0000000..6d9fd29 --- /dev/null +++ b/holycow_backend/src/routes/mod.rs @@ -0,0 +1,2 @@ +pub mod results; +pub mod matches; diff --git a/holycow_backend/src/routes/results.rs b/holycow_backend/src/routes/results.rs new file mode 100644 index 0000000..0744752 --- /dev/null +++ b/holycow_backend/src/routes/results.rs @@ -0,0 +1,53 @@ +use axum::extract::Path; +use axum::http::StatusCode; +use axum::Json; +use diesel::{Connection, PgConnection}; +use model::Player; +use crate::config; +use crate::database::model; +use crate::database::model::TelegramId; + +#[axum::debug_handler] +pub async fn get_all() + -> Result>, StatusCode> +{ + let mut conn = PgConnection::establish(config::DATABASE_URL()) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + let players = Player::all(&mut conn) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(Json(players)) +} + +#[axum::debug_handler] +pub async fn get_by_id( + Path(player_id): Path, +) + -> Result, StatusCode> +{ + let mut conn = PgConnection::establish(config::DATABASE_URL()) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + let player = Player::get_by_id(&mut conn, player_id) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .ok_or(StatusCode::NOT_FOUND)?; + + Ok(Json(player)) +} + +#[axum::debug_handler] +pub async fn get_by_telegram_id( + Path(telegram_id): Path, +) + -> Result, StatusCode> +{ + let mut conn = PgConnection::establish(config::DATABASE_URL()) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + let player = Player::get_by_telegram_id(&mut conn, telegram_id) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .ok_or(StatusCode::NOT_FOUND)?; + + Ok(Json(player)) +} diff --git a/holycow_backend/src/telegram/mod.rs b/holycow_backend/src/telegram/mod.rs new file mode 100644 index 0000000..56b418a --- /dev/null +++ b/holycow_backend/src/telegram/mod.rs @@ -0,0 +1 @@ +pub mod webapp; \ No newline at end of file diff --git a/holycow_backend/src/telegram/webapp.rs b/holycow_backend/src/telegram/webapp.rs new file mode 100644 index 0000000..a221885 --- /dev/null +++ b/holycow_backend/src/telegram/webapp.rs @@ -0,0 +1,8 @@ +use teloxide::types::WebAppData; + +pub async fn process_data( + web_app_data: WebAppData, +) -> anyhow::Result<()> { + log::trace!("{web_app_data:#?}"); + Ok(()) +} diff --git a/holycow_frontend/next.config.mjs b/holycow_frontend/next.config.mjs deleted file mode 100644 index 6f12dc4..0000000 --- a/holycow_frontend/next.config.mjs +++ /dev/null @@ -1,6 +0,0 @@ -/** @type {import("next").NextConfig} */ -const nextConfig = { - output: "export" -}; - -export default nextConfig; diff --git a/holycow_frontend/next.config.ts b/holycow_frontend/next.config.ts new file mode 100644 index 0000000..7625ced --- /dev/null +++ b/holycow_frontend/next.config.ts @@ -0,0 +1,7 @@ +import {NextConfig} from "next" + +const nextConfig: NextConfig = { + output: "export", +}; + +export default nextConfig; diff --git a/holycow_frontend/package.json b/holycow_frontend/package.json index a08a9f2..3ce1ea2 100644 --- a/holycow_frontend/package.json +++ b/holycow_frontend/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --turbopack", + "dev": "next dev --turbopack --port 30000", "build": "next build", "start": "next start", "lint": "next lint" diff --git a/holycow_frontend/src/app/layout.tsx b/holycow_frontend/src/app/layout.tsx index d12e1b3..e4c2c8b 100644 --- a/holycow_frontend/src/app/layout.tsx +++ b/holycow_frontend/src/app/layout.tsx @@ -38,9 +38,7 @@ export default function RootLayout({ children }) {

© Stefano Pigozzi  -  - A quanto pare non posso mettere link esterni qui -  -  - Garasauto + che cursata, non ha senso, che cursata

diff --git a/holycow_frontend/src/app/page.tsx b/holycow_frontend/src/app/page.tsx index 0e85d2e..a68b446 100644 --- a/holycow_frontend/src/app/page.tsx +++ b/holycow_frontend/src/app/page.tsx @@ -1,89 +1,18 @@ "use client"; -import { StatPanel } from "@/components/StatPanel"; import { useTelegram } from "@/components/useTelegram"; import {useEffect, useMemo} from "react" -import classNames from "classnames"; export default function Page() { const telegram = useTelegram() const telegramData = telegram?.WebApp?.initDataUnsafe const userId = telegramData?.user?.id - const userName = telegramData?.user?.first_name ?? "???" + const userName = telegramData?.user?.first_name + const startParam = telegramData?.start_param const resultsData = undefined const resultsError = undefined - useEffect(() => { - if(telegramData.start_param === "report") { - // TODO - } - }, [telegramData]) - - const contents = useMemo(() => { - if(resultsError) { - return resultsError.toString() - } - - const played = resultsData?.["played"] ?? 0 - const wins = resultsData?.["wins"] ?? 0 - const rating = resultsData?.["rating"] ?? 0 - - return ( -
-
-

- {userName} -

-
- - {played} - - )} - /> - - {wins} - - )} - /> - - {rating === 0 ? "-" : rating} - - )} - /> -
-
-
- ) - }, [userName, resultsData, resultsError]) - - return <> -
- {contents} -
- + return
+
} diff --git a/holycow_frontend/src/components/ProfileBox.tsx b/holycow_frontend/src/components/ProfileBox.tsx new file mode 100644 index 0000000..d2ece1c --- /dev/null +++ b/holycow_frontend/src/components/ProfileBox.tsx @@ -0,0 +1,83 @@ +import {StatPanel} from "@/components/StatPanel" +import classNames from "classnames" + + +export type ProfileBoxProps = { + userName: string, + played: number, + won: number, + rating: number, + uncertainty: number, +} + + +export function ProfileBox({userName, played, won, rating, uncertainty}: ProfileBoxProps) { + return ( +
+
+

+ {userName} +

+
+ + {played} + + )} + /> + + {won} + + )} + /> + + {rating === 0 + ? + <> + + - + + + : + <> + + {rating} + + + ± + + + {uncertainty} + + + } + + )} + /> +
+
+
+ ) +} \ No newline at end of file