+7
-1
@@ -16,4 +16,10 @@ target/
|
|||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
token.key
|
token.key
|
||||||
|
|
||||||
|
# Added by cargo
|
||||||
|
|
||||||
|
/target
|
||||||
|
|
||||||
|
*.db
|
||||||
Generated
+1874
-993
File diff suppressed because it is too large
Load Diff
+12
-7
@@ -1,11 +1,16 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "auraclient-api"
|
name = "server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base64 = "0.22.1"
|
reqwest = { version = "0.13.2", features = ["json"] }
|
||||||
minreq = { version = "2.13.2", features = ["https", "urlencoding", "json-using-serde"] }
|
serde = { version = "1.0.228", features = ["serde_derive"] }
|
||||||
mongodb = { version = "3.2.1", features = ["sync"] }
|
serde_json = "1.0.149"
|
||||||
openssl = "0.10.72"
|
session-rs = "0.1.2"
|
||||||
serde = { version ="1.0.218", features = ["derive"] }
|
sqlx = { version = "0.8.6", features = [
|
||||||
|
"sqlite",
|
||||||
|
"runtime-tokio-native-tls",
|
||||||
|
"macros",
|
||||||
|
] }
|
||||||
|
tokio = "1.49.0"
|
||||||
|
|||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
# -------------------------
|
||||||
|
# 1️⃣ Build Stage
|
||||||
|
# -------------------------
|
||||||
|
FROM rust:1.76 as builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies required for sqlx + native-tls
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
pkg-config \
|
||||||
|
libssl-dev \
|
||||||
|
ca-certificates \
|
||||||
|
sqlite3 \
|
||||||
|
libsqlite3-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Cache dependencies
|
||||||
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
RUN mkdir src && echo "fn main() {}" > src/main.rs
|
||||||
|
RUN cargo build --release
|
||||||
|
RUN rm -rf src
|
||||||
|
|
||||||
|
# Copy real source
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build release binary
|
||||||
|
RUN cargo build --release
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# 2️⃣ Runtime Stage
|
||||||
|
# -------------------------
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install runtime deps only
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
ca-certificates \
|
||||||
|
libssl3 \
|
||||||
|
sqlite3 \
|
||||||
|
libsqlite3-0 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy compiled binary
|
||||||
|
COPY --from=builder /app/target/release/server /app/server
|
||||||
|
|
||||||
|
# Create data directory
|
||||||
|
RUN mkdir -p /data
|
||||||
|
|
||||||
|
# Expose port (change if needed)
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Declare volume for SQLite database
|
||||||
|
VOLUME ["/data"]
|
||||||
|
|
||||||
|
# Run the server
|
||||||
|
CMD ["./server"]
|
||||||
@@ -1,32 +1,3 @@
|
|||||||
# SaturnClient API
|
# SaturnClient API (v2)
|
||||||
|
|
||||||
The api for AuraClient which is a Minecraft mod Client
|
The api for AuraClient which is a Minecraft mod Client
|
||||||
|
|
||||||
# Handshake
|
|
||||||
In order to communicate with the server, you will have to make a handshake, here is how
|
|
||||||
```
|
|
||||||
<MOJANG-SESSION-TOKEN>
|
|
||||||
```
|
|
||||||
The session token is stored on your minecraft client when it communicates with Mojang servers, as you might understand, this token is a private token used for authentication with Mojang servers, Why do we need it? Because our server needs to understand that whoever logs in with SaturnClient is you and when you make changes in your client, the changes get synced with others, Example: Changing Cloak.
|
|
||||||
|
|
||||||
# Protocol base
|
|
||||||
Every request should be only one line
|
|
||||||
|
|
||||||
```
|
|
||||||
method@key1=value1@key2=value2
|
|
||||||
```
|
|
||||||
|
|
||||||
# Error handling
|
|
||||||
request
|
|
||||||
```
|
|
||||||
invalid-method
|
|
||||||
```
|
|
||||||
response
|
|
||||||
```
|
|
||||||
!Invalid method
|
|
||||||
```
|
|
||||||
On a response `!` means it's an error.
|
|
||||||
|
|
||||||
# Methods
|
|
||||||
### `set_cloak`
|
|
||||||
- `cloak` -> `string`
|
|
||||||
|
|||||||
+107
-1
@@ -1,3 +1,109 @@
|
|||||||
pub const CLOAKS: &[&str] = &["mercedes_flow", "glitch", "crimson_mark", "bmw", "amg", "amg_petronas", "ferrari", "redbull", "black_hole_amethyst", "black_hole_flame", "black_hole_white"];
|
use sqlx::SqlitePool;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::{types::UUID, user};
|
||||||
|
|
||||||
|
pub const CLOAKS: &[&str] = &[
|
||||||
|
"glitch",
|
||||||
|
"mercedes_flow",
|
||||||
|
"crimson_mark",
|
||||||
|
"bmw",
|
||||||
|
"amg",
|
||||||
|
"amg_petronas",
|
||||||
|
"ferrari",
|
||||||
|
"redbull",
|
||||||
|
"black_hole_amethyst",
|
||||||
|
"black_hole_flame",
|
||||||
|
"black_hole_white",
|
||||||
|
];
|
||||||
|
|
||||||
pub const HATS: &[&str] = &["horns_black", "horns_white", "halo", "halo_black"];
|
pub const HATS: &[&str] = &["horns_black", "horns_white", "halo", "halo_black"];
|
||||||
|
|
||||||
|
pub enum CosmeticKind {
|
||||||
|
Hat,
|
||||||
|
Cloak,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buy a cosmetic
|
||||||
|
pub async fn buy(
|
||||||
|
kind: CosmeticKind,
|
||||||
|
uuid: UUID,
|
||||||
|
item_id: String,
|
||||||
|
pool: Arc<SqlitePool>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
// Lock once
|
||||||
|
let uuid = uuid.lock().await.clone();
|
||||||
|
|
||||||
|
let mut user = user::get(&uuid, &pool).await?;
|
||||||
|
|
||||||
|
match kind {
|
||||||
|
CosmeticKind::Hat => {
|
||||||
|
if !HATS.contains(&item_id.as_str()) {
|
||||||
|
return Err("Hat does not exist".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !user.hats.contains(&item_id) {
|
||||||
|
user.hats.push(item_id.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CosmeticKind::Cloak => {
|
||||||
|
if !CLOAKS.contains(&item_id.as_str()) {
|
||||||
|
return Err("Cloak does not exist".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !user.cloaks.contains(&item_id) {
|
||||||
|
user.cloaks.push(item_id.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlx::query("UPDATE users SET cloaks = ?, hats = ? WHERE uuid = ?")
|
||||||
|
.bind(serde_json::to_string(&user.cloaks).map_err(|e| e.to_string())?)
|
||||||
|
.bind(serde_json::to_string(&user.hats).map_err(|e| e.to_string())?)
|
||||||
|
.bind(&uuid)
|
||||||
|
.execute(pool.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
Ok(format!("Item {} bought successfully!", item_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equip a cosmetic
|
||||||
|
pub async fn equip(
|
||||||
|
kind: CosmeticKind,
|
||||||
|
uuid: UUID,
|
||||||
|
item_id: String,
|
||||||
|
pool: Arc<SqlitePool>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
// Lock once
|
||||||
|
let uuid = uuid.lock().await.clone();
|
||||||
|
|
||||||
|
let mut user = user::get(&uuid, &pool).await?;
|
||||||
|
|
||||||
|
match kind {
|
||||||
|
CosmeticKind::Hat => {
|
||||||
|
if !user.hats.contains(&item_id) {
|
||||||
|
return Err("You don't own this hat".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
user.hat = item_id.clone();
|
||||||
|
}
|
||||||
|
CosmeticKind::Cloak => {
|
||||||
|
if !user.cloaks.contains(&item_id) {
|
||||||
|
return Err("You don't own this cloak".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
user.cloak = item_id.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlx::query("UPDATE users SET cloak = ?, hat = ? WHERE uuid = ?")
|
||||||
|
.bind(&user.cloak)
|
||||||
|
.bind(&user.hat)
|
||||||
|
.bind(&uuid)
|
||||||
|
.execute(pool.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
Ok(format!("Item {} equipped successfully!", item_id))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
|
||||||
pub struct Player {
|
|
||||||
pub uuid: String,
|
|
||||||
pub cloaks: Vec<String>,
|
|
||||||
pub cloak: String,
|
|
||||||
pub hats: Vec<String>,
|
|
||||||
pub hat: String,
|
|
||||||
pub coins: u32,
|
|
||||||
pub online: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Database {
|
|
||||||
pub players: mongodb::sync::Collection<Player>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Database {
|
|
||||||
pub fn new(client: &mongodb::sync::Client) -> Self {
|
|
||||||
let db = client.database("saturnclient");
|
|
||||||
Self {
|
|
||||||
players: db.collection("players"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
use std::{
|
|
||||||
fmt::Display,
|
|
||||||
io::{BufRead, BufReader, Write},
|
|
||||||
net::TcpStream,
|
|
||||||
};
|
|
||||||
|
|
||||||
use openssl::{
|
|
||||||
pkey::Private,
|
|
||||||
rsa::Rsa,
|
|
||||||
symm::{decrypt, encrypt, Cipher},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::response::Error;
|
|
||||||
|
|
||||||
fn aes_encrypt(key: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, Error> {
|
|
||||||
let cipher = Cipher::aes_256_ecb();
|
|
||||||
encrypt(cipher, key, None, plaintext)
|
|
||||||
.map_err(|e| Error::EncryptionError(format!("Failed to encrypt: {}", e)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn aes_decrypt(key: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, Error> {
|
|
||||||
let cipher = Cipher::aes_256_ecb();
|
|
||||||
decrypt(cipher, key, None, ciphertext)
|
|
||||||
.map_err(|e| Error::EncryptionError(format!("Failed to decrypt: {}", e)))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(deprecated)]
|
|
||||||
pub fn handshake(mut stream: TcpStream, rsa: Rsa<Private>) -> Result<ETcp, Error> {
|
|
||||||
let stream_clone = stream
|
|
||||||
.try_clone()
|
|
||||||
.map_err(|e| Error::NetworkError(format!("Failed to clone stream: {}", e)))?;
|
|
||||||
let mut reader = BufReader::new(stream_clone);
|
|
||||||
|
|
||||||
let public_key = rsa
|
|
||||||
.public_key_to_pem()
|
|
||||||
.map_err(|e| Error::EncryptionError(format!("Failed to get public key: {}", e)))?;
|
|
||||||
stream
|
|
||||||
.write_all(&public_key)
|
|
||||||
.map_err(|e| Error::NetworkError(format!("Failed to write public key: {}", e)))?;
|
|
||||||
|
|
||||||
stream
|
|
||||||
.flush()
|
|
||||||
.map_err(|e| Error::NetworkError(format!("Failed to flush stream: {}", e)))?;
|
|
||||||
|
|
||||||
let mut aes_encoded = String::new();
|
|
||||||
reader
|
|
||||||
.read_line(&mut aes_encoded)
|
|
||||||
.map_err(|e| Error::NetworkError(format!("Failed to read AES key: {}", e)))?;
|
|
||||||
|
|
||||||
let encrypted_data = base64::decode(aes_encoded.trim())
|
|
||||||
.map_err(|e| Error::EncryptionError(format!("Failed to decode base64: {}", e)))?;
|
|
||||||
|
|
||||||
let mut aes_decrypted = vec![0; 256];
|
|
||||||
let aes_len = rsa
|
|
||||||
.private_decrypt(
|
|
||||||
&encrypted_data,
|
|
||||||
&mut aes_decrypted,
|
|
||||||
openssl::rsa::Padding::PKCS1,
|
|
||||||
)
|
|
||||||
.map_err(|e| Error::EncryptionError(format!("Failed to decrypt AES key: {}", e)))?;
|
|
||||||
|
|
||||||
println!("len: {}", aes_len);
|
|
||||||
aes_decrypted.truncate(aes_len);
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"Decrypted AES key ({} bytes): {:?}",
|
|
||||||
aes_decrypted.len(),
|
|
||||||
aes_decrypted
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(ETcp {
|
|
||||||
stream,
|
|
||||||
aes: aes_decrypted,
|
|
||||||
reader,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ETcp {
|
|
||||||
stream: TcpStream,
|
|
||||||
reader: BufReader<TcpStream>,
|
|
||||||
aes: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ETcp {
|
|
||||||
#[allow(deprecated)]
|
|
||||||
pub fn send<T: Display>(&mut self, d: T) -> Result<(), Error> {
|
|
||||||
let encrypted = aes_encrypt(&self.aes, d.to_string().as_bytes())?;
|
|
||||||
let encoded = base64::encode(encrypted) + "\n";
|
|
||||||
|
|
||||||
self.stream
|
|
||||||
.write_all(encoded.as_bytes())
|
|
||||||
.map_err(|e| Error::NetworkError(format!("Failed to write to stream: {}", e)))?;
|
|
||||||
|
|
||||||
self.stream
|
|
||||||
.flush()
|
|
||||||
.map_err(|e| Error::NetworkError(format!("Failed to flush stream: {}", e)))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(deprecated)]
|
|
||||||
pub fn read(&mut self) -> Result<Option<String>, Error> {
|
|
||||||
let mut v_encoded = String::new();
|
|
||||||
|
|
||||||
if self
|
|
||||||
.reader
|
|
||||||
.read_line(&mut v_encoded)
|
|
||||||
.map_err(|e| Error::NetworkError(format!("Failed to read line: {}", e)))?
|
|
||||||
== 0
|
|
||||||
{
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let encrypted_data = base64::decode(v_encoded.trim())
|
|
||||||
.map_err(|e| Error::EncryptionError(format!("Failed to decode base64: {}", e)))?;
|
|
||||||
|
|
||||||
let decrypted = aes_decrypt(&self.aes, &encrypted_data)?;
|
|
||||||
let text = String::from_utf8(decrypted)
|
|
||||||
.map_err(|e| Error::EncryptionError(format!("Failed to decode UTF-8: {}", e)))?;
|
|
||||||
|
|
||||||
Ok(Some(text))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn close(&self) -> Result<(), Error> {
|
|
||||||
self.stream
|
|
||||||
.shutdown(std::net::Shutdown::Both)
|
|
||||||
.map_err(|e| Error::NetworkError(format!("Failed to shutdown stream: {}", e)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn try_clone(&mut self) -> Result<Self, Error> {
|
|
||||||
let stream = self
|
|
||||||
.stream
|
|
||||||
.try_clone()
|
|
||||||
.map_err(|e| Error::NetworkError(format!("Failed to clone stream: {}", e)))?;
|
|
||||||
let stream_clone = stream
|
|
||||||
.try_clone()
|
|
||||||
.map_err(|e| Error::NetworkError(format!("Failed to clone stream: {}", e)))?;
|
|
||||||
|
|
||||||
Ok(ETcp {
|
|
||||||
reader: BufReader::new(stream_clone),
|
|
||||||
stream,
|
|
||||||
aes: self.aes.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Clone for ETcp {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
let stream = self
|
|
||||||
.stream
|
|
||||||
.try_clone()
|
|
||||||
.map_err(|e| Error::NetworkError(format!("Failed to clone stream: {}", e)))
|
|
||||||
.unwrap();
|
|
||||||
let stream_clone = stream
|
|
||||||
.try_clone()
|
|
||||||
.map_err(|e| Error::NetworkError(format!("Failed to clone stream: {}", e)))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
ETcp {
|
|
||||||
reader: BufReader::new(stream_clone),
|
|
||||||
stream,
|
|
||||||
aes: self.aes.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+197
-98
@@ -1,114 +1,213 @@
|
|||||||
use std::{
|
mod cosmetics;
|
||||||
net::{TcpListener, TcpStream},
|
mod methods;
|
||||||
sync::Arc,
|
mod types;
|
||||||
time::{Duration, Instant},
|
mod user;
|
||||||
};
|
|
||||||
|
|
||||||
use openssl::{pkey::Private, rsa::Rsa};
|
use std::{collections::HashMap, pin::Pin, sync::Arc};
|
||||||
|
|
||||||
use crate::methods::SocketMap;
|
use session_rs::server::SessionServer;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
pub mod cosmetics;
|
use crate::types::SessionMap;
|
||||||
pub mod database;
|
|
||||||
pub mod encryption;
|
|
||||||
pub mod methods;
|
|
||||||
pub mod parser;
|
|
||||||
pub mod response;
|
|
||||||
|
|
||||||
fn main() -> Result<(), response::Error> {
|
#[tokio::main(flavor = "current_thread")]
|
||||||
let rsa = Rsa::generate(2048).map_err(|e| {
|
async fn main() -> session_rs::Result<()> {
|
||||||
response::Error::EncryptionError(format!("Failed to generate RSA keys: {}", e))
|
let pool = Arc::new(user::init_db().await);
|
||||||
})?;
|
let server = SessionServer::bind("127.0.0.1:8080").await?;
|
||||||
|
|
||||||
let client =
|
let sessions: SessionMap = Arc::new(Mutex::new(HashMap::new()));
|
||||||
mongodb::sync::Client::with_uri_str("mongodb://admin:[email protected]/").map_err(|e| {
|
|
||||||
response::Error::DatabaseError(format!("Failed to connect to MongoDB: {}", e))
|
|
||||||
})?;
|
|
||||||
let database = Arc::new(database::Database::new(&client));
|
|
||||||
let sockets = SocketMap::default();
|
|
||||||
|
|
||||||
let listener = TcpListener::bind("0.0.0.0:8080").map_err(|e| {
|
server
|
||||||
response::Error::NetworkError(format!("Failed to bind to port 8080: {}", e))
|
.session_loop({
|
||||||
})?;
|
let pool = Arc::clone(&pool);
|
||||||
|
move |session, _| {
|
||||||
|
let pool = Arc::clone(&pool);
|
||||||
|
let sessions = Arc::clone(&sessions);
|
||||||
|
|
||||||
println!("Server listening on port 8080");
|
Box::pin(async move {
|
||||||
|
println!("Connected");
|
||||||
|
let uuid = Arc::new(Mutex::new(String::new()));
|
||||||
|
let name = Arc::new(Mutex::new(String::new()));
|
||||||
|
|
||||||
for stream_result in listener.incoming() {
|
session
|
||||||
let stream = match stream_result {
|
.on_close({
|
||||||
Ok(stream) => stream,
|
let session = session.clone();
|
||||||
Err(e) => {
|
let uuid = uuid.clone();
|
||||||
eprintln!("Failed to accept connection: {}", e);
|
let sessions = Arc::clone(&sessions);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
println!("New connection");
|
move || {
|
||||||
let database = Arc::clone(&database);
|
let uuid = uuid.clone();
|
||||||
let sockets = Arc::clone(&sockets);
|
let sessions = Arc::clone(&sessions);
|
||||||
let rsa = rsa.clone();
|
let session = session.clone();
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
Box::pin(async move {
|
||||||
if let Err(e) = handle_client(stream, rsa, database, sockets) {
|
let uuid_lock = uuid.lock().await;
|
||||||
eprintln!("Client error: {}", e);
|
if !uuid_lock.is_empty() {
|
||||||
}
|
if let Some(sessions) =
|
||||||
});
|
sessions.lock().await.get_mut(uuid_lock.as_str())
|
||||||
}
|
{
|
||||||
|
sessions.remove(&session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
Ok(())
|
session.start_ping(
|
||||||
}
|
tokio::time::Duration::from_secs(30),
|
||||||
|
tokio::time::Duration::from_secs(5),
|
||||||
fn handle_client(
|
|
||||||
stream: TcpStream,
|
|
||||||
rsa: Rsa<Private>,
|
|
||||||
database: Arc<database::Database>,
|
|
||||||
sockets: SocketMap,
|
|
||||||
) -> Result<(), response::Error> {
|
|
||||||
let mut stream = encryption::handshake(stream, rsa)?;
|
|
||||||
let mut last_activity = Instant::now();
|
|
||||||
|
|
||||||
match methods::Session::new(stream.try_clone()?, database, sockets) {
|
|
||||||
Ok((session, res)) => {
|
|
||||||
stream.send(res)?;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
// Check for inactivity
|
|
||||||
if last_activity.elapsed() > Duration::from_secs(60) {
|
|
||||||
println!(
|
|
||||||
"[MOJANG] {} inactive for too long",
|
|
||||||
session.local_player.name
|
|
||||||
);
|
);
|
||||||
methods::player::logout(&session)?;
|
|
||||||
println!("[MOJANG] {} went offline", session.local_player.name);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
match stream.read()? {
|
session
|
||||||
None => {
|
.on_request::<methods::Auth, _>({
|
||||||
// Client disconnected
|
let pool = Arc::clone(&pool);
|
||||||
println!("[MOJANG] {} disconnected", session.local_player.name);
|
let uuid = Arc::clone(&uuid);
|
||||||
methods::player::logout(&session)?;
|
let sessions = Arc::clone(&sessions);
|
||||||
println!("[MOJANG] {} went offline", session.local_player.name);
|
let name = Arc::clone(&name);
|
||||||
break;
|
let session = session.clone();
|
||||||
}
|
|
||||||
Some(request_string) => {
|
|
||||||
last_activity = Instant::now();
|
|
||||||
let (method, params) = parser::parse(&request_string)?;
|
|
||||||
let response = session.handle_request(&method, ¶ms);
|
|
||||||
|
|
||||||
stream.send(match response {
|
move |_, token| {
|
||||||
Ok(response) => response.to_string(),
|
methods::auth::authenticate(
|
||||||
Err(e) => format!("!{e}"),
|
sessions.clone(),
|
||||||
})?;
|
session.clone(),
|
||||||
}
|
name.clone(),
|
||||||
}
|
uuid.clone(),
|
||||||
|
token,
|
||||||
|
pool.clone(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
session
|
||||||
|
.on_request::<methods::BuyCloak, _>({
|
||||||
|
let uuid = Arc::clone(&uuid);
|
||||||
|
let pool = Arc::clone(&pool);
|
||||||
|
|
||||||
|
move |_, item_id| {
|
||||||
|
let pool = Arc::clone(&pool);
|
||||||
|
let item_id = item_id.clone();
|
||||||
|
|
||||||
|
cosmetics::buy(
|
||||||
|
cosmetics::CosmeticKind::Cloak,
|
||||||
|
uuid.clone(),
|
||||||
|
item_id,
|
||||||
|
pool,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
session
|
||||||
|
.on_request::<methods::BuyHat, _>({
|
||||||
|
let uuid = Arc::clone(&uuid);
|
||||||
|
let pool = Arc::clone(&pool);
|
||||||
|
|
||||||
|
move |_, item_id| {
|
||||||
|
let pool = Arc::clone(&pool);
|
||||||
|
let item_id = item_id.clone();
|
||||||
|
|
||||||
|
cosmetics::buy(
|
||||||
|
cosmetics::CosmeticKind::Hat,
|
||||||
|
uuid.clone(),
|
||||||
|
item_id,
|
||||||
|
pool,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
session
|
||||||
|
.on_request::<methods::SetCloak, _>({
|
||||||
|
let uuid = Arc::clone(&uuid);
|
||||||
|
let pool = Arc::clone(&pool);
|
||||||
|
|
||||||
|
move |_, item_id| {
|
||||||
|
let pool = Arc::clone(&pool);
|
||||||
|
let item_id = item_id.clone();
|
||||||
|
|
||||||
|
cosmetics::equip(
|
||||||
|
cosmetics::CosmeticKind::Cloak,
|
||||||
|
uuid.clone(),
|
||||||
|
item_id,
|
||||||
|
pool,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
session
|
||||||
|
.on_request::<methods::SetHat, _>({
|
||||||
|
let uuid = Arc::clone(&uuid);
|
||||||
|
let pool = Arc::clone(&pool);
|
||||||
|
|
||||||
|
move |_, item_id| {
|
||||||
|
let pool = Arc::clone(&pool);
|
||||||
|
let item_id = item_id.clone();
|
||||||
|
|
||||||
|
cosmetics::equip(
|
||||||
|
cosmetics::CosmeticKind::Hat,
|
||||||
|
uuid.clone(),
|
||||||
|
item_id,
|
||||||
|
pool,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
session
|
||||||
|
.on_request::<methods::Emote, _>({
|
||||||
|
let sessions = Arc::clone(&sessions);
|
||||||
|
let uuid = Arc::clone(&uuid);
|
||||||
|
|
||||||
|
move |_, emote| {
|
||||||
|
let sessions = Arc::clone(&sessions);
|
||||||
|
let uuid = Arc::clone(&uuid);
|
||||||
|
|
||||||
|
methods::emote::send_emote(sessions, uuid, emote)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
session
|
||||||
|
.on_request::<methods::GetPlayer, _>({
|
||||||
|
let sessions = Arc::clone(&sessions);
|
||||||
|
let pool = Arc::clone(&pool);
|
||||||
|
|
||||||
|
move |_, uuid| {
|
||||||
|
methods::user::get_user(
|
||||||
|
sessions.clone(),
|
||||||
|
uuid.clone(),
|
||||||
|
pool.clone(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
session
|
||||||
|
.on_request::<methods::SendPlayer, _>({
|
||||||
|
let sessions = Arc::clone(&sessions);
|
||||||
|
let pool = Arc::clone(&pool);
|
||||||
|
let uuid = Arc::clone(&uuid);
|
||||||
|
let name = Arc::clone(&name);
|
||||||
|
|
||||||
|
move |_, targets| {
|
||||||
|
methods::user::send_user(
|
||||||
|
sessions.clone(),
|
||||||
|
name.clone(),
|
||||||
|
uuid.clone(),
|
||||||
|
targets.targets.clone(),
|
||||||
|
pool.clone(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok::<(), session_rs::Error>(())
|
||||||
|
}) as Pin<Box<dyn Future<Output = _> + Send>>
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
Err(e) => {
|
.await
|
||||||
stream.send(format!("!{e}"))?;
|
|
||||||
println!("Disconnected");
|
|
||||||
stream.close()?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use session_rs::session::Session;
|
||||||
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
types::{SessionMap, UUID},
|
||||||
|
user::User,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn authenticate(
|
||||||
|
sessions: SessionMap,
|
||||||
|
session: Session,
|
||||||
|
name: UUID,
|
||||||
|
uuid: UUID,
|
||||||
|
session_token: String,
|
||||||
|
pool: Arc<SqlitePool>,
|
||||||
|
) -> Result<User, String> {
|
||||||
|
if !uuid.lock().await.is_empty() {
|
||||||
|
return Err(format!("Already authenticated"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.get("https://api.minecraftservices.com/minecraft/profile")
|
||||||
|
.bearer_auth(&session_token)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|_| "Failed to validate session".to_string())?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(format!(
|
||||||
|
"Authentication failed with code {}",
|
||||||
|
response.status()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let auth = response
|
||||||
|
.json::<crate::types::MinecraftAuthResponse>()
|
||||||
|
.await
|
||||||
|
.map_err(|_| "Unable to parse auth response".to_string())
|
||||||
|
.and_then(|auth| {
|
||||||
|
let id = crate::types::format_uuid(&auth.id)?;
|
||||||
|
Ok(crate::types::MinecraftAuthResponse {
|
||||||
|
name: auth.name,
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
*uuid.lock().await = auth.id.clone();
|
||||||
|
*name.lock().await = auth.name.clone();
|
||||||
|
|
||||||
|
sessions
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.entry(auth.id.clone())
|
||||||
|
.or_default()
|
||||||
|
.insert(session);
|
||||||
|
|
||||||
|
println!("{:?}", auth);
|
||||||
|
|
||||||
|
crate::user::get_put(&auth.id, &pool).await
|
||||||
|
}
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
use mongodb::bson::doc;
|
|
||||||
|
|
||||||
use crate::{methods::Session, response::Result};
|
|
||||||
|
|
||||||
pub fn buy_cloak(session: &Session, cloak: String) -> Result {
|
|
||||||
let filter = doc! {
|
|
||||||
"uuid": session.local_player.id.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(doc) = session.database.players.find_one(filter.clone()).run()? {
|
|
||||||
if doc.coins >= 100 && !doc.cloaks.contains(&cloak) {
|
|
||||||
session
|
|
||||||
.database
|
|
||||||
.players
|
|
||||||
.update_one(
|
|
||||||
filter,
|
|
||||||
doc! { "$inc": { "coins": -100 }, "$push": { "cloaks": cloak.clone() } },
|
|
||||||
)
|
|
||||||
.run()?;
|
|
||||||
Ok(crate::response::Response::SuccessfulTransaction(format!(
|
|
||||||
"cloak={cloak}"
|
|
||||||
)))
|
|
||||||
} else {
|
|
||||||
Err(crate::response::Error::TransactionError(
|
|
||||||
"Not enough coins".to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(crate::response::Error::DatabaseError(
|
|
||||||
"Player not found".to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn buy_hat(session: &Session, hat: String) -> Result {
|
|
||||||
let filter = doc! {
|
|
||||||
"uuid": session.local_player.id.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(doc) = session.database.players.find_one(filter.clone()).run()? {
|
|
||||||
if doc.coins >= 50 && !doc.hats.contains(&hat) {
|
|
||||||
session
|
|
||||||
.database
|
|
||||||
.players
|
|
||||||
.update_one(
|
|
||||||
filter,
|
|
||||||
doc! { "$inc": { "coins": -50 }, "$push": { "hats": hat.clone() } },
|
|
||||||
)
|
|
||||||
.run()?;
|
|
||||||
Ok(crate::response::Response::SuccessfulTransaction(format!(
|
|
||||||
"hat={hat}"
|
|
||||||
)))
|
|
||||||
} else {
|
|
||||||
Err(crate::response::Error::TransactionError(
|
|
||||||
"Not enough coins".to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(crate::response::Error::DatabaseError(
|
|
||||||
"Player not found".to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+29
-9
@@ -1,14 +1,34 @@
|
|||||||
use crate::response::{Response, Result};
|
use crate::types::{SessionMap, UUID};
|
||||||
|
|
||||||
use super::Session;
|
pub async fn send_emote(
|
||||||
|
sessions: SessionMap,
|
||||||
|
uuid: UUID,
|
||||||
|
emote: crate::types::EmoteRequest,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let emote_event = crate::types::EventEmote {
|
||||||
|
from: uuid.lock().await.clone(),
|
||||||
|
emote: emote.emote,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn emote(session: &Session, emote: String, notify: Vec<&str>) -> Result {
|
for target in emote.targets {
|
||||||
if notify.len() > 0 {
|
if let Some(sessions) = sessions.lock().await.get_mut(&target) {
|
||||||
session.notify(
|
let emote_event = emote_event.clone();
|
||||||
¬ify,
|
let mut bad_sessions = Vec::new();
|
||||||
&format!("emote@uuid={}@name={emote}", session.local_player.id),
|
|
||||||
)?;
|
for s in sessions.iter() {
|
||||||
|
if s.notify::<crate::methods::EmoteEvent>(emote_event.clone())
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
bad_sessions.push(s.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for s in bad_sessions {
|
||||||
|
sessions.remove(&s);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Response::Success)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+100
-181
@@ -1,197 +1,116 @@
|
|||||||
mod buy;
|
pub mod auth;
|
||||||
mod emote;
|
pub mod emote;
|
||||||
pub mod player;
|
pub mod user;
|
||||||
mod set_cloak;
|
|
||||||
mod set_hat;
|
|
||||||
|
|
||||||
use std::{
|
|
||||||
collections::HashMap,
|
|
||||||
sync::{mpsc, Arc, Mutex},
|
|
||||||
};
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use session_rs::Method;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
encryption::ETcp,
|
types::{EmoteRequest, EventEmote, PlayerStream},
|
||||||
parser::ParamMap,
|
user::User,
|
||||||
response::{PlayerResponse, Response, Result},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct LocalPlayer {
|
pub struct Auth;
|
||||||
pub id: String,
|
|
||||||
pub name: String,
|
impl Method for Auth {
|
||||||
|
const NAME: &'static str = "auth";
|
||||||
|
type Request = String;
|
||||||
|
type Response = User;
|
||||||
|
type Error = String;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type SocketMap = Arc<Mutex<HashMap<String, ETcp>>>;
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SetCloak;
|
||||||
|
|
||||||
pub struct Session {
|
impl Method for SetCloak {
|
||||||
pub session_token: String,
|
const NAME: &'static str = "set_cloak";
|
||||||
pub database: Arc<crate::database::Database>,
|
type Request = String;
|
||||||
pub local_player: LocalPlayer,
|
type Response = String;
|
||||||
sockets: SocketMap,
|
type Error = String;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Session {
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub fn new(
|
pub struct SetHat;
|
||||||
mut stream: ETcp,
|
|
||||||
database: Arc<crate::database::Database>,
|
|
||||||
sockets: SocketMap,
|
|
||||||
) -> Result<(Self, Response)> {
|
|
||||||
let (token_send, token_recv) =
|
|
||||||
mpsc::channel::<std::result::Result<String, crate::response::Error>>();
|
|
||||||
|
|
||||||
let stream_2 = stream.try_clone()?;
|
impl Method for SetHat {
|
||||||
|
const NAME: &'static str = "set_hat";
|
||||||
std::thread::spawn(move || match stream.read() {
|
type Request = String;
|
||||||
Ok(Some(session_token)) => token_send.send(Ok(session_token)),
|
type Response = String;
|
||||||
_ => token_send.send(Err(crate::response::Error::InvalidHandshake(
|
type Error = String;
|
||||||
"Failed to read session token".to_string(),
|
|
||||||
))),
|
|
||||||
});
|
|
||||||
|
|
||||||
match token_recv.recv_timeout(std::time::Duration::from_secs(20)) {
|
|
||||||
Ok(Ok(session_token)) => {
|
|
||||||
// Validate session id
|
|
||||||
let response = minreq::get("https://api.minecraftservices.com/minecraft/profile")
|
|
||||||
.with_header("Authorization", &format!("Bearer {session_token}"))
|
|
||||||
.send()
|
|
||||||
.map_err(|_| {
|
|
||||||
crate::response::Error::InvalidSession(
|
|
||||||
"Failed to validate session".to_string(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// If the session is invalid, return an error
|
|
||||||
if response.status_code != 200 {
|
|
||||||
return Err(crate::response::Error::InvalidSession(format!(
|
|
||||||
"Invalid session status code: {}",
|
|
||||||
response.status_code
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the player data
|
|
||||||
let local_player: LocalPlayer = response.json().map_err(|_| {
|
|
||||||
crate::response::Error::InvalidSession(
|
|
||||||
"Failed to parse player data".to_string(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
println!("[MOJANG] {} successfully logged on", &local_player.name);
|
|
||||||
|
|
||||||
let session = Self {
|
|
||||||
session_token,
|
|
||||||
database,
|
|
||||||
local_player,
|
|
||||||
sockets,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Capture the player
|
|
||||||
match player::login(&session) {
|
|
||||||
Ok(player) => {
|
|
||||||
session.add_socket(stream_2);
|
|
||||||
Ok((session, player))
|
|
||||||
}
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(mpsc::RecvTimeoutError::Timeout) => Err(crate::response::Error::Timeout(
|
|
||||||
"Session handshake timed out".to_string(),
|
|
||||||
)),
|
|
||||||
_ => Err(crate::response::Error::InvalidHandshake(
|
|
||||||
"Failed to receive handshake response".to_string(),
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_request(&self, method: &str, params: &HashMap<String, String>) -> Result {
|
|
||||||
match method {
|
|
||||||
"ping" => Ok(Response::Pong),
|
|
||||||
|
|
||||||
"set_cloak" => set_cloak::set_cloak(
|
|
||||||
self,
|
|
||||||
params.parse_param("cloak")?,
|
|
||||||
params
|
|
||||||
.parse_param::<String>("notify")
|
|
||||||
.unwrap_or_default()
|
|
||||||
.split("$")
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
|
|
||||||
"set_hat" => set_hat::set_hat(
|
|
||||||
self,
|
|
||||||
params.parse_param("hat")?,
|
|
||||||
params
|
|
||||||
.parse_param::<String>("notify")
|
|
||||||
.unwrap_or_default()
|
|
||||||
.split("$")
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
|
|
||||||
"emote" => emote::emote(
|
|
||||||
self,
|
|
||||||
params.parse_param("name")?,
|
|
||||||
params
|
|
||||||
.parse_param::<String>("notify")
|
|
||||||
.unwrap_or_default()
|
|
||||||
.split("$")
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
|
|
||||||
"player" => player::player(self, params.parse_param("uuid")?),
|
|
||||||
|
|
||||||
"players" => {
|
|
||||||
let mut players: Vec<PlayerResponse> = Vec::new();
|
|
||||||
let uuids = params.parse_param::<String>("uuids")?;
|
|
||||||
if uuids.is_empty() {
|
|
||||||
return Err(crate::response::Error::InvalidParameter {
|
|
||||||
param: "uuids".to_string(),
|
|
||||||
reason: "UUIDs list cannot be empty".to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
for uuid in uuids.split("$") {
|
|
||||||
if uuid.is_empty() {
|
|
||||||
return Err(crate::response::Error::InvalidParameter {
|
|
||||||
param: "uuids".to_string(),
|
|
||||||
reason: "UUID cannot be empty".to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
match player::player(self, uuid.to_string())? {
|
|
||||||
Response::Player(p) => players.push(p),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(Response::Players(players))
|
|
||||||
}
|
|
||||||
|
|
||||||
"buy_cloak" => buy::buy_cloak(self, params.parse_param("cloak")?),
|
|
||||||
"buy_hat" => buy::buy_hat(self, params.parse_param("hat")?),
|
|
||||||
|
|
||||||
_ => Err(crate::response::Error::InvalidMethod(method.to_string())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Session {
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub fn add_socket(&self, sock: ETcp) {
|
pub struct BuyCloak;
|
||||||
self.sockets
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.insert(self.local_player.id.clone(), sock);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove_from_sockets(&self) {
|
impl Method for BuyCloak {
|
||||||
self.sockets.lock().unwrap().remove(&self.local_player.id);
|
const NAME: &'static str = "buy_cloak";
|
||||||
}
|
type Request = String;
|
||||||
|
type Response = String;
|
||||||
pub fn notify(&self, players: &[&str], message: &str) -> Result<()> {
|
type Error = String;
|
||||||
let mut sockets = self.sockets.lock().unwrap();
|
}
|
||||||
for player in players {
|
|
||||||
if let Some(i) = sockets.get_mut(&player.to_string()) {
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
i.send(message)
|
pub struct BuyHat;
|
||||||
.map_err(|e| crate::response::Error::DatabaseError(format!("{e}")))?;
|
|
||||||
}
|
impl Method for BuyHat {
|
||||||
}
|
const NAME: &'static str = "buy_hat";
|
||||||
Ok(())
|
type Request = String;
|
||||||
}
|
type Response = String;
|
||||||
|
type Error = String;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Emote;
|
||||||
|
|
||||||
|
impl Method for Emote {
|
||||||
|
const NAME: &'static str = "emote";
|
||||||
|
type Request = EmoteRequest;
|
||||||
|
type Response = ();
|
||||||
|
type Error = String;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct EmoteEvent;
|
||||||
|
|
||||||
|
impl Method for EmoteEvent {
|
||||||
|
const NAME: &'static str = "emote_event";
|
||||||
|
type Request = EventEmote;
|
||||||
|
type Response = ();
|
||||||
|
type Error = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct GetPlayer;
|
||||||
|
|
||||||
|
impl Method for GetPlayer {
|
||||||
|
const NAME: &'static str = "get_player";
|
||||||
|
type Request = String;
|
||||||
|
type Response = Option<User>;
|
||||||
|
type Error = String;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Player;
|
||||||
|
|
||||||
|
impl Method for Player {
|
||||||
|
const NAME: &'static str = "player";
|
||||||
|
type Request = PlayerStream;
|
||||||
|
type Response = ();
|
||||||
|
type Error = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SendPlayer;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SendPlayerRequest {
|
||||||
|
pub targets: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Method for SendPlayer {
|
||||||
|
const NAME: &'static str = "send_player";
|
||||||
|
type Request = SendPlayerRequest;
|
||||||
|
type Response = ();
|
||||||
|
type Error = String;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
use mongodb::bson::doc;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
database::Player,
|
|
||||||
response::{Response, Result},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::Session;
|
|
||||||
|
|
||||||
pub fn player(session: &Session, uuid: String) -> Result {
|
|
||||||
// Try to find the player with the given UUID
|
|
||||||
let mut cursor = session
|
|
||||||
.database
|
|
||||||
.players
|
|
||||||
.find(doc! { "uuid": uuid.clone() })
|
|
||||||
.run()?;
|
|
||||||
|
|
||||||
// Check if there's a document in the cursor
|
|
||||||
Ok(match cursor.next() {
|
|
||||||
Some(Ok(player)) => {
|
|
||||||
if player.online {
|
|
||||||
Response::Player(crate::response::PlayerResponse::Player {
|
|
||||||
cloak: player.cloak,
|
|
||||||
uuid,
|
|
||||||
cloaks: player.cloaks,
|
|
||||||
hats: player.hats,
|
|
||||||
hat: player.hat,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
crate::response::Response::Player(crate::response::PlayerResponse::NonSaturnPlayer(
|
|
||||||
uuid,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => crate::response::Response::Player(crate::response::PlayerResponse::NonSaturnPlayer(
|
|
||||||
uuid,
|
|
||||||
)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create(session: &Session) -> Result {
|
|
||||||
let uuid = session.local_player.id.clone();
|
|
||||||
|
|
||||||
let player = Player {
|
|
||||||
uuid: uuid.clone(),
|
|
||||||
cloaks: Vec::new(),
|
|
||||||
cloak: "".to_string(),
|
|
||||||
hats: Vec::new(),
|
|
||||||
hat: String::new(),
|
|
||||||
coins: 0,
|
|
||||||
online: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
session.database.players.insert_one(player.clone()).run()?;
|
|
||||||
|
|
||||||
Ok(Response::Player(crate::response::PlayerResponse::Player {
|
|
||||||
cloak: player.cloak,
|
|
||||||
uuid,
|
|
||||||
cloaks: player.cloaks,
|
|
||||||
hats: player.hats,
|
|
||||||
hat: player.hat,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn login(session: &Session) -> Result {
|
|
||||||
let uuid = session.local_player.id.clone();
|
|
||||||
|
|
||||||
// Try to find the player with the given UUID
|
|
||||||
let mut cursor = session
|
|
||||||
.database
|
|
||||||
.players
|
|
||||||
.find(doc! { "uuid": uuid.clone() })
|
|
||||||
.run()?;
|
|
||||||
|
|
||||||
// Check if there's a document in the cursor
|
|
||||||
match cursor.next() {
|
|
||||||
Some(Ok(mut player)) => {
|
|
||||||
// Update player's online status
|
|
||||||
player.online = true;
|
|
||||||
session
|
|
||||||
.database
|
|
||||||
.players
|
|
||||||
.update_one(
|
|
||||||
doc! { "uuid": uuid.clone() },
|
|
||||||
doc! { "$set": { "online": true } },
|
|
||||||
)
|
|
||||||
.run()?;
|
|
||||||
|
|
||||||
// Player exists, deserialize and return their data
|
|
||||||
Ok(Response::Player(crate::response::PlayerResponse::Player {
|
|
||||||
cloak: player.cloak,
|
|
||||||
uuid,
|
|
||||||
cloaks: player.cloaks,
|
|
||||||
hats: player.hats,
|
|
||||||
hat: player.hat,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// No document found or deserialization error
|
|
||||||
_ => create(session),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn logout(session: &Session) -> Result {
|
|
||||||
session.remove_from_sockets();
|
|
||||||
let uuid = session.local_player.id.clone();
|
|
||||||
|
|
||||||
// Update player's online status to false
|
|
||||||
session
|
|
||||||
.database
|
|
||||||
.players
|
|
||||||
.update_one(
|
|
||||||
doc! { "uuid": uuid.clone() },
|
|
||||||
doc! { "$set": { "online": false } },
|
|
||||||
)
|
|
||||||
.run()?;
|
|
||||||
|
|
||||||
Ok(Response::Player(
|
|
||||||
crate::response::PlayerResponse::NonSaturnPlayer(uuid),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
use mongodb::bson::doc;
|
|
||||||
|
|
||||||
use crate::response::{Error, Response, Result};
|
|
||||||
|
|
||||||
use super::{player, Session};
|
|
||||||
|
|
||||||
pub fn set_cloak(session: &Session, cloak: String, notify: Vec<&str>) -> Result {
|
|
||||||
match player::player(session, session.local_player.id.clone())? {
|
|
||||||
Response::Player(crate::response::PlayerResponse::Player { cloaks, .. }) => {
|
|
||||||
if !cloaks.contains(&cloak) && !cloak.is_empty() {
|
|
||||||
return Err(Error::ValidationError(format!(
|
|
||||||
"Player does not own cloak: {}",
|
|
||||||
cloak
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
session
|
|
||||||
.database
|
|
||||||
.players
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"uuid": session.local_player.id.clone(),
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$set": {
|
|
||||||
"cloak": cloak.clone(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.run()
|
|
||||||
.map_err(|e| Error::DatabaseError(format!("Failed to update cloak: {}", e)))?;
|
|
||||||
|
|
||||||
if notify.len() > 0 {
|
|
||||||
session.notify(
|
|
||||||
¬ify,
|
|
||||||
&format!(
|
|
||||||
"update_cloak@uuid={}@cloak={cloak}",
|
|
||||||
session.local_player.id
|
|
||||||
),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Response::Success)
|
|
||||||
}
|
|
||||||
_ => Err(Error::ValidationError(
|
|
||||||
"Invalid player data received".to_string(),
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
use mongodb::bson::doc;
|
|
||||||
|
|
||||||
use crate::response::{Error, Response, Result};
|
|
||||||
|
|
||||||
use super::{player, Session};
|
|
||||||
|
|
||||||
pub fn set_hat(session: &Session, hat: String, notify: Vec<&str>) -> Result {
|
|
||||||
match player::player(session, session.local_player.id.clone())? {
|
|
||||||
Response::Player(crate::response::PlayerResponse::Player { hats, .. }) => {
|
|
||||||
if !hats.contains(&hat) && !hat.is_empty() {
|
|
||||||
return Err(Error::ValidationError(format!(
|
|
||||||
"Player does not own hat: {}",
|
|
||||||
hat
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
session
|
|
||||||
.database
|
|
||||||
.players
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"uuid": session.local_player.id.clone(),
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$set": {
|
|
||||||
"hat": hat.clone(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.run()
|
|
||||||
.map_err(|e| Error::DatabaseError(format!("Failed to update hat: {}", e)))?;
|
|
||||||
|
|
||||||
if notify.len() > 0 {
|
|
||||||
session.notify(
|
|
||||||
¬ify,
|
|
||||||
&format!("update_hat@uuid={}@hat={hat}", session.local_player.id),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Response::Success)
|
|
||||||
}
|
|
||||||
_ => Err(Error::ValidationError(
|
|
||||||
"Invalid player data received".to_string(),
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
methods,
|
||||||
|
types::{PlayerStream, SessionMap, UUID},
|
||||||
|
user::User,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn get_user(
|
||||||
|
sessions: SessionMap,
|
||||||
|
uuid: String,
|
||||||
|
pool: Arc<SqlitePool>,
|
||||||
|
) -> Result<Option<User>, String> {
|
||||||
|
if !sessions.lock().await.contains_key(&uuid) {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(crate::user::get(&uuid, pool.as_ref()).await?))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_user(
|
||||||
|
sessions: SessionMap,
|
||||||
|
name: UUID,
|
||||||
|
uuid: UUID,
|
||||||
|
targets: Vec<String>,
|
||||||
|
pool: Arc<SqlitePool>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
println!("send player {targets:?}");
|
||||||
|
let uuid = uuid.lock().await.to_string();
|
||||||
|
|
||||||
|
let user = PlayerStream {
|
||||||
|
player: crate::user::get(&uuid, pool.as_ref()).await?,
|
||||||
|
uuid: uuid.clone(),
|
||||||
|
name: name.lock().await.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("ready {targets:?}");
|
||||||
|
|
||||||
|
for target in targets {
|
||||||
|
if let Some(sessions) = sessions.lock().await.get_mut(&target) {
|
||||||
|
let mut bad_sessions = Vec::new();
|
||||||
|
|
||||||
|
for s in sessions.iter() {
|
||||||
|
if s.notify::<methods::Player>(user.clone()).await.is_err() {
|
||||||
|
bad_sessions.push(s.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for s in bad_sessions {
|
||||||
|
sessions.remove(&s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("sent");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
use std::{collections::HashMap, str::FromStr};
|
|
||||||
|
|
||||||
pub trait ParamMap {
|
|
||||||
fn parse_param<T: FromStr>(&self, param: &str) -> Result<T, crate::response::Error>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ParamMap for HashMap<String, String> {
|
|
||||||
fn parse_param<T: FromStr>(&self, param: &str) -> Result<T, crate::response::Error> {
|
|
||||||
match self.get(param) {
|
|
||||||
Some(value) => T::from_str(value).map_err(|_| crate::response::Error::InvalidParameter {
|
|
||||||
param: param.to_string(),
|
|
||||||
reason: "Failed to parse parameter value".to_string(),
|
|
||||||
}),
|
|
||||||
None => Err(crate::response::Error::ParameterNotFound(param.to_string())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse(s: &str) -> Result<(String, HashMap<String, String>), crate::response::Error> {
|
|
||||||
let mut parts = s.split("@");
|
|
||||||
|
|
||||||
let method = parts.next()
|
|
||||||
.ok_or_else(|| crate::response::Error::InvalidRequest("Empty request".to_string()))?
|
|
||||||
.trim()
|
|
||||||
.to_lowercase();
|
|
||||||
|
|
||||||
if method.is_empty() {
|
|
||||||
return Err(crate::response::Error::InvalidRequest("Method name cannot be empty".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut params = HashMap::new();
|
|
||||||
for part in parts {
|
|
||||||
let key_value: Vec<&str> = part.split("=").collect();
|
|
||||||
match key_value.as_slice() {
|
|
||||||
[key, value] => {
|
|
||||||
let key = key.trim();
|
|
||||||
if key.is_empty() {
|
|
||||||
return Err(crate::response::Error::InvalidRequest("Parameter key cannot be empty".to_string()));
|
|
||||||
}
|
|
||||||
params.insert(key.to_string(), value.trim().to_string());
|
|
||||||
},
|
|
||||||
_ => return Err(crate::response::Error::InvalidRequest(
|
|
||||||
format!("Invalid parameter format in part: {}", part)
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((method, params))
|
|
||||||
}
|
|
||||||
-111
@@ -1,111 +0,0 @@
|
|||||||
pub type Result<T = Response> = std::result::Result<T, Error>;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum PlayerResponse {
|
|
||||||
Player {
|
|
||||||
uuid: String,
|
|
||||||
cloaks: Vec<String>,
|
|
||||||
cloak: String,
|
|
||||||
hats: Vec<String>,
|
|
||||||
hat: String,
|
|
||||||
},
|
|
||||||
NonSaturnPlayer(String),
|
|
||||||
}
|
|
||||||
impl std::fmt::Display for PlayerResponse {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
PlayerResponse::Player {
|
|
||||||
uuid,
|
|
||||||
cloaks,
|
|
||||||
cloak,
|
|
||||||
hats,
|
|
||||||
hat,
|
|
||||||
} => write!(
|
|
||||||
f,
|
|
||||||
"player@cloak={cloak}@uuid={uuid}@cloaks={}@hats={}@hat={hat}@saturn=true",
|
|
||||||
cloaks.join("$"),
|
|
||||||
hats.join("$"),
|
|
||||||
),
|
|
||||||
PlayerResponse::NonSaturnPlayer(uuid) => write!(f, "@saturn=false@uuid={uuid}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum Response {
|
|
||||||
Pong,
|
|
||||||
Success,
|
|
||||||
SuccessfulTransaction(String),
|
|
||||||
Player(PlayerResponse),
|
|
||||||
Players(Vec<PlayerResponse>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Response {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Response::Pong => write!(f, "Pong"),
|
|
||||||
Response::Success => write!(f, "Success"),
|
|
||||||
Response::Player(player) => write!(f, "{player}"),
|
|
||||||
Response::Players(players) => {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"{}",
|
|
||||||
players
|
|
||||||
.iter()
|
|
||||||
.map(|p| format!("{p}"))
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join("\n")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Response::SuccessfulTransaction(s) => write!(f, "successful_transaction@{s}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum Error {
|
|
||||||
InvalidRequest(String),
|
|
||||||
InvalidMethod(String),
|
|
||||||
InvalidParameter { param: String, reason: String },
|
|
||||||
ParameterNotFound(String),
|
|
||||||
InvalidSession(String),
|
|
||||||
InvalidHandshake(String),
|
|
||||||
DatabaseError(String),
|
|
||||||
NetworkError(String),
|
|
||||||
Timeout(String),
|
|
||||||
AuthenticationError(String),
|
|
||||||
ValidationError(String),
|
|
||||||
EncryptionError(String),
|
|
||||||
TransactionError(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Error {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Error::InvalidRequest(msg) => write!(f, "Invalid request: {}", msg),
|
|
||||||
Error::InvalidMethod(method) => write!(f, "Invalid method: {}", method),
|
|
||||||
Error::InvalidParameter { param, reason } => {
|
|
||||||
write!(f, "Invalid parameter '{}': {}", param, reason)
|
|
||||||
}
|
|
||||||
Error::ParameterNotFound(param) => write!(f, "Required parameter not found: {}", param),
|
|
||||||
Error::InvalidSession(details) => write!(f, "Invalid session: {}", details),
|
|
||||||
Error::InvalidHandshake(details) => write!(f, "Handshake failed: {}", details),
|
|
||||||
Error::DatabaseError(details) => write!(f, "Database error: {}", details),
|
|
||||||
Error::NetworkError(details) => write!(f, "Network error: {}", details),
|
|
||||||
Error::Timeout(operation) => write!(f, "Operation timed out: {}", operation),
|
|
||||||
Error::AuthenticationError(details) => write!(f, "Authentication failed: {}", details),
|
|
||||||
Error::ValidationError(details) => write!(f, "Validation failed: {}", details),
|
|
||||||
Error::EncryptionError(details) => write!(f, "Encryption error: {}", details),
|
|
||||||
Error::TransactionError(details) => write!(f, "Transaction error: {}", details),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<mongodb::error::Error> for Error {
|
|
||||||
fn from(e: mongodb::error::Error) -> Self {
|
|
||||||
println!("{e}");
|
|
||||||
Error::DatabaseError(e.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for Error {}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
uuid TEXT PRIMARY KEY,
|
||||||
|
cloak TEXT DEFAULT "",
|
||||||
|
hat TEXT DEFAULT "",
|
||||||
|
cloaks TEXT DEFAULT '[]',
|
||||||
|
hats TEXT DEFAULT '[]'
|
||||||
|
);
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use session_rs::session::Session;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::user::User;
|
||||||
|
|
||||||
|
pub type SessionMap = Arc<Mutex<HashMap<String, HashSet<Session>>>>;
|
||||||
|
pub type UUID = Arc<Mutex<String>>;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct MinecraftAuthResponse {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
/// Received from the client to send to the targets
|
||||||
|
pub struct EmoteRequest {
|
||||||
|
pub emote: String,
|
||||||
|
pub targets: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// To send to the targets
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct EventEmote {
|
||||||
|
pub emote: String,
|
||||||
|
pub from: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct PlayerStream {
|
||||||
|
pub player: User,
|
||||||
|
pub uuid: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_uuid(input: &str) -> Result<String, &'static str> {
|
||||||
|
if input.len() != 32 {
|
||||||
|
return Err("Input must be exactly 32 hex characters");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(format!(
|
||||||
|
"{}-{}-{}-{}-{}",
|
||||||
|
&input[0..8],
|
||||||
|
&input[8..12],
|
||||||
|
&input[12..16],
|
||||||
|
&input[16..20],
|
||||||
|
&input[20..32],
|
||||||
|
))
|
||||||
|
}
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::{Row, SqlitePool};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct User {
|
||||||
|
pub cloak: String,
|
||||||
|
pub hat: String,
|
||||||
|
pub cloaks: Vec<String>,
|
||||||
|
pub hats: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize SQLite connection pool
|
||||||
|
pub async fn init_db() -> SqlitePool {
|
||||||
|
let pool = SqlitePool::connect("sqlite://users.db?mode=rwc")
|
||||||
|
.await
|
||||||
|
.expect("Failed to open database");
|
||||||
|
|
||||||
|
sqlx::query(include_str!("schema.sql"))
|
||||||
|
.execute(&pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to run schema");
|
||||||
|
|
||||||
|
pool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user by UUID
|
||||||
|
pub async fn get(uuid: &str, pool: &SqlitePool) -> Result<User, String> {
|
||||||
|
let row = sqlx::query("SELECT cloak, hat, cloaks, hats FROM users WHERE uuid = ?")
|
||||||
|
.bind(uuid)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
if let Some(r) = row {
|
||||||
|
let user = User {
|
||||||
|
cloak: r.get::<String, _>("cloak"),
|
||||||
|
hat: r.get::<String, _>("hat"),
|
||||||
|
cloaks: serde_json::from_str(&r.get::<String, _>("cloaks")).unwrap_or_default(),
|
||||||
|
hats: serde_json::from_str(&r.get::<String, _>("hats")).unwrap_or_default(),
|
||||||
|
};
|
||||||
|
Ok(user)
|
||||||
|
} else {
|
||||||
|
Err("User not found".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user if not exists
|
||||||
|
pub async fn get_put(uuid: &str, pool: &SqlitePool) -> Result<User, String> {
|
||||||
|
match get(uuid, pool).await {
|
||||||
|
Ok(user) => Ok(user),
|
||||||
|
Err(_) => {
|
||||||
|
let user = User {
|
||||||
|
cloak: "".into(),
|
||||||
|
hat: "".into(),
|
||||||
|
cloaks: vec![],
|
||||||
|
hats: vec![],
|
||||||
|
};
|
||||||
|
sqlx::query(
|
||||||
|
"INSERT INTO users (uuid, cloak, hat, cloaks, hats) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
)
|
||||||
|
.bind(uuid)
|
||||||
|
.bind(&user.cloak)
|
||||||
|
.bind(&user.hat)
|
||||||
|
.bind(serde_json::to_string(&user.cloaks).unwrap())
|
||||||
|
.bind(serde_json::to_string(&user.hats).unwrap())
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user