megamappingway/server/src/main.rs

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