2021-10-07 01:02:31 +00:00
|
|
|
use std::cmp::Ordering;
|
2021-10-08 21:56:35 +00:00
|
|
|
use std::convert::Infallible;
|
2021-10-04 03:17:09 +00:00
|
|
|
use anyhow::{Result, Context};
|
|
|
|
use std::sync::Arc;
|
2021-10-07 16:46:18 +00:00
|
|
|
use chrono::Utc;
|
2021-10-04 05:02:36 +00:00
|
|
|
use mongodb::{Client as MongoClient, Collection, IndexModel};
|
|
|
|
use mongodb::options::{IndexOptions, UpdateOptions};
|
2021-10-04 03:17:09 +00:00
|
|
|
use mongodb::results::UpdateResult;
|
|
|
|
use tokio_stream::StreamExt;
|
|
|
|
use warp::{Filter, Reply};
|
|
|
|
use warp::filters::BoxedFilter;
|
|
|
|
use warp::http::Uri;
|
|
|
|
use crate::config::Config;
|
2021-10-10 17:29:55 +00:00
|
|
|
use crate::ffxiv::Language;
|
2021-10-04 03:17:09 +00:00
|
|
|
use crate::listing::PartyFinderListing;
|
|
|
|
use crate::listing_container::{ListingContainer, QueriedListing};
|
|
|
|
use crate::template::listings::ListingsTemplate;
|
|
|
|
|
|
|
|
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(())
|
|
|
|
}
|
|
|
|
|
|
|
|
struct State {
|
|
|
|
config: Arc<Config>,
|
|
|
|
mongo: MongoClient,
|
|
|
|
}
|
|
|
|
|
|
|
|
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-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
|
|
|
|
.context("could not create index")?;
|
|
|
|
|
|
|
|
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)))
|
|
|
|
.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())
|
|
|
|
.or(listings_css())
|
2021-10-04 19:14:40 +00:00
|
|
|
.or(listings_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()
|
|
|
|
}
|
|
|
|
|
|
|
|
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-04 03:17:09 +00:00
|
|
|
fn index() -> BoxedFilter<(impl Reply, )> {
|
|
|
|
let route = warp::path::end()
|
|
|
|
.map(|| warp::redirect(Uri::from_static("/listings")));
|
|
|
|
warp::get().and(route).boxed()
|
|
|
|
}
|
|
|
|
|
|
|
|
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-04 03:17:09 +00:00
|
|
|
use mongodb::bson::doc;
|
2021-10-10 17:29:55 +00:00
|
|
|
let lang = Language::from_codes(codes.as_deref());
|
2021-10-04 03:17:09 +00:00
|
|
|
|
|
|
|
let res = state
|
|
|
|
.collection()
|
|
|
|
.aggregate(
|
|
|
|
[
|
|
|
|
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 {
|
|
|
|
let json = serde_json::to_vec(&container)?;
|
|
|
|
let result: QueriedListing = serde_json::from_slice(&json)?;
|
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn insert_listing(state: &State, listing: PartyFinderListing) -> mongodb::error::Result<UpdateResult> {
|
|
|
|
use mongodb::bson::doc;
|
|
|
|
|
|
|
|
let opts = UpdateOptions::builder()
|
|
|
|
.upsert(true)
|
|
|
|
.build();
|
2021-10-08 21:56:35 +00:00
|
|
|
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
|
|
|
|
}
|