OrangeGuidanceTomestone/server/src/web/get_location.rs

181 lines
5.9 KiB
Rust

use std::sync::Arc;
use anyhow::Context;
use chrono::{Duration, Utc};
use parking_lot::RwLock;
use rand::Rng;
use rand::seq::SliceRandom;
use rayon::prelude::*;
use serde::Deserialize;
use warp::{Filter, Rejection, Reply};
use warp::filters::BoxedFilter;
use crate::message::RetrievedMessage;
use crate::State;
use crate::util::HOUSING_ZONES;
use crate::web::{AnyhowRejection, WebError};
pub fn get_location(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
warp::get()
.and(warp::path("messages"))
.and(warp::path::param())
.and(warp::path::end())
.and(super::get_id(Arc::clone(&state)))
.and(warp::query::<GetLocationQuery>())
.and_then(move |location: u32, (id, _), query| logic(Arc::clone(&state), id, location, query))
.boxed()
}
#[derive(Deserialize)]
pub struct GetLocationQuery {
#[serde(default)]
world: Option<u32>,
#[serde(default)]
ward: Option<u32>,
#[serde(default)]
plot: Option<u32>,
}
async fn logic(state: Arc<State>, id: i64, location: u32, query: GetLocationQuery) -> Result<impl Reply, Rejection> {
let housing = HOUSING_ZONES.contains(&location);
if housing && (query.world.is_none() || query.ward.is_none()) {
return Err(warp::reject::custom(WebError::MissingHousingInfo));
}
if !housing && (query.ward.is_some() || query.plot.is_some()) {
return Err(warp::reject::custom(WebError::UnnecessaryHousingInfo));
}
let world = if housing {
query.world
} else {
None
};
let location = location as i64;
let mut messages = sqlx::query_as!(
RetrievedMessage,
// language=sqlite
r#"
select m.id,
m.x,
m.y,
m.z,
m.yaw,
m.message,
coalesce(sum(v.vote between 0 and 1), 0) as positive_votes,
coalesce(sum(v.vote between -1 and 0), 0) as negative_votes,
v2.vote as user_vote,
m.glyph,
m.created,
m.user,
cast((julianday(current_timestamp) - julianday(u.last_seen)) * 1440 as int) as last_seen_minutes
from messages m
left join votes v on m.id = v.message
left join votes v2 on m.id = v2.message and v2.user = ?
inner join users u on m.user = u.id
where m.territory = ? and m.world is ? and m.ward is ? and m.plot is ?
group by m.id"#,
id,
location,
world,
query.ward,
query.plot,
)
.fetch_all(&state.db)
.await
.context("could not get messages from database")
.map_err(AnyhowRejection)
.map_err(warp::reject::custom)?;
filter_messages(&mut messages, id);
Ok(warp::reply::json(&messages))
}
fn filter_messages(messages: &mut Vec<RetrievedMessage>, id: i64) {
// remove messages where the user has been offline for over 35 minutes
// also remove messages with low score (that aren't the from the user)
messages.drain_filter(|msg| msg.last_seen_minutes >= 35 || (msg.user != id && (msg.positive_votes - msg.negative_votes) < crate::consts::VOTE_THRESHOLD_HIDE));
// shuffle messages since we'll be excluding later based on messages
// that have already been included, so this will be more fair
messages.shuffle(&mut rand::thread_rng());
// just count nearby messages. this is O(n^2) but alternatives are hard
let ids = RwLock::new(Vec::with_capacity(messages.len()));
messages.par_iter()
.for_each(|a| {
if a.user == id {
ids.write().push(a.id.clone());
return;
}
let mut nearby_ids = Vec::new();
for b in messages.iter() {
if a.id == b.id {
continue;
}
let distance = (a.x - b.x).powi(2)
+ (a.y - b.y).powi(2)
+ (a.z - b.z).powi(2);
// 10 squared
if distance >= 100.0 {
continue;
}
nearby_ids.push(&b.id);
}
let mut nearby = nearby_ids.len() as u32;
let (numerator, denominator) = if nearby <= 2 {
// no need to do calculations for groups of three or fewer
(17, 20)
} else {
let already_visible = {
let ids = ids.read();
nearby_ids.iter()
.filter(|id| ids.contains(id))
.count()
};
if already_visible >= 3 {
return;
}
let time_since_creation = a.created.signed_duration_since(Utc::now().naive_utc());
let brand_new = time_since_creation < Duration::minutes(30);
let new = time_since_creation < Duration::hours(2);
let mut numerator = 1;
if brand_new {
numerator = nearby;
} else if new {
numerator += (nearby / 3).min(1);
}
let score = (a.positive_votes - a.negative_votes).max(0);
if score > 0 {
let pad = score as f32 / nearby as f32;
let rounded = pad.floor() as u32;
numerator += rounded.max(nearby / 2);
}
nearby *= 2;
if numerator * 5 > nearby * 4 {
numerator = 4;
nearby = 5;
}
(numerator, nearby)
};
if rand::thread_rng().gen_ratio(numerator.min(denominator), denominator) {
ids.write().push(a.id.clone());
}
});
let ids = ids.into_inner();
messages.drain_filter(|msg| !ids.contains(&msg.id));
}