549 lines
15 KiB
Rust
549 lines
15 KiB
Rust
use std::ops::{Deref, DerefMut};
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use anyhow::Result;
|
|
use axum::{async_trait, BoxError, Router, Server};
|
|
use axum::body::HttpBody;
|
|
use axum::error_handling::HandleErrorLayer;
|
|
use axum::extract::{FromRequest, Path, State};
|
|
use axum::extract::rejection::BytesRejection;
|
|
use axum::http::{HeaderValue, Request, StatusCode};
|
|
#[cfg(not(debug_assertions))]
|
|
use axum::http::Method;
|
|
use axum::response::{IntoResponse, Response};
|
|
use axum::routing::{get, post};
|
|
use bytes::Bytes;
|
|
use rand::distributions::{Alphanumeric, DistString};
|
|
use serde::{Deserialize, Serialize};
|
|
use serde::de::DeserializeOwned;
|
|
use siphasher::sip::SipHasher;
|
|
use sqlx::SqlitePool;
|
|
use tower::ServiceBuilder;
|
|
use tower_http::compression::CompressionLayer;
|
|
use tower_http::cors::CorsLayer;
|
|
use tower_http::decompression::RequestDecompressionLayer;
|
|
|
|
static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!();
|
|
|
|
struct AppState {
|
|
salt: String,
|
|
pool: Arc<SqlitePool>,
|
|
hasher: SipHasher,
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
let pool = SqlitePool::connect("./database.sqlite").await?;
|
|
MIGRATOR.run(&pool).await?;
|
|
|
|
let pool = Arc::new(pool);
|
|
|
|
{
|
|
let pool = Arc::clone(&pool);
|
|
tokio::task::spawn(async move {
|
|
let mut interval = tokio::time::interval(Duration::from_secs(60 * 30));
|
|
loop {
|
|
interval.tick().await;
|
|
|
|
println!("deleting old records");
|
|
let result = sqlx::query!(
|
|
// language=sqlite
|
|
"delete from players where unixepoch('now') - unixepoch(timestamp) > 3600",
|
|
)
|
|
.execute(&*pool)
|
|
.await;
|
|
|
|
match result {
|
|
Ok(res) => println!("{} record(s) deleted", res.rows_affected()),
|
|
Err(e) => eprintln!("could not delete old records: {e:#}"),
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
let state = Arc::new(AppState {
|
|
pool,
|
|
salt: Alphanumeric.sample_string(&mut rand::thread_rng(), 8),
|
|
hasher: SipHasher::new(),
|
|
});
|
|
|
|
#[cfg(not(debug_assertions))]
|
|
let cors = CorsLayer::new()
|
|
.allow_origin([
|
|
"https://map.anna.lgbt".parse()?,
|
|
])
|
|
.allow_methods([Method::GET]);
|
|
|
|
#[cfg(debug_assertions)]
|
|
let cors = CorsLayer::permissive();
|
|
|
|
let app = Router::new()
|
|
.route("/:territory", get(territory))
|
|
.route("/:territory/:world", get(territory_world))
|
|
.route("/upload", post(upload))
|
|
.with_state(state)
|
|
.layer(cors)
|
|
.layer(CompressionLayer::new())
|
|
.layer(
|
|
ServiceBuilder::new()
|
|
.layer(HandleErrorLayer::new(|_: BoxError| async move {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, "unhandled server error")
|
|
}))
|
|
.layer(RequestDecompressionLayer::new())
|
|
);
|
|
|
|
Server::bind(&"127.0.0.1:30888".parse()?)
|
|
.serve(app.into_make_service())
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn territory(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(territory): Path<u32>,
|
|
) -> Result<MsgPack<Vec<AnonymousPlayerInfo>>, AppError>
|
|
{
|
|
let info = sqlx::query_as!(
|
|
AnonymousPlayerInfoInternal,
|
|
// language=sqlite
|
|
r#"
|
|
select hash,
|
|
world,
|
|
x,
|
|
y,
|
|
z,
|
|
w,
|
|
customize,
|
|
level,
|
|
job,
|
|
free_company,
|
|
current_hp,
|
|
max_hp,
|
|
coalesce(unixepoch('now') - unixepoch(timestamp), 30) as age
|
|
from players
|
|
where territory = ?
|
|
and age < 30
|
|
"#,
|
|
territory,
|
|
)
|
|
.fetch_all(&*state.pool)
|
|
.await?;
|
|
|
|
let info = info.into_iter()
|
|
.map(|player| AnonymousPlayerInfo::new_from(player, &state.hasher, &state.salt, territory))
|
|
.collect();
|
|
|
|
Ok(MsgPack(info))
|
|
}
|
|
|
|
async fn territory_world(
|
|
State(state): State<Arc<AppState>>,
|
|
Path((territory, world)): Path<(u32, u32)>,
|
|
) -> Result<MsgPack<Vec<AnonymousPlayerInfo>>, AppError>
|
|
{
|
|
let info = sqlx::query_as!(
|
|
AnonymousPlayerInfoInternal,
|
|
// language=sqlite
|
|
r#"
|
|
select hash,
|
|
world,
|
|
x,
|
|
y,
|
|
z,
|
|
w,
|
|
customize,
|
|
level,
|
|
job,
|
|
free_company,
|
|
current_hp,
|
|
max_hp,
|
|
coalesce(unixepoch('now') - unixepoch(timestamp), 30) as age
|
|
from players
|
|
where territory = ?
|
|
and current_world = ?
|
|
and age < 30
|
|
"#,
|
|
territory,
|
|
world,
|
|
)
|
|
.fetch_all(&*state.pool)
|
|
.await?;
|
|
|
|
let info = info.into_iter()
|
|
.map(|player| AnonymousPlayerInfo::new_from(player, &state.hasher, &state.salt, territory))
|
|
.collect();
|
|
|
|
Ok(MsgPack(info))
|
|
}
|
|
|
|
async fn upload(
|
|
state: State<Arc<AppState>>,
|
|
data: MsgPack<Update>,
|
|
) -> Result<(), AppError> {
|
|
if data.version != 2 {
|
|
return Err(anyhow::anyhow!("invalid update request version").into());
|
|
}
|
|
|
|
let mut t = state.pool.begin().await?;
|
|
|
|
for player in &data.players {
|
|
if !player.is_sane() {
|
|
continue;
|
|
}
|
|
|
|
let fc = match &player.free_company {
|
|
Some(x) if x.trim() == "" => None,
|
|
Some(x) => Some(x),
|
|
None => None,
|
|
};
|
|
|
|
sqlx::query!(
|
|
// language=sqlite
|
|
"
|
|
insert into players (hash, world, timestamp, territory, current_world, x, y, z, w, customize, level, job, free_company, current_hp,
|
|
max_hp)
|
|
values (?, ?, current_timestamp, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
on conflict (hash) do update set timestamp = current_timestamp,
|
|
territory = ?,
|
|
current_world = ?,
|
|
x = ?,
|
|
y = ?,
|
|
z = ?,
|
|
w = ?,
|
|
customize = ?,
|
|
level = ?,
|
|
job = ?,
|
|
free_company = ?,
|
|
current_hp = ?,
|
|
max_hp = ?
|
|
",
|
|
player.hash,
|
|
player.world,
|
|
|
|
data.territory,
|
|
data.world,
|
|
player.x,
|
|
player.y,
|
|
player.z,
|
|
player.w,
|
|
player.customize,
|
|
player.level,
|
|
player.job,
|
|
fc,
|
|
player.current_hp,
|
|
player.max_hp,
|
|
|
|
data.territory,
|
|
data.world,
|
|
player.x,
|
|
player.y,
|
|
player.z,
|
|
player.w,
|
|
player.customize,
|
|
player.level,
|
|
player.job,
|
|
fc,
|
|
player.current_hp,
|
|
player.max_hp,
|
|
)
|
|
.execute(&mut *t)
|
|
.await?;
|
|
}
|
|
|
|
t.commit().await?;
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct Update {
|
|
version: u8,
|
|
territory: u32,
|
|
world: u32,
|
|
players: Vec<PlayerInfo>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct PlayerInfo {
|
|
#[serde(with = "serde_bytes")]
|
|
hash: Vec<u8>,
|
|
world: u32,
|
|
x: f64,
|
|
y: f64,
|
|
z: f64,
|
|
w: f64,
|
|
#[serde(with = "serde_bytes")]
|
|
customize: Vec<u8>,
|
|
level: u8,
|
|
job: u32,
|
|
free_company: Option<String>,
|
|
current_hp: u32,
|
|
max_hp: u32,
|
|
}
|
|
|
|
impl PlayerInfo {
|
|
fn is_sane(&self) -> bool {
|
|
!self.hash.is_empty()
|
|
&& self.world != 65535
|
|
&& self.level != 0
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct AnonymousPlayerInfo {
|
|
x: f64,
|
|
y: f64,
|
|
z: f64,
|
|
w: f64,
|
|
gender: u8,
|
|
race: u8,
|
|
level: u8,
|
|
job: u8,
|
|
hp_percent: f64,
|
|
age: i8,
|
|
territory_unique_id: u64,
|
|
}
|
|
|
|
impl AnonymousPlayerInfo {
|
|
fn new_from(value: AnonymousPlayerInfoInternal, hasher: &SipHasher, salt: &str, territory: u32) -> Self {
|
|
let customize = value.customize();
|
|
|
|
Self {
|
|
territory_unique_id: value.gen_hash(hasher, salt, territory),
|
|
x: value.x,
|
|
y: value.y,
|
|
z: value.z,
|
|
w: value.w,
|
|
gender: customize.gender,
|
|
race: customize.race,
|
|
level: value.level as u8,
|
|
job: value.job as u8,
|
|
hp_percent: value.current_hp as f64 / value.max_hp as f64,
|
|
age: value.age as i8,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
#[allow(dead_code)]
|
|
struct Customize {
|
|
race: u8,
|
|
gender: u8,
|
|
model_type: u8,
|
|
height: u8,
|
|
tribe: u8,
|
|
face_type: u8,
|
|
hairstyle: u8,
|
|
has_highlights: u8,
|
|
skin_colour: u8,
|
|
eye_colour: u8,
|
|
hair_colour: u8,
|
|
hair_colour2: u8,
|
|
face_features: u8,
|
|
face_features_colour: u8,
|
|
eyebrows: u8,
|
|
eye_colour2: u8,
|
|
eye_shape: u8,
|
|
nose_shape: u8,
|
|
jaw_shape: u8,
|
|
lip_style: u8,
|
|
lip_colour: u8,
|
|
race_feature_size: u8,
|
|
race_feature_type: u8,
|
|
bust_size: u8,
|
|
facepaint: u8,
|
|
facepaint_colour: u8,
|
|
}
|
|
|
|
impl Customize {
|
|
pub fn new(data: &[u8]) -> Option<Self> {
|
|
if data.len() < 26 {
|
|
return None;
|
|
}
|
|
|
|
Some(Self {
|
|
race: data[0],
|
|
gender: data[1],
|
|
model_type: data[2],
|
|
height: data[3],
|
|
tribe: data[4],
|
|
face_type: data[5],
|
|
hairstyle: data[6],
|
|
has_highlights: data[7],
|
|
skin_colour: data[8],
|
|
eye_colour: data[9],
|
|
hair_colour: data[10],
|
|
hair_colour2: data[11],
|
|
face_features: data[12],
|
|
face_features_colour: data[13],
|
|
eyebrows: data[14],
|
|
eye_colour2: data[15],
|
|
eye_shape: data[16],
|
|
nose_shape: data[17],
|
|
jaw_shape: data[18],
|
|
lip_style: data[19],
|
|
lip_colour: data[20],
|
|
race_feature_size: data[21],
|
|
race_feature_type: data[22],
|
|
bust_size: data[23],
|
|
facepaint: data[24],
|
|
facepaint_colour: data[25],
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct AnonymousPlayerInfoInternal {
|
|
#[serde(skip)]
|
|
hash: Vec<u8>,
|
|
world: i64,
|
|
x: f64,
|
|
y: f64,
|
|
z: f64,
|
|
w: f64,
|
|
#[serde(with = "serde_bytes")]
|
|
customize: Vec<u8>,
|
|
level: i64,
|
|
job: i64,
|
|
free_company: Option<String>,
|
|
current_hp: i64,
|
|
max_hp: i64,
|
|
age: i64,
|
|
}
|
|
|
|
impl AnonymousPlayerInfoInternal {
|
|
pub fn gen_hash(&self, hasher: &SipHasher, salt: &str, territory: u32) -> u64 {
|
|
hasher.hash(format!(
|
|
"{}-{}-{}",
|
|
salt,
|
|
territory,
|
|
data_encoding::HEXLOWER.encode(&self.hash),
|
|
).as_bytes())
|
|
}
|
|
|
|
pub fn customize(&self) -> Customize {
|
|
Customize::new(&self.customize).unwrap_or_default()
|
|
}
|
|
}
|
|
|
|
// Make our own error that wraps `anyhow::Error`.
|
|
struct AppError(anyhow::Error);
|
|
|
|
// Tell axum how to convert `AppError` into a response.
|
|
impl IntoResponse for AppError {
|
|
fn into_response(self) -> Response {
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
format!("Something went wrong: {}", self.0),
|
|
)
|
|
.into_response()
|
|
}
|
|
}
|
|
|
|
// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into
|
|
// `Result<_, AppError>`. That way you don't need to do that manually.
|
|
impl<E> From<E> for AppError
|
|
where
|
|
E: Into<anyhow::Error>,
|
|
{
|
|
fn from(err: E) -> Self {
|
|
Self(err.into())
|
|
}
|
|
}
|
|
|
|
struct MsgPack<T>(pub T);
|
|
|
|
impl<T> Deref for MsgPack<T> {
|
|
type Target = T;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl<T> DerefMut for MsgPack<T> {
|
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
&mut self.0
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl<S, B, T> FromRequest<S, B> for MsgPack<T>
|
|
where S: Send + Sync,
|
|
B: HttpBody + Send + 'static,
|
|
B::Data: Send,
|
|
B::Error: Into<BoxError>,
|
|
T: DeserializeOwned,
|
|
{
|
|
type Rejection = MsgPackRejection;
|
|
|
|
async fn from_request(req: Request<B>, state: &S) -> std::result::Result<Self, Self::Rejection> {
|
|
if req.headers().get("content-type").and_then(|val| val.to_str().ok()) != Some("application/msgpack") {
|
|
return Err(MsgPackRejection::ContentType);
|
|
}
|
|
|
|
let bytes = Bytes::from_request(req, state).await?;
|
|
let value = rmp_serde::from_slice(&bytes)?;
|
|
// let des = &mut rmp_serde::Deserializer::from_read_ref(&bytes);
|
|
// let value = match serde_path_to_error::deserialize(des) {
|
|
// Ok(v) => v,
|
|
// Err(err) => {
|
|
// let rejection = match err.inner() {
|
|
// rmp_serde::decode::Error::DepthLimitExceeded => {},
|
|
// };
|
|
//
|
|
// return Err(rejection);
|
|
// }
|
|
// };
|
|
|
|
Ok(MsgPack(value))
|
|
}
|
|
}
|
|
|
|
impl<T> IntoResponse for MsgPack<T>
|
|
where T: Serialize,
|
|
{
|
|
fn into_response(self) -> Response {
|
|
(
|
|
[(
|
|
axum::http::header::CONTENT_TYPE,
|
|
HeaderValue::from_static("application/msgpack"),
|
|
)],
|
|
rmp_serde::to_vec(&self.0)
|
|
.map_err(|e| AppError(e.into()))
|
|
.into_response()
|
|
).into_response()
|
|
}
|
|
}
|
|
|
|
enum MsgPackRejection {
|
|
ContentType,
|
|
Bytes(BytesRejection),
|
|
MsgPack(rmp_serde::decode::Error),
|
|
}
|
|
|
|
impl IntoResponse for MsgPackRejection {
|
|
fn into_response(self) -> Response {
|
|
(
|
|
StatusCode::BAD_REQUEST,
|
|
match self {
|
|
Self::ContentType => "expected application/msgpack content-type header".into_response(),
|
|
Self::Bytes(e) => e.into_response(),
|
|
Self::MsgPack(e) => format!("could not deserialize msgpack: {:#}", e).into_response(),
|
|
}
|
|
).into_response()
|
|
}
|
|
}
|
|
|
|
impl From<BytesRejection> for MsgPackRejection {
|
|
fn from(value: BytesRejection) -> Self {
|
|
Self::Bytes(value)
|
|
}
|
|
}
|
|
|
|
impl From<rmp_serde::decode::Error> for MsgPackRejection {
|
|
fn from(value: rmp_serde::decode::Error) -> Self {
|
|
Self::MsgPack(value)
|
|
}
|
|
}
|