feat: Add game thumbnail fetching from external APIs, migrate
This commit is contained in:
@@ -16,6 +16,7 @@ bcrypt = "0.17.1"
|
||||
bincode = "2.0.1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
reqwest = { version = "0.12.24", features = ["json"] }
|
||||
|
||||
[build-dependencies]
|
||||
prost-build = "0.14.1"
|
||||
|
||||
+15
-9
@@ -1,8 +1,8 @@
|
||||
use crate::items;
|
||||
use crate::proto_utils::Proto;
|
||||
use rocket::State;
|
||||
use rocket::futures::lock::Mutex;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct AuthState {
|
||||
@@ -18,6 +18,12 @@ impl AuthState {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AuthState {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct Token {
|
||||
@@ -39,7 +45,7 @@ impl<'r> rocket::request::FromRequest<'r> for Token {
|
||||
// Check if token starts with "Bearer "
|
||||
if let Some(token) = token.strip_prefix("Bearer ") {
|
||||
let state = request.guard::<&State<AuthState>>().await.unwrap();
|
||||
let tokens = state.tokens.lock().unwrap();
|
||||
let tokens = state.tokens.lock().await;
|
||||
|
||||
if let Some(username) = tokens.get(token) {
|
||||
return rocket::request::Outcome::Success(Token {
|
||||
@@ -57,19 +63,19 @@ impl<'r> rocket::request::FromRequest<'r> for Token {
|
||||
}
|
||||
|
||||
#[post("/login", data = "<request>")]
|
||||
pub fn login(
|
||||
pub async fn login(
|
||||
state: &State<AuthState>,
|
||||
user_list: &State<Mutex<Vec<crate::User>>>,
|
||||
request: Proto<items::LoginRequest>,
|
||||
) -> items::LoginResponse {
|
||||
let req = request.into_inner();
|
||||
let users = user_list.lock().unwrap();
|
||||
let users = user_list.lock().await;
|
||||
|
||||
if let Some(user) = users.iter().find(|u| u.person.name == req.username)
|
||||
&& bcrypt::verify(&req.password, &user.password_hash).unwrap_or(false)
|
||||
{
|
||||
let token = Uuid::new_v4().to_string();
|
||||
let mut tokens = state.tokens.lock().unwrap();
|
||||
let mut tokens = state.tokens.lock().await;
|
||||
tokens.insert(token.clone(), req.username);
|
||||
|
||||
return items::LoginResponse {
|
||||
@@ -87,12 +93,12 @@ pub fn login(
|
||||
}
|
||||
|
||||
#[post("/logout", data = "<request>")]
|
||||
pub fn logout(
|
||||
pub async fn logout(
|
||||
state: &State<AuthState>,
|
||||
request: Proto<items::LogoutRequest>,
|
||||
) -> items::LogoutResponse {
|
||||
let req = request.into_inner();
|
||||
let mut tokens = state.tokens.lock().unwrap();
|
||||
let mut tokens = state.tokens.lock().await;
|
||||
|
||||
if tokens.remove(&req.token).is_some() {
|
||||
items::LogoutResponse {
|
||||
@@ -108,12 +114,12 @@ pub fn logout(
|
||||
}
|
||||
|
||||
#[post("/get_auth_status", data = "<request>")]
|
||||
pub fn get_auth_status(
|
||||
pub async fn get_auth_status(
|
||||
state: &State<AuthState>,
|
||||
request: Proto<items::AuthStatusRequest>,
|
||||
) -> items::AuthStatusResponse {
|
||||
let req = request.into_inner();
|
||||
let tokens = state.tokens.lock().unwrap();
|
||||
let tokens = state.tokens.lock().await;
|
||||
|
||||
if let Some(username) = tokens.get(&req.token) {
|
||||
items::AuthStatusResponse {
|
||||
|
||||
+102
-21
@@ -1,5 +1,5 @@
|
||||
use rocket::fs::FileServer;
|
||||
use std::sync::Mutex;
|
||||
use rocket::futures::lock::Mutex;
|
||||
|
||||
use backend::auth;
|
||||
use backend::items::{self, Game};
|
||||
@@ -10,12 +10,12 @@ use backend::store::{self, User, save_state};
|
||||
extern crate rocket;
|
||||
|
||||
#[get("/<name>")]
|
||||
fn get_user(
|
||||
async fn get_user(
|
||||
_token: auth::Token,
|
||||
user_list: &rocket::State<Mutex<Vec<User>>>,
|
||||
name: &str,
|
||||
) -> Option<items::Person> {
|
||||
let users = user_list.lock().unwrap();
|
||||
let users = user_list.lock().await;
|
||||
users
|
||||
.iter()
|
||||
.find(|user| user.person.name == name)
|
||||
@@ -23,42 +23,45 @@ fn get_user(
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
fn get_users(
|
||||
async fn get_users(
|
||||
_token: auth::Token,
|
||||
user_list: &rocket::State<Mutex<Vec<User>>>,
|
||||
) -> items::PersonList {
|
||||
let users = user_list.lock().unwrap();
|
||||
let users = user_list.lock().await;
|
||||
items::PersonList {
|
||||
person: users.iter().map(|u| u.person.clone()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/game/<title>")]
|
||||
fn get_game(
|
||||
async fn get_game(
|
||||
_token: auth::Token,
|
||||
game_list: &rocket::State<Mutex<Vec<Game>>>,
|
||||
title: &str,
|
||||
) -> Option<items::Game> {
|
||||
let games = game_list.lock().unwrap();
|
||||
let games = game_list.lock().await;
|
||||
games.iter().find(|g| g.title == title).cloned()
|
||||
}
|
||||
|
||||
#[get("/games")]
|
||||
fn get_games(_token: auth::Token, game_list: &rocket::State<Mutex<Vec<Game>>>) -> items::GameList {
|
||||
let games = game_list.lock().unwrap();
|
||||
async fn get_games(
|
||||
_token: auth::Token,
|
||||
game_list: &rocket::State<Mutex<Vec<Game>>>,
|
||||
) -> items::GameList {
|
||||
let games = game_list.lock().await;
|
||||
items::GameList {
|
||||
games: games.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/game", data = "<game>")]
|
||||
fn add_game(
|
||||
async fn add_game(
|
||||
_token: auth::Token,
|
||||
game_list: &rocket::State<Mutex<Vec<Game>>>,
|
||||
user_list: &rocket::State<Mutex<Vec<User>>>,
|
||||
game: proto_utils::Proto<items::Game>,
|
||||
) -> Option<items::Game> {
|
||||
let mut games = game_list.lock().unwrap();
|
||||
let mut games = game_list.lock().await;
|
||||
let game = game.into_inner();
|
||||
|
||||
if games.iter().any(|g| g.title == game.title) {
|
||||
@@ -67,21 +70,21 @@ fn add_game(
|
||||
|
||||
games.push(game.clone());
|
||||
|
||||
let users = user_list.lock().unwrap();
|
||||
let users = user_list.lock().await;
|
||||
save_state(&games, &users);
|
||||
|
||||
Some(game)
|
||||
}
|
||||
|
||||
#[post("/opinion", data = "<req>")]
|
||||
fn add_opinion(
|
||||
async fn add_opinion(
|
||||
token: auth::Token,
|
||||
user_list: &rocket::State<Mutex<Vec<User>>>,
|
||||
game_list: &rocket::State<Mutex<Vec<Game>>>,
|
||||
req: proto_utils::Proto<items::AddOpinionRequest>,
|
||||
) -> Option<items::Person> {
|
||||
let mut users = user_list.lock().unwrap();
|
||||
let games = game_list.lock().unwrap();
|
||||
let mut users = user_list.lock().await;
|
||||
let games = game_list.lock().await;
|
||||
let mut result = None;
|
||||
|
||||
// Validate game exists
|
||||
@@ -115,6 +118,68 @@ fn add_opinion(
|
||||
result
|
||||
}
|
||||
|
||||
#[get("/game_thumbnail/<title>")]
|
||||
async fn get_game_thumbnail(
|
||||
title: &str,
|
||||
game_list: &rocket::State<Mutex<Vec<Game>>>,
|
||||
) -> Option<(rocket::http::ContentType, Vec<u8>)> {
|
||||
let games = game_list.lock().await;
|
||||
let game = games.iter().find(|g| g.title == title)?;
|
||||
|
||||
let url = match items::Source::try_from(game.source).ok()? {
|
||||
items::Source::Steam => format!(
|
||||
"https://cdn.cloudflare.steamstatic.com/steam/apps/{}/header.jpg",
|
||||
game.remote_id
|
||||
),
|
||||
items::Source::Roblox => {
|
||||
let universe_id = {
|
||||
let api_url = format!(
|
||||
"https://apis.roblox.com/universes/v1/places/{}/universe",
|
||||
game.remote_id
|
||||
);
|
||||
reqwest::get(&api_url)
|
||||
.await
|
||||
.ok()?
|
||||
.json::<serde_json::Value>()
|
||||
.await
|
||||
.ok()?
|
||||
.get("universeId")?
|
||||
.as_u64()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
let api_url = format!(
|
||||
"https://thumbnails.roblox.com/v1/games/icons?universeIds={}&size=512x512&format=Webp&isCircular=false",
|
||||
universe_id
|
||||
);
|
||||
match reqwest::get(&api_url).await {
|
||||
Ok(resp) => {
|
||||
if let Ok(json) = resp.json::<serde_json::Value>().await {
|
||||
json["data"][0]["imageUrl"].as_str()?.to_string()
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match reqwest::get(&url).await {
|
||||
Ok(resp) => {
|
||||
let content_type = resp
|
||||
.headers()
|
||||
.get(reqwest::header::CONTENT_TYPE)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(rocket::http::ContentType::parse_flexible)
|
||||
.unwrap_or(rocket::http::ContentType::Binary);
|
||||
let bytes = resp.bytes().await.ok()?.to_vec();
|
||||
Some((content_type, bytes))
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/<_..>", rank = 20)]
|
||||
async fn index_fallback() -> Option<rocket::fs::NamedFile> {
|
||||
// Try multiple paths for robustness
|
||||
@@ -139,16 +204,31 @@ async fn main() -> Result<(), std::io::Error> {
|
||||
min_players: 1,
|
||||
max_players: 90,
|
||||
price: 0,
|
||||
remote_id: 0,
|
||||
remote_id: 6032337657, // Universe ID for Naramo Nuclear Plant V2
|
||||
});
|
||||
|
||||
game_list.push(Game {
|
||||
title: "Terraria".to_string(),
|
||||
source: items::Source::Steam.into(),
|
||||
min_players: 1,
|
||||
max_players: 8,
|
||||
price: 999,
|
||||
remote_id: 105600, // App ID for Terraria
|
||||
});
|
||||
|
||||
user_list.push(User {
|
||||
person: items::Person {
|
||||
name: "John".to_string(),
|
||||
opinion: vec![items::Opinion {
|
||||
title: "Naramo Nuclear Plant V2".to_string(),
|
||||
would_play: true,
|
||||
}],
|
||||
opinion: vec![
|
||||
items::Opinion {
|
||||
title: "Naramo Nuclear Plant V2".to_string(),
|
||||
would_play: true,
|
||||
},
|
||||
items::Opinion {
|
||||
title: "Terraria".to_string(),
|
||||
would_play: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
password_hash: bcrypt::hash("password123", bcrypt::DEFAULT_COST).unwrap(),
|
||||
});
|
||||
@@ -167,7 +247,8 @@ async fn main() -> Result<(), std::io::Error> {
|
||||
get_game,
|
||||
get_games,
|
||||
add_opinion,
|
||||
add_game
|
||||
add_game,
|
||||
get_game_thumbnail
|
||||
],
|
||||
)
|
||||
.mount(
|
||||
|
||||
Reference in New Issue
Block a user