it's a good moment to commit
This commit is contained in:
parent
14c28762bf
commit
c3cf86f6e9
19 changed files with 249 additions and 175 deletions
4
Caddyfile
Normal file
4
Caddyfile
Normal file
|
@ -0,0 +1,4 @@
|
|||
:30002 {
|
||||
reverse_proxy http://localhost:30000
|
||||
reverse_proxy /api http://localhost:30001
|
||||
}
|
|
@ -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]
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
1
holycow_backend/src/database/migrations.rs
Normal file
1
holycow_backend/src/database/migrations.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub const MIGRATIONS: diesel_migrations::EmbeddedMigrations = diesel_migrations::embed_migrations!();
|
3
holycow_backend/src/database/mod.rs
Normal file
3
holycow_backend/src/database/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod model;
|
||||
pub mod migrations;
|
||||
mod schema;
|
|
@ -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<schema::sql_types::OutcomeT, Pg> for Outcome {
|
|||
}
|
||||
|
||||
impl Player {
|
||||
pub fn total(conn: &mut PgConnection) -> QueryResult<i64> {
|
||||
schema::players::table
|
||||
.select(diesel::dsl::count_star())
|
||||
.get_result::<i64>(conn)
|
||||
}
|
||||
|
||||
pub fn all(conn: &mut PgConnection) -> QueryResult<Vec<Self>> {
|
||||
schema::players::table
|
||||
.select(Self::as_select())
|
||||
.get_results::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn get_by_id(conn: &mut PgConnection, player_id: i32) -> QueryResult<Option<Self>> {
|
||||
schema::players::table
|
||||
.select(Self::as_select())
|
||||
|
@ -174,6 +187,13 @@ impl Match {
|
|||
.get_result::<i64>(conn)
|
||||
}
|
||||
|
||||
pub fn all(conn: &mut PgConnection) -> QueryResult<Vec<Self>> {
|
||||
schema::matches::table
|
||||
.select(Self::as_select())
|
||||
.order_by(schema::matches::instant)
|
||||
.get_results::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn played_by_count(conn: &mut PgConnection, player_id: i32) -> QueryResult<i64> {
|
||||
schema::matches::table
|
||||
.select(diesel::dsl::count_star())
|
||||
|
@ -192,3 +212,19 @@ impl Match {
|
|||
.get_result::<i64>(conn)
|
||||
}
|
||||
}
|
||||
|
||||
impl PlayerI {
|
||||
pub fn insert(self, conn: &mut PgConnection) -> QueryResult<Player> {
|
||||
insert_into(schema::players::table)
|
||||
.values(&[self])
|
||||
.get_result::<Player>(conn)
|
||||
}
|
||||
}
|
||||
|
||||
impl MatchI {
|
||||
pub fn insert(self, conn: &mut PgConnection) -> QueryResult<Match> {
|
||||
insert_into(schema::matches::table)
|
||||
.values(&[self])
|
||||
.get_result::<Match>(conn)
|
||||
}
|
||||
}
|
|
@ -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<Infallible> {
|
||||
|
@ -30,7 +23,7 @@ async fn main() -> anyhow::Result<Infallible> {
|
|||
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<Infallible> {
|
|||
};
|
||||
|
||||
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<Infallible> {
|
|||
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<Infallible> {
|
|||
|
||||
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::<teloxide::Bot, anyhow::Error, DefaultKey>::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<Infallible> {
|
|||
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<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]
|
||||
async fn results_by_id_handler(
|
||||
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(())
|
||||
}
|
18
holycow_backend/src/routes/matches.rs
Normal file
18
holycow_backend/src/routes/matches.rs
Normal file
|
@ -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<Json<Vec<Match>>, 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))
|
||||
}
|
2
holycow_backend/src/routes/mod.rs
Normal file
2
holycow_backend/src/routes/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod results;
|
||||
pub mod matches;
|
53
holycow_backend/src/routes/results.rs
Normal file
53
holycow_backend/src/routes/results.rs
Normal file
|
@ -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<Json<Vec<Player>>, 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<i32>,
|
||||
)
|
||||
-> Result<Json<Player>, 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<TelegramId>,
|
||||
)
|
||||
-> Result<Json<Player>, 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))
|
||||
}
|
1
holycow_backend/src/telegram/mod.rs
Normal file
1
holycow_backend/src/telegram/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod webapp;
|
8
holycow_backend/src/telegram/webapp.rs
Normal file
8
holycow_backend/src/telegram/webapp.rs
Normal file
|
@ -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(())
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
/** @type {import("next").NextConfig} */
|
||||
const nextConfig = {
|
||||
output: "export"
|
||||
};
|
||||
|
||||
export default nextConfig;
|
7
holycow_frontend/next.config.ts
Normal file
7
holycow_frontend/next.config.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import {NextConfig} from "next"
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "export",
|
||||
};
|
||||
|
||||
export default nextConfig;
|
|
@ -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"
|
||||
|
|
|
@ -38,9 +38,7 @@ export default function RootLayout({ children }) {
|
|||
<p>
|
||||
© Stefano Pigozzi
|
||||
-
|
||||
A quanto pare non posso mettere link esterni qui
|
||||
-
|
||||
Garasauto
|
||||
che cursata, non ha senso, che cursata
|
||||
</p>
|
||||
</footer>
|
||||
</body>
|
||||
|
|
|
@ -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 (
|
||||
<div className={"chapter-1"}>
|
||||
<div className={"panel box"}>
|
||||
<h3>
|
||||
{userName}
|
||||
</h3>
|
||||
<div className={"chapter-3"}>
|
||||
<StatPanel
|
||||
name={"Giocate"}
|
||||
value={(
|
||||
<data
|
||||
className={classNames({
|
||||
"fade": played === 0
|
||||
})}
|
||||
value={played}
|
||||
>
|
||||
{played}
|
||||
</data>
|
||||
)}
|
||||
/>
|
||||
<StatPanel
|
||||
name={"Vinte"}
|
||||
value={(
|
||||
<data
|
||||
className={classNames({
|
||||
"fade": wins === 0
|
||||
})}
|
||||
value={wins}
|
||||
>
|
||||
{wins}
|
||||
</data>
|
||||
)}
|
||||
/>
|
||||
<StatPanel
|
||||
name={"Rating"}
|
||||
value={(
|
||||
<data
|
||||
className={classNames({
|
||||
"fade": rating === 0
|
||||
})}
|
||||
value={rating}
|
||||
>
|
||||
{rating === 0 ? "-" : rating}
|
||||
</data>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}, [userName, resultsData, resultsError])
|
||||
|
||||
return <>
|
||||
<main>
|
||||
{contents}
|
||||
</main>
|
||||
</>
|
||||
return <main>
|
||||
</main>
|
||||
}
|
||||
|
|
83
holycow_frontend/src/components/ProfileBox.tsx
Normal file
83
holycow_frontend/src/components/ProfileBox.tsx
Normal file
|
@ -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 (
|
||||
<div className={"chapter-1"}>
|
||||
<div className={"panel box"}>
|
||||
<h3>
|
||||
{userName}
|
||||
</h3>
|
||||
<div className={"chapter-4"}>
|
||||
<StatPanel
|
||||
name={"Giocate"}
|
||||
value={(
|
||||
<data
|
||||
className={classNames({
|
||||
"fade": played === 0
|
||||
})}
|
||||
value={played}
|
||||
>
|
||||
{played}
|
||||
</data>
|
||||
)}
|
||||
/>
|
||||
<StatPanel
|
||||
name={"Vinte"}
|
||||
value={(
|
||||
<data
|
||||
className={classNames({
|
||||
"fade": won === 0
|
||||
})}
|
||||
value={won}
|
||||
>
|
||||
{won}
|
||||
</data>
|
||||
)}
|
||||
/>
|
||||
<StatPanel
|
||||
name={"Punteggio"}
|
||||
value={(
|
||||
<span
|
||||
className={classNames({
|
||||
"fade": rating === 0
|
||||
})}
|
||||
>
|
||||
{rating === 0
|
||||
?
|
||||
<>
|
||||
<data value={rating}>
|
||||
-
|
||||
</data>
|
||||
</>
|
||||
:
|
||||
<>
|
||||
<data value={rating}>
|
||||
{rating}
|
||||
</data>
|
||||
<span>
|
||||
±
|
||||
</span>
|
||||
<data value={uncertainty}>
|
||||
{uncertainty}
|
||||
</data>
|
||||
</>
|
||||
}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
Loading…
Reference in a new issue