Compare commits
6 Commits
0147a3bafd
...
f0a9405e69
Author | SHA1 | Date |
---|---|---|
Anna | f0a9405e69 | |
Anna | 75a4ec5ca8 | |
Anna | 71ecba455f | |
Anna | 28746242d3 | |
Anna | fe151e6905 | |
Anna | e46d2897cf |
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>1.0.0-alpha.1</Version>
|
||||
<Version>1.0.0-alpha.2</Version>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
@ -55,6 +55,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DalamudPackager" Version="2.1.12"/>
|
||||
<PackageReference Include="MessagePack" Version="2.5.129"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
name: Player Map
|
||||
author: Anna
|
||||
punchline: Crowdsourced location maps
|
||||
description: Crowdsourced location maps
|
||||
description: |-
|
||||
Use this plugin to upload player data to the crowdsourced location maps at
|
||||
https://map.anna.lgbt/. It uploads the players around you every five seconds.
|
||||
repo_url: https://map.anna.lgbt/
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
using System.Diagnostics;
|
||||
using System.IO.Compression;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using MessagePack;
|
||||
|
||||
namespace PlayerMap;
|
||||
|
||||
|
@ -78,22 +76,18 @@ public class Plugin : IDalamudPlugin {
|
|||
players.Shuffle();
|
||||
|
||||
var update = new Update(territory, player.CurrentWorld.Id, players);
|
||||
var json = JsonConvert.SerializeObject(update, new JsonSerializerSettings {
|
||||
ContractResolver = new DefaultContractResolver {
|
||||
NamingStrategy = new SnakeCaseNamingStrategy(),
|
||||
},
|
||||
});
|
||||
var msgpack = MessagePackSerializer.Serialize(update, MessagePackSerializerOptions.Standard);
|
||||
|
||||
using var mem = new MemoryStream();
|
||||
await using (var gz = new GZipStream(mem, CompressionLevel.Optimal)) {
|
||||
await gz.WriteAsync(Encoding.UTF8.GetBytes(json));
|
||||
await gz.WriteAsync(msgpack);
|
||||
await gz.FlushAsync();
|
||||
}
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Post, "http://localhost:30888/upload") {
|
||||
var req = new HttpRequestMessage(HttpMethod.Post, "https://map.anna.lgbt/api/upload") {
|
||||
Content = new ByteArrayContent(mem.ToArray()) {
|
||||
Headers = {
|
||||
ContentType = new MediaTypeHeaderValue("application/json"),
|
||||
ContentType = new MediaTypeHeaderValue("application/msgpack"),
|
||||
ContentEncoding = { "gzip" },
|
||||
},
|
||||
},
|
||||
|
@ -115,13 +109,20 @@ public class Plugin : IDalamudPlugin {
|
|||
}
|
||||
|
||||
[Serializable]
|
||||
[MessagePackObject]
|
||||
internal struct Update(uint territory, uint world, List<PlayerInfo> players) {
|
||||
[Key(0)]
|
||||
public readonly uint Territory = territory;
|
||||
|
||||
[Key(1)]
|
||||
public readonly uint World = world;
|
||||
|
||||
[Key(2)]
|
||||
public readonly List<PlayerInfo> Players = players;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
[MessagePackObject]
|
||||
internal struct PlayerInfo(
|
||||
string name,
|
||||
uint world,
|
||||
|
@ -136,16 +137,39 @@ internal struct PlayerInfo(
|
|||
uint currentHp,
|
||||
uint maxHp
|
||||
) {
|
||||
[Key(0)]
|
||||
public readonly string Name = name;
|
||||
|
||||
[Key(1)]
|
||||
public readonly uint World = world;
|
||||
|
||||
[Key(2)]
|
||||
public readonly float X = x;
|
||||
|
||||
[Key(3)]
|
||||
public readonly float Y = y;
|
||||
|
||||
[Key(4)]
|
||||
public readonly float Z = z;
|
||||
|
||||
[Key(5)]
|
||||
public readonly float W = w;
|
||||
|
||||
[Key(6)]
|
||||
public readonly List<byte> Customize = customize;
|
||||
|
||||
[Key(7)]
|
||||
public readonly byte Level = level;
|
||||
|
||||
[Key(8)]
|
||||
public readonly uint Job = job;
|
||||
|
||||
[Key(9)]
|
||||
public readonly string FreeCompany = freeCompany;
|
||||
|
||||
[Key(10)]
|
||||
public readonly uint CurrentHp = currentHp;
|
||||
|
||||
[Key(11)]
|
||||
public readonly uint MaxHp = maxHp;
|
||||
};
|
||||
|
|
|
@ -7,6 +7,32 @@
|
|||
"requested": "[2.1.12, )",
|
||||
"resolved": "2.1.12",
|
||||
"contentHash": "Sc0PVxvgg4NQjcI8n10/VfUQBAS4O+Fw2pZrAqBdRMbthYGeogzu5+xmIGCGmsEZ/ukMOBuAqiNiB5qA3MRalg=="
|
||||
},
|
||||
"MessagePack": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.5.129, )",
|
||||
"resolved": "2.5.129",
|
||||
"contentHash": "1jBW0Q3qvv+PJBwer8lQ2l26/fKptJIqFgVdyKfn4zW+LSYE8xEcvd4svfh0erI5f4d+rQRIAN229I2ARI/A5w==",
|
||||
"dependencies": {
|
||||
"MessagePack.Annotations": "2.5.129",
|
||||
"Microsoft.NET.StringTools": "17.6.3",
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
|
||||
}
|
||||
},
|
||||
"MessagePack.Annotations": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.5.129",
|
||||
"contentHash": "wNJB3EaJKjq+5pti+0T8b444fEb2PRw3hFefp9+of/BvDdTWO0iIUWfYNZbKx5aAIrpFzhLEfMAEJnGWWq3OFQ=="
|
||||
},
|
||||
"Microsoft.NET.StringTools": {
|
||||
"type": "Transitive",
|
||||
"resolved": "17.6.3",
|
||||
"contentHash": "N0ZIanl1QCgvUumEL1laasU0a7sOE5ZwLZVTn0pAePnfhq8P7SvTjF8Axq+CnavuQkmdQpGNXQ1efZtu5kDFbA=="
|
||||
},
|
||||
"System.Runtime.CompilerServices.Unsafe": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -236,7 +236,10 @@ checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
|
|||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
|
@ -1040,7 +1043,11 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"rmp-serde",
|
||||
"serde",
|
||||
"serde_path_to_error",
|
||||
"sqlx",
|
||||
"tokio",
|
||||
"tower",
|
||||
|
@ -1110,6 +1117,28 @@ dependencies = [
|
|||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rmp"
|
||||
version = "0.8.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f9860a6cc38ed1da53456442089b4dfa35e7cedaa326df63017af88385e6b20"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"num-traits",
|
||||
"paste",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rmp-serde"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bffea85eea980d8a74453e5d02a8d93028f3c34725de143085a844ebe953258a"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"rmp",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.9.2"
|
||||
|
|
|
@ -8,8 +8,12 @@ edition = "2021"
|
|||
[dependencies]
|
||||
anyhow = "1"
|
||||
axum = "0.6"
|
||||
bytes = "1"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
rmp-serde = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_path_to_error = "0.1"
|
||||
sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite", "chrono"] }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||
tower = "0.4"
|
||||
tower-http = { version = "0.4", features = ["decompression-gzip"] }
|
||||
tower-http = { version = "0.4", features = ["compression-gzip", "decompression-gzip", "cors"] }
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use axum::{BoxError, Json, Router, Server};
|
||||
use axum::{async_trait, BoxError, Router, Server};
|
||||
use axum::body::HttpBody;
|
||||
use axum::error_handling::HandleErrorLayer;
|
||||
use axum::extract::State;
|
||||
use axum::http::StatusCode;
|
||||
use axum::extract::{FromRequest, Path, State};
|
||||
use axum::extract::rejection::BytesRejection;
|
||||
use axum::http::{HeaderValue, Request, StatusCode};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::routing::{get, post};
|
||||
use serde::Deserialize;
|
||||
use bytes::Bytes;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::de::DeserializeOwned;
|
||||
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!();
|
||||
|
@ -19,10 +26,23 @@ async fn main() -> Result<()> {
|
|||
let pool = SqlitePool::connect("./database.sqlite").await?;
|
||||
MIGRATOR.run(&pool).await?;
|
||||
|
||||
#[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("/", get(index))
|
||||
.route("/:territory", get(territory))
|
||||
.route("/:territory/:world", get(territory_world))
|
||||
.route("/upload", post(upload))
|
||||
.with_state(Arc::new(pool))
|
||||
.layer(cors)
|
||||
.layer(CompressionLayer::new())
|
||||
.layer(
|
||||
ServiceBuilder::new()
|
||||
.layer(HandleErrorLayer::new(|_: BoxError| async move {
|
||||
|
@ -37,20 +57,89 @@ async fn main() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn index() -> &'static str {
|
||||
"hi"
|
||||
async fn territory(
|
||||
State(pool): State<Arc<SqlitePool>>,
|
||||
Path(territory): Path<u32>,
|
||||
) -> Result<MsgPack<Vec<AnonymousPlayerInfo>>, AppError>
|
||||
{
|
||||
let info = sqlx::query_as!(
|
||||
AnonymousPlayerInfo,
|
||||
// language=sqlite
|
||||
r#"
|
||||
select world,
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
w,
|
||||
customize,
|
||||
level,
|
||||
job,
|
||||
free_company,
|
||||
current_hp,
|
||||
max_hp
|
||||
from players
|
||||
where territory = ?
|
||||
--and unixepoch('now') - unixepoch(timestamp) < 30
|
||||
"#,
|
||||
territory,
|
||||
)
|
||||
.fetch_all(&*pool)
|
||||
.await?;
|
||||
|
||||
Ok(MsgPack(info))
|
||||
}
|
||||
|
||||
async fn territory_world(
|
||||
State(pool): State<Arc<SqlitePool>>,
|
||||
Path((territory, world)): Path<(u32, u32)>,
|
||||
) -> Result<MsgPack<Vec<AnonymousPlayerInfo>>, AppError>
|
||||
{
|
||||
let info = sqlx::query_as!(
|
||||
AnonymousPlayerInfo,
|
||||
// language=sqlite
|
||||
r#"
|
||||
select world,
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
w,
|
||||
customize,
|
||||
level,
|
||||
job,
|
||||
free_company,
|
||||
current_hp,
|
||||
max_hp
|
||||
from players
|
||||
where territory = ?
|
||||
and current_world = ?
|
||||
--and unixepoch('now') - unixepoch(timestamp) < 30
|
||||
"#,
|
||||
territory,
|
||||
world,
|
||||
)
|
||||
.fetch_all(&*pool)
|
||||
.await?;
|
||||
|
||||
Ok(MsgPack(info))
|
||||
}
|
||||
|
||||
async fn upload(
|
||||
pool: State<Arc<SqlitePool>>,
|
||||
data: Json<Update>,
|
||||
data: MsgPack<Update>,
|
||||
) -> Result<(), AppError> {
|
||||
let mut t = pool.begin().await?;
|
||||
|
||||
for player in &data.players {
|
||||
let fc = match player.free_company.trim() {
|
||||
"" => None,
|
||||
x => Some(x),
|
||||
if !player.is_sane() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let name = player.name.trim();
|
||||
|
||||
let fc = match &player.free_company {
|
||||
Some(x) if x.trim() == "" => None,
|
||||
Some(x) => Some(x),
|
||||
None => None,
|
||||
};
|
||||
|
||||
sqlx::query!(
|
||||
|
@ -73,8 +162,9 @@ async fn upload(
|
|||
current_hp = ?,
|
||||
max_hp = ?
|
||||
",
|
||||
player.name,
|
||||
name,
|
||||
player.world,
|
||||
|
||||
data.territory,
|
||||
data.world,
|
||||
player.x,
|
||||
|
@ -120,18 +210,48 @@ struct Update {
|
|||
struct PlayerInfo {
|
||||
name: String,
|
||||
world: u32,
|
||||
x: f32,
|
||||
y: f32,
|
||||
z: f32,
|
||||
w: f32,
|
||||
x: f64,
|
||||
y: f64,
|
||||
z: f64,
|
||||
w: f64,
|
||||
customize: Vec<u8>,
|
||||
level: u8,
|
||||
job: u32,
|
||||
free_company: String,
|
||||
free_company: Option<String>,
|
||||
current_hp: u32,
|
||||
max_hp: u32,
|
||||
}
|
||||
|
||||
impl PlayerInfo {
|
||||
fn is_sane(&self) -> bool {
|
||||
if self.name.trim().is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.world == 65535 || self.level == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AnonymousPlayerInfo {
|
||||
world: i64,
|
||||
// timestamp: DateTime<Utc>,
|
||||
x: f64,
|
||||
y: f64,
|
||||
z: f64,
|
||||
w: f64,
|
||||
customize: Vec<u8>,
|
||||
level: i64,
|
||||
job: i64,
|
||||
free_company: Option<String>,
|
||||
current_hp: i64,
|
||||
max_hp: i64,
|
||||
}
|
||||
|
||||
// Make our own error that wraps `anyhow::Error`.
|
||||
struct AppError(anyhow::Error);
|
||||
|
||||
|
@ -156,3 +276,99 @@ impl<E> From<E> for AppError
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue