1
Fork 0

it's a good moment to commit

This commit is contained in:
Steffo 2024-11-29 12:56:23 +01:00
parent 14c28762bf
commit c3cf86f6e9
Signed by: steffo
GPG key ID: 5ADA3868646C3FC0
19 changed files with 249 additions and 175 deletions

4
Caddyfile Normal file
View file

@ -0,0 +1,4 @@
:30002 {
reverse_proxy http://localhost:30000
reverse_proxy /api http://localhost:30001
}

View file

@ -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]

View file

@ -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,
}

View file

@ -0,0 +1 @@
pub const MIGRATIONS: diesel_migrations::EmbeddedMigrations = diesel_migrations::embed_migrations!();

View file

@ -0,0 +1,3 @@
pub mod model;
pub mod migrations;
mod schema;

View file

@ -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)
}
}

View file

@ -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(())
}

View 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))
}

View file

@ -0,0 +1,2 @@
pub mod results;
pub mod matches;

View 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))
}

View file

@ -0,0 +1 @@
pub mod webapp;

View 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(())
}

View file

@ -1,6 +0,0 @@
/** @type {import("next").NextConfig} */
const nextConfig = {
output: "export"
};
export default nextConfig;

View file

@ -0,0 +1,7 @@
import {NextConfig} from "next"
const nextConfig: NextConfig = {
output: "export",
};
export default nextConfig;

View file

@ -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"

View file

@ -38,9 +38,7 @@ export default function RootLayout({ children }) {
<p>
© Stefano Pigozzi
&nbsp;-&nbsp;
A quanto pare non posso mettere link esterni qui
&nbsp;-&nbsp;
Garasauto
che cursata, non ha senso, che cursata
</p>
</footer>
</body>

View file

@ -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}
return <main>
</main>
</>
}

View 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>
)
}