remote-party-finder/server/src/web.rs

494 lines
15 KiB
Rust
Raw Normal View History

2021-11-28 21:37:44 +00:00
use std::{
cmp::Ordering,
convert::Infallible,
sync::Arc,
time::Duration,
};
2022-08-25 21:09:30 +00:00
use anyhow::{Context, Result};
2021-10-07 16:46:18 +00:00
use chrono::Utc;
2021-11-28 21:37:44 +00:00
use mongodb::{
2022-08-25 21:09:30 +00:00
bson::doc,
2021-11-28 21:37:44 +00:00
Client as MongoClient,
Collection,
IndexModel,
options::{IndexOptions, UpdateOptions},
results::UpdateResult,
};
2021-10-10 23:43:29 +00:00
use tokio::sync::RwLock;
2021-10-04 03:17:09 +00:00
use tokio_stream::StreamExt;
2021-11-28 21:37:44 +00:00
use warp::{
Filter,
filters::BoxedFilter,
http::Uri,
2022-08-25 21:09:30 +00:00
Reply,
2021-11-28 21:37:44 +00:00
};
2022-08-25 21:09:30 +00:00
2021-11-28 21:37:44 +00:00
use crate::{
config::Config,
ffxiv::Language,
listing::PartyFinderListing,
listing_container::{ListingContainer, QueriedListing},
stats::CachedStatistics,
template::listings::ListingsTemplate,
template::stats::StatsTemplate,
};
2021-10-04 03:17:09 +00:00
2022-08-25 21:09:30 +00:00
mod stats;
2021-10-04 03:17:09 +00:00
pub async fn start(config: Arc<Config>) -> Result<()> {
let state = State::new(Arc::clone(&config)).await?;
println!("listening at {}", config.web.host);
warp::serve(router(state))
.run(config.web.host)
.await;
Ok(())
}
2021-10-10 23:43:29 +00:00
pub struct State {
2021-10-04 03:17:09 +00:00
config: Arc<Config>,
mongo: MongoClient,
2021-10-23 23:19:32 +00:00
stats: RwLock<Option<CachedStatistics>>,
2021-10-04 03:17:09 +00:00
}
impl State {
pub async fn new(config: Arc<Config>) -> Result<Arc<Self>> {
let mongo = MongoClient::with_uri_str(&config.mongo.url)
.await
.context("could not create mongodb client")?;
2021-10-04 05:02:36 +00:00
let state = Arc::new(Self {
2021-10-04 03:17:09 +00:00
config,
mongo,
2021-10-10 23:43:29 +00:00
stats: Default::default(),
2021-10-04 05:02:36 +00:00
});
state.collection()
.create_index(
IndexModel::builder()
.keys(mongodb::bson::doc! {
"listing.id": 1,
2021-10-07 16:20:04 +00:00
"listing.last_server_restart": 1,
"listing.created_world": 1,
2021-10-04 05:02:36 +00:00
})
.options(IndexOptions::builder()
.unique(true)
.build())
.build(),
None,
)
.await
2021-11-28 21:38:30 +00:00
.context("could not create unique index")?;
state.collection()
.create_index(
IndexModel::builder()
.keys(mongodb::bson::doc! {
"updated_at": 1,
2021-11-28 21:38:30 +00:00
})
.build(),
None,
)
.await
.context("could not create updated_at index")?;
2021-10-04 05:02:36 +00:00
2021-10-10 23:43:29 +00:00
let task_state = Arc::clone(&state);
tokio::task::spawn(async move {
loop {
2021-10-23 23:19:32 +00:00
let all_time = match self::stats::get_stats(&*task_state).await {
2021-10-10 23:43:29 +00:00
Ok(stats) => stats,
Err(e) => {
eprintln!("error generating stats: {:#?}", e);
continue;
}
};
2021-10-23 23:19:32 +00:00
let seven_days = match self::stats::get_stats_seven_days(&*task_state).await {
Ok(stats) => stats,
Err(e) => {
eprintln!("error generating stats: {:#?}", e);
continue;
2021-11-28 21:38:30 +00:00
}
2021-10-23 23:19:32 +00:00
};
*task_state.stats.write().await = Some(CachedStatistics {
all_time,
seven_days,
});
2021-10-10 23:43:29 +00:00
tokio::time::sleep(Duration::from_secs(60 * 5)).await;
}
});
2021-10-04 05:02:36 +00:00
Ok(state)
2021-10-04 03:17:09 +00:00
}
pub fn collection(&self) -> Collection<ListingContainer> {
self.mongo.database("rpf").collection("listings")
}
}
fn router(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
index()
.or(listings(Arc::clone(&state)))
.or(contribute(Arc::clone(&state)))
.or(contribute_multiple(Arc::clone(&state)))
2021-10-10 23:43:29 +00:00
.or(stats(Arc::clone(&state)))
2021-10-23 23:19:32 +00:00
.or(stats_seven_days(Arc::clone(&state)))
2021-10-04 03:17:09 +00:00
.or(assets())
.boxed()
}
fn assets() -> BoxedFilter<(impl Reply, )> {
warp::get()
.and(warp::path("assets"))
.and(
icons()
2021-10-04 17:53:49 +00:00
.or(minireset())
2021-10-10 23:43:29 +00:00
.or(common_css())
2021-10-04 17:53:49 +00:00
.or(listings_css())
2021-10-04 19:14:40 +00:00
.or(listings_js())
2021-10-10 23:43:29 +00:00
.or(stats_css())
.or(stats_js())
2022-05-08 21:41:03 +00:00
.or(d3())
.or(pico())
.or(common_js())
2022-07-04 19:44:58 +00:00
.or(list_js())
2021-10-04 03:17:09 +00:00
)
.boxed()
}
fn icons() -> BoxedFilter<(impl Reply, )> {
warp::path("icons.svg")
.and(warp::path::end())
.and(warp::fs::file("./assets/icons.svg"))
.boxed()
}
2021-10-04 17:53:49 +00:00
fn minireset() -> BoxedFilter<(impl Reply, )> {
warp::path("minireset.css")
.and(warp::path::end())
.and(warp::fs::file("./assets/minireset.css"))
.boxed()
}
2021-10-10 23:43:29 +00:00
fn common_css() -> BoxedFilter<(impl Reply, )> {
warp::path("common.css")
.and(warp::path::end())
.and(warp::fs::file("./assets/common.css"))
.boxed()
}
2021-10-04 17:53:49 +00:00
fn listings_css() -> BoxedFilter<(impl Reply, )> {
warp::path("listings.css")
.and(warp::path::end())
.and(warp::fs::file("./assets/listings.css"))
.boxed()
}
2021-10-04 19:14:40 +00:00
fn listings_js() -> BoxedFilter<(impl Reply, )> {
warp::path("listings.js")
.and(warp::path::end())
.and(warp::fs::file("./assets/listings.js"))
.boxed()
}
2021-10-10 23:43:29 +00:00
fn stats_css() -> BoxedFilter<(impl Reply, )> {
warp::path("stats.css")
.and(warp::path::end())
.and(warp::fs::file("./assets/stats.css"))
.boxed()
}
fn stats_js() -> BoxedFilter<(impl Reply, )> {
warp::path("stats.js")
.and(warp::path::end())
.and(warp::fs::file("./assets/stats.js"))
.boxed()
}
2022-05-08 21:41:03 +00:00
fn d3() -> BoxedFilter<(impl Reply, )> {
warp::path("d3.js")
.and(warp::path::end())
.and(warp::fs::file("./assets/d3.v7.min.js"))
.boxed()
}
fn pico() -> BoxedFilter<(impl Reply, )> {
warp::path("pico.css")
.and(warp::path::end())
.and(warp::fs::file("./assets/pico.min.css"))
.boxed()
}
fn common_js() -> BoxedFilter<(impl Reply, )> {
warp::path("common.js")
.and(warp::path::end())
.and(warp::fs::file("./assets/common.js"))
.boxed()
}
2022-07-04 19:44:58 +00:00
fn list_js() -> BoxedFilter<(impl Reply, )> {
warp::path("list.js")
.and(warp::path::end())
.and(warp::fs::file("./assets/list.min.js"))
.boxed()
}
2022-07-04 19:47:35 +00:00
fn index() -> BoxedFilter<(impl Reply, )> {
let route = warp::path::end()
.map(|| warp::redirect(Uri::from_static("/listings")));
warp::get().and(route).boxed()
}
2021-10-04 03:17:09 +00:00
fn listings(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
2021-10-10 15:39:45 +00:00
async fn logic(state: Arc<State>, codes: Option<String>) -> std::result::Result<impl Reply, Infallible> {
2021-10-10 17:29:55 +00:00
let lang = Language::from_codes(codes.as_deref());
2021-10-04 03:17:09 +00:00
2021-11-28 21:38:30 +00:00
let two_hours_ago = Utc::now() - chrono::Duration::hours(2);
2021-10-04 03:17:09 +00:00
let res = state
.collection()
.aggregate(
[
2021-11-28 21:38:30 +00:00
// don't ask me why, but mongo shits itself unless you provide a hard date
// doc! {
// "$match": {
// "created_at": {
// "$gte": {
// "$dateSubtract": {
// "startDate": "$$NOW",
// "unit": "hour",
// "amount": 2,
// },
// },
// },
// }
// },
doc! {
"$match": {
"updated_at": { "$gte": two_hours_ago },
2021-11-28 21:38:30 +00:00
}
},
doc! {
"$match": {
// filter private pfs
"listing.search_area": { "$bitsAllClear": 2 },
}
},
2021-10-04 03:17:09 +00:00
doc! {
"$set": {
"time_left": {
"$divide": [
{
"$subtract": [
{ "$multiply": ["$listing.seconds_remaining", 1000] },
{ "$subtract": ["$$NOW", "$updated_at"] },
]
},
1000,
]
},
"updated_minute": {
"$dateTrunc": {
"date": "$updated_at",
"unit": "minute",
"binSize": 5,
},
},
}
},
doc! {
"$match": {
2021-10-08 22:33:23 +00:00
"time_left": { "$gte": 0 },
2021-10-04 03:17:09 +00:00
}
},
],
None,
)
.await;
Ok(match res {
Ok(mut cursor) => {
let mut containers = Vec::new();
while let Ok(Some(container)) = cursor.try_next().await {
let res: Result<QueriedListing> = try {
2021-10-10 23:43:29 +00:00
let result: QueriedListing = mongodb::bson::from_document(container)?;
2021-10-04 03:17:09 +00:00
result
};
if let Ok(listing) = res {
containers.push(listing);
}
}
2021-10-07 01:02:31 +00:00
containers.sort_by(|a, b| a.time_left.partial_cmp(&b.time_left).unwrap_or(Ordering::Equal));
containers.sort_by_key(|container| container.listing.pf_category());
containers.reverse();
containers.sort_by_key(|container| container.updated_minute);
containers.reverse();
2021-10-04 03:17:09 +00:00
Ok(ListingsTemplate {
containers,
2021-10-10 17:29:55 +00:00
lang,
2021-10-04 03:17:09 +00:00
})
}
Err(e) => {
eprintln!("{:#?}", e);
Ok(ListingsTemplate {
containers: Default::default(),
2021-10-10 17:29:55 +00:00
lang,
2021-10-04 03:17:09 +00:00
})
}
})
}
let route = warp::path("listings")
.and(warp::path::end())
2021-10-10 15:39:45 +00:00
.and(
2021-10-10 17:29:55 +00:00
warp::cookie::<String>("lang")
.or(warp::header::<String>("accept-language"))
.unify()
.map(Some)
.or(warp::any().map(|| None))
.unify()
2021-10-10 15:39:45 +00:00
)
.and_then(move |codes: Option<String>| logic(Arc::clone(&state), codes));
2021-10-04 03:17:09 +00:00
warp::get().and(route).boxed()
}
2021-10-23 23:19:32 +00:00
async fn stats_logic(state: Arc<State>, codes: Option<String>, seven_days: bool) -> std::result::Result<impl Reply, Infallible> {
let lang = Language::from_codes(codes.as_deref());
let stats = state.stats.read().await.clone();
Ok(match stats {
Some(stats) => StatsTemplate {
stats: if seven_days { stats.seven_days } else { stats.all_time },
lang,
},
None => panic!(),
})
}
2021-10-10 23:43:29 +00:00
fn stats(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
2021-10-23 23:19:32 +00:00
let route = warp::path("stats")
.and(warp::path::end())
.and(
warp::cookie::<String>("lang")
.or(warp::header::<String>("accept-language"))
.unify()
.map(Some)
.or(warp::any().map(|| None))
.unify()
)
.and_then(move |codes: Option<String>| stats_logic(Arc::clone(&state), codes, false));
warp::get().and(route).boxed()
}
2021-10-10 23:43:29 +00:00
2021-10-23 23:19:32 +00:00
fn stats_seven_days(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
2021-10-10 23:43:29 +00:00
let route = warp::path("stats")
2021-10-23 23:19:32 +00:00
.and(warp::path("7days"))
2021-10-10 23:43:29 +00:00
.and(warp::path::end())
.and(
warp::cookie::<String>("lang")
.or(warp::header::<String>("accept-language"))
.unify()
.map(Some)
.or(warp::any().map(|| None))
.unify()
)
2021-10-23 23:19:32 +00:00
.and_then(move |codes: Option<String>| stats_logic(Arc::clone(&state), codes, true));
2021-10-10 23:43:29 +00:00
warp::get().and(route).boxed()
}
2021-10-04 03:17:09 +00:00
fn contribute(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
async fn logic(state: Arc<State>, listing: PartyFinderListing) -> std::result::Result<impl Reply, Infallible> {
if listing.seconds_remaining > 60 * 60 {
return Ok("invalid listing".to_string());
}
let result = insert_listing(&*state, listing).await;
Ok(format!("{:#?}", result))
}
let route = warp::path("contribute")
.and(warp::path::end())
.and(warp::body::json())
.and_then(move |listing: PartyFinderListing| logic(Arc::clone(&state), listing));
warp::post().and(route).boxed()
}
fn contribute_multiple(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
async fn logic(state: Arc<State>, listings: Vec<PartyFinderListing>) -> std::result::Result<impl Reply, Infallible> {
let total = listings.len();
let mut successful = 0;
for listing in listings {
if listing.seconds_remaining > 60 * 60 {
continue;
}
let result = insert_listing(&*state, listing).await;
if result.is_ok() {
successful += 1;
2021-10-07 16:20:04 +00:00
} else {
eprintln!("{:#?}", result);
2021-10-04 03:17:09 +00:00
}
}
Ok(format!("{}/{} updated", successful, total))
}
let route = warp::path("contribute")
.and(warp::path("multiple"))
.and(warp::path::end())
.and(warp::body::json())
.and_then(move |listings: Vec<PartyFinderListing>| logic(Arc::clone(&state), listings));
warp::post().and(route).boxed()
}
2022-08-25 21:09:30 +00:00
async fn insert_listing(state: &State, mut listing: PartyFinderListing) -> mongodb::error::Result<UpdateResult> {
if listing.created_world >= 144 && listing.created_world <= 147 {
listing.created_world += 256;
}
if listing.home_world >= 144 && listing.home_world <= 147 {
listing.home_world += 256;
}
if listing.current_world >= 144 && listing.current_world <= 147 {
listing.current_world += 256;
}
2021-10-04 03:17:09 +00:00
let opts = UpdateOptions::builder()
.upsert(true)
.build();
let bson_value = mongodb::bson::to_bson(&listing).unwrap();
2021-10-07 16:46:18 +00:00
let now = Utc::now();
2021-10-04 03:17:09 +00:00
state
.collection()
.update_one(
doc! {
"listing.id": listing.id,
2021-10-07 16:20:04 +00:00
"listing.last_server_restart": listing.last_server_restart,
"listing.created_world": listing.created_world as u32,
2021-10-04 03:17:09 +00:00
},
doc! {
"$currentDate": {
"updated_at": true,
},
"$set": {
"listing": bson_value,
2021-10-07 16:46:18 +00:00
},
"$setOnInsert": {
"created_at": now,
},
2021-10-04 03:17:09 +00:00
},
opts,
)
.await
}