feat(api): build bingo card generation
This commit is contained in:
87
Cargo.lock
generated
87
Cargo.lock
generated
@ -439,7 +439,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
|
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
"subtle",
|
"subtle",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
@ -632,7 +632,7 @@ dependencies = [
|
|||||||
"hkdf",
|
"hkdf",
|
||||||
"pem-rfc7468",
|
"pem-rfc7468",
|
||||||
"pkcs8",
|
"pkcs8",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
"sec1",
|
"sec1",
|
||||||
"subtle",
|
"subtle",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
@ -697,7 +697,7 @@ version = "0.13.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
|
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -756,9 +756,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.30"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
|
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
@ -766,15 +766,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.30"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
|
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-executor"
|
name = "futures-executor"
|
||||||
version = "0.3.30"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
|
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
@ -794,27 +794,27 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-io"
|
name = "futures-io"
|
||||||
version = "0.3.30"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
|
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.30"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
|
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-task"
|
name = "futures-task"
|
||||||
version = "0.3.30"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
|
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-util"
|
name = "futures-util"
|
||||||
version = "0.3.30"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
|
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
@ -875,7 +875,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
|
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ff",
|
"ff",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1202,7 +1202,6 @@ dependencies = [
|
|||||||
name = "ice-bingo"
|
name = "ice-bingo"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
|
||||||
"axum",
|
"axum",
|
||||||
"axum-auth",
|
"axum-auth",
|
||||||
"axum-core",
|
"axum-core",
|
||||||
@ -1210,6 +1209,7 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"config",
|
"config",
|
||||||
"openidconnect",
|
"openidconnect",
|
||||||
|
"rand 0.9.1",
|
||||||
"reqwest 0.12.15",
|
"reqwest 0.12.15",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -1450,7 +1450,7 @@ dependencies = [
|
|||||||
"num-integer",
|
"num-integer",
|
||||||
"num-iter",
|
"num-iter",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"rand",
|
"rand 0.8.5",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
@ -1501,7 +1501,7 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"getrandom 0.2.15",
|
"getrandom 0.2.15",
|
||||||
"http 0.2.12",
|
"http 0.2.12",
|
||||||
"rand",
|
"rand 0.8.5",
|
||||||
"reqwest 0.11.27",
|
"reqwest 0.11.27",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -1543,7 +1543,7 @@ dependencies = [
|
|||||||
"oauth2",
|
"oauth2",
|
||||||
"p256",
|
"p256",
|
||||||
"p384",
|
"p384",
|
||||||
"rand",
|
"rand 0.8.5",
|
||||||
"rsa",
|
"rsa",
|
||||||
"serde",
|
"serde",
|
||||||
"serde-value",
|
"serde-value",
|
||||||
@ -1834,8 +1834,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"rand_chacha",
|
"rand_chacha 0.3.1",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
|
||||||
|
dependencies = [
|
||||||
|
"rand_chacha 0.9.0",
|
||||||
|
"rand_core 0.9.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1845,7 +1855,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ppv-lite86",
|
"ppv-lite86",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core 0.9.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1857,6 +1877,15 @@ dependencies = [
|
|||||||
"getrandom 0.2.15",
|
"getrandom 0.2.15",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.6"
|
version = "0.5.6"
|
||||||
@ -2001,7 +2030,7 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
"pkcs1",
|
"pkcs1",
|
||||||
"pkcs8",
|
"pkcs8",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
"signature",
|
"signature",
|
||||||
"spki",
|
"spki",
|
||||||
"subtle",
|
"subtle",
|
||||||
@ -2371,7 +2400,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"digest",
|
"digest",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2541,7 +2570,7 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"rand",
|
"rand 0.8.5",
|
||||||
"rsa",
|
"rsa",
|
||||||
"serde",
|
"serde",
|
||||||
"sha1",
|
"sha1",
|
||||||
@ -2581,7 +2610,7 @@ dependencies = [
|
|||||||
"md-5",
|
"md-5",
|
||||||
"memchr",
|
"memchr",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rand",
|
"rand 0.8.5",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
@ -22,4 +22,4 @@ reqwest = { version = "0.12.15", features = ["json"] }
|
|||||||
strum = { version = "0.27.1", features = ["derive"] }
|
strum = { version = "0.27.1", features = ["derive"] }
|
||||||
strum_macros = "0.27.1"
|
strum_macros = "0.27.1"
|
||||||
chrono = { version = "0.4.41", features = ["alloc"] }
|
chrono = { version = "0.4.41", features = ["alloc"] }
|
||||||
async-trait = "0.1.88"
|
rand = "0.9.1"
|
94
src/api/bingo_card.rs
Normal file
94
src/api/bingo_card.rs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use rand::seq::SliceRandom;
|
||||||
|
use rand::rng;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use sqlx::postgres::PgQueryResult;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use crate::model;
|
||||||
|
use crate::model::bingo_card::BingoCard;
|
||||||
|
use crate::model::database::BingoCardField;
|
||||||
|
use crate::model::database::Train;
|
||||||
|
|
||||||
|
pub(crate) async fn get(id: Uuid, db: Arc<PgPool>) -> sqlx::Result<BingoCard> {
|
||||||
|
let card = sqlx::query_as!(
|
||||||
|
crate::model::database::BingoCard,
|
||||||
|
"SELECT uuid as id FROM bingo_cards WHERE uuid = $1",
|
||||||
|
id,
|
||||||
|
).fetch_one(db.as_ref()).await?;
|
||||||
|
let mut raw_fields = get_fields_with_train(card.id, db.clone())
|
||||||
|
.await?;
|
||||||
|
raw_fields.sort_by(|a, b| (a.y * 5 + a.x).partial_cmp(&(b.y * 5 + b.x)).unwrap());
|
||||||
|
raw_fields.truncate(24);
|
||||||
|
raw_fields.shrink_to_fit();
|
||||||
|
let fields: [Train; 24] = raw_fields
|
||||||
|
.into_iter()
|
||||||
|
.map(|field| field.train)
|
||||||
|
.collect::<Vec<Train>>()
|
||||||
|
.try_into()
|
||||||
|
.unwrap_or_else(|v: Vec<Train>| panic!("Expected a Vec of length 24 but it was {}", v.len()));
|
||||||
|
Ok(BingoCard {
|
||||||
|
id: card.id,
|
||||||
|
fields
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_fields_with_train(id: Uuid, db: Arc<PgPool>) -> sqlx::Result<Vec<BingoCardField>> {
|
||||||
|
sqlx::query_as(
|
||||||
|
"SELECT c.uuid, c.x_pos, c.y_pos, c.card_uuid, t.uuid, t.tz_id, t.name FROM bingo_card c LEFT JOIN triebzug t ON t.uuid = c.triebzug WHERE card_uuid = $1"
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_all(db.as_ref())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn generate(db: Arc<PgPool>) -> sqlx::Result<BingoCard> {
|
||||||
|
let card_id = Uuid::new_v4();
|
||||||
|
let mut all_trains = fetch_all(db.clone()).await?;
|
||||||
|
all_trains.shuffle(&mut rng());
|
||||||
|
all_trains.truncate(24);
|
||||||
|
all_trains.shrink_to_fit();
|
||||||
|
let trains = all_trains.to_owned();
|
||||||
|
let fields = trains
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(idx, train)| BingoCardField {
|
||||||
|
uuid: Uuid::new_v4(),
|
||||||
|
x: (idx as i32).rem_euclid(5),
|
||||||
|
y: (idx as i32).div_euclid(5),
|
||||||
|
train,
|
||||||
|
card_id
|
||||||
|
})
|
||||||
|
.collect::<Vec<BingoCardField>>();
|
||||||
|
let card = model::database::BingoCard {
|
||||||
|
id: card_id,
|
||||||
|
};
|
||||||
|
save_card(card, db.clone()).await?;
|
||||||
|
for field in fields {
|
||||||
|
save_card_field(field, db.clone()).await?;
|
||||||
|
}
|
||||||
|
get(card_id, db.clone()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_card(card: model::database::BingoCard, db: Arc<PgPool>) -> sqlx::Result<PgQueryResult> {
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO bingo_cards VALUES ($1, NOW(), NOW() + interval '7 day')",
|
||||||
|
card.id
|
||||||
|
).execute(db.as_ref()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_card_field(field: BingoCardField, db: Arc<PgPool>) -> sqlx::Result<PgQueryResult> {
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO bingo_card VALUES ($1, $2, $3, $4, $5)",
|
||||||
|
field.uuid,
|
||||||
|
field.x,
|
||||||
|
field.y,
|
||||||
|
field.train.uuid,
|
||||||
|
field.card_id
|
||||||
|
).execute(db.as_ref()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_all(db: Arc<PgPool>) -> sqlx::Result<Vec<Train>> {
|
||||||
|
sqlx::query_as("SELECT * FROM triebzug")
|
||||||
|
.fetch_all(db.as_ref())
|
||||||
|
.await
|
||||||
|
}
|
@ -1,2 +1,3 @@
|
|||||||
pub(crate) mod handlers;
|
pub(crate) mod handlers;
|
||||||
mod db_vendo_navigator;
|
pub(crate) mod bingo_card;
|
||||||
|
mod db_vendo_navigator;
|
||||||
|
@ -22,6 +22,8 @@ async fn main() {
|
|||||||
let state: AppState = AppState::new(config.clone()).await.unwrap();
|
let state: AppState = AppState::new(config.clone()).await.unwrap();
|
||||||
migrate(state.clone()).await.expect("Database migration failed");
|
migrate(state.clone()).await.expect("Database migration failed");
|
||||||
|
|
||||||
|
crate::api::bingo_card::generate(state.db.clone()).await.unwrap();
|
||||||
|
|
||||||
let router = build_router(state);
|
let router = build_router(state);
|
||||||
let listener = listen(config).await;
|
let listener = listen(config).await;
|
||||||
axum::serve::serve(listener, router).await.unwrap()
|
axum::serve::serve(listener, router).await.unwrap()
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::model::train::Train;
|
use crate::model::database::Train;
|
||||||
|
|
||||||
struct BingoCard {
|
pub struct BingoCard {
|
||||||
id: uuid::Uuid,
|
pub id: uuid::Uuid,
|
||||||
fields: [Train; 24],
|
pub fields: [Train; 24],
|
||||||
}
|
}
|
@ -11,4 +11,22 @@ pub struct Train {
|
|||||||
pub uuid: Uuid,
|
pub uuid: Uuid,
|
||||||
pub tz_id: i32,
|
pub tz_id: i32,
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow, Debug, Clone)]
|
||||||
|
pub struct BingoCardField {
|
||||||
|
pub uuid: Uuid,
|
||||||
|
#[sqlx(rename = "x_pos")]
|
||||||
|
pub x: i32,
|
||||||
|
#[sqlx(rename = "y_pos")]
|
||||||
|
pub y: i32,
|
||||||
|
#[sqlx(rename = "card_uuid")]
|
||||||
|
pub card_id: Uuid,
|
||||||
|
#[sqlx(flatten, rename = "triebzug")]
|
||||||
|
pub train: Train,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow, Debug, Clone)]
|
||||||
|
pub struct BingoCard {
|
||||||
|
pub id: Uuid,
|
||||||
}
|
}
|
@ -2,7 +2,7 @@ pub mod uic;
|
|||||||
pub mod db_vendo_navigator_api;
|
pub mod db_vendo_navigator_api;
|
||||||
pub(crate) mod travelynx;
|
pub(crate) mod travelynx;
|
||||||
pub(crate) mod traewelling;
|
pub(crate) mod traewelling;
|
||||||
mod bingo_card;
|
pub(crate) mod bingo_card;
|
||||||
mod train;
|
pub(crate) mod train;
|
||||||
pub(crate) mod app;
|
pub(crate) mod app;
|
||||||
pub(crate) mod database;
|
pub(crate) mod database;
|
@ -7,8 +7,8 @@ CREATE TABLE IF NOT EXISTS triebzug (
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS bingo_cards (
|
CREATE TABLE IF NOT EXISTS bingo_cards (
|
||||||
uuid UUID PRIMARY KEY,
|
uuid UUID PRIMARY KEY,
|
||||||
start_time INT NOT NULL,
|
start_time TIMESTAMP NOT NULL,
|
||||||
end_time INT NOT NULL
|
end_time TIMESTAMP NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS bingo_card (
|
CREATE TABLE IF NOT EXISTS bingo_card (
|
||||||
|
Reference in New Issue
Block a user