serverino

This commit is contained in:
Anna 2022-09-03 06:35:15 -04:00
parent d2faa0e2e4
commit 216a62708e
18 changed files with 2791 additions and 0 deletions

2
server/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
/database.sqlite*

1744
server/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

18
server/Cargo.toml Normal file
View File

@ -0,0 +1,18 @@
[package]
name = "server"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1"
base64 = "0.13"
rand = "0.8"
serde = { version = "1", features = ["derive"] }
serde_yaml = "0.9"
sha3 = "0.10"
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "sqlite", "chrono"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
uuid = { version = "1", features = ["serde", "v4"] }
warp = "0.3"

View File

@ -0,0 +1,27 @@
create table users
(
id integer not null primary key autoincrement,
auth text not null
);
create table messages
(
id text not null primary key,
user text not null references users (id) on delete cascade,
created timestamp not null default current_timestamp,
territory integer not null,
x float not null,
y float not null,
z float not null,
message text not null
);
create table votes
(
user text not null references users (id) on delete cascade,
message text not null references messages (id) on delete cascade,
vote tinyint not null,
primary key (user, message)
);
create index votes_user_idx on votes (user);

View File

View File

@ -0,0 +1,446 @@
name: Elden Ring
id: 8169c792-3d7c-44eb-aeee-9823c8521ae6
templates:
- '{0} ahead'
- 'No {0} ahead'
- '{0} required ahead'
- 'Be wary of {0}'
- 'Try {0}'
- 'Likely {0}'
- 'First off, {0}'
- 'Seek {0}'
- 'Still no {0}...'
- 'Why is it always {0}?'
- 'If only I had a {0}...'
- 'Didn''t expect {0}...'
- 'Visions of {0}...'
- 'Could this be a {0}?'
- 'Time for {0}'
- '{0}, O {0}'
- 'Behold, {0}!'
- 'Offer {0}'
- 'Praise the {0}'
- 'Let there be {0}'
- 'Ahh, {0}...'
- '{0}'
- '{0}!'
- '{0}?'
- '{0}...'
conjunctions:
- 'and then'
- 'or'
- 'but'
- 'therefore'
- 'in short'
- 'except'
- 'by the way'
- 'so to speak'
- 'all the more'
- ','
words:
- name: Enemies
words:
- 'enemy'
- 'weak foe'
- 'strong foe'
- 'monster'
- 'dragon'
- 'boss'
- 'sentry'
- 'group'
- 'pack'
- 'decoy'
- 'undead'
- 'soldier'
- 'knight'
- 'cavalier'
- 'archer'
- 'sniper'
- 'mage'
- 'ordnance'
- 'monarch'
- 'lord'
- 'demi-human'
- 'outsider'
- 'giant'
- 'horse'
- 'dog'
- 'wolf'
- 'rat'
- 'beast'
- 'bird'
- 'raptor'
- 'snake'
- 'crab'
- 'prawn'
- 'octopus'
- 'bug'
- 'scarab'
- 'slug'
- 'wraith'
- 'skeleton'
- 'monstrosity'
- 'ill-omened creature'
- name: People
words:
- 'Tarnished'
- 'warrior'
- 'swordfighter'
- 'knight'
- 'samurai'
- 'sorcerer'
- 'cleric'
- 'sage'
- 'merchant'
- 'teacher'
- 'master'
- 'friend'
- 'lover'
- 'old dear'
- 'old codger'
- 'angel'
- 'fat coinpurse'
- 'pauper'
- 'good sort'
- 'wicked sort'
- 'plump sort'
- 'skinny sort'
- 'lovable sort'
- 'pathetic sort'
- 'strange sort'
- 'nimble sort'
- 'laggardly sort'
- 'invisible sort'
- 'unfathomable sort'
- 'giant sort'
- 'sinner'
- 'thief'
- 'liar'
- 'dastard'
- 'traitor'
- 'pair'
- 'trio'
- 'noble'
- 'aristocrat'
- 'hero'
- 'champion'
- 'monarch'
- 'lord'
- 'god'
- name: Things
words:
- 'item'
- 'necessary item'
- 'precious item'
- 'something'
- 'something incredible'
- 'treasure chest'
- 'corpse'
- 'coffin'
- 'trap'
- 'armament'
- 'shield'
- 'bow'
- 'projectile weapon'
- 'armor'
- 'talisman'
- 'skill'
- 'sorcery'
- 'incantation'
- 'map'
- 'material'
- 'flower'
- 'grass'
- 'tree'
- 'fruit'
- 'seed'
- 'mushroom'
- 'tear'
- 'crystal'
- 'butterfly'
- 'bug'
- 'dung'
- 'grace'
- 'door'
- 'key'
- 'ladder'
- 'lever'
- 'lift'
- 'spiritspring'
- 'sending gate'
- 'stone astrolabe'
- 'Birdseye Telescope'
- 'message'
- 'bloodstain'
- 'Erdtree'
- 'Elden Ring'
- name: Battle Tactics
words:
- 'close-quarters battle'
- 'ranged battle'
- 'horseback battle'
- 'luring out'
- 'defeating one-by-one'
- 'taking on all at once'
- 'rushing in'
- 'stealth'
- 'mimicry'
- 'confusion'
- 'pursuit'
- 'fleeing'
- 'summoning'
- 'circling around'
- 'jumping off'
- 'dashing through'
- 'brief respite'
- name: Actions
words:
- 'attacking'
- 'jump attack'
- 'running attack'
- 'critical hit'
- 'two-handing'
- 'blocking'
- 'parrying'
- 'guard counter'
- 'sorcery'
- 'incantation'
- 'skill'
- 'summoning'
- 'throwing'
- 'healing'
- 'running'
- 'rolling'
- 'backstepping'
- 'jumping'
- 'crouching'
- 'target lock'
- 'item crafting'
- 'gesturing'
- name: Situations
words:
- 'morning'
- 'noon'
- 'evening'
- 'night'
- 'clear sky'
- 'overcast'
- 'rain'
- 'storm'
- 'mist'
- 'snow'
- 'patrolling'
- 'procession'
- 'crowd'
- 'surprise attack'
- 'ambush'
- 'pincer attack'
- 'beating to a pulp'
- 'battle'
- 'reinforcements'
- 'ritual'
- 'explosion'
- 'high spot'
- 'defensible spot'
- 'climbable spot'
- 'bright spot'
- 'dark spot'
- 'open area'
- 'cramped area'
- 'hiding place'
- 'sniping spot'
- 'recon spot'
- 'safety'
- 'danger'
- 'gorgeous view'
- 'detour'
- 'hidden path'
- 'secret passage'
- 'shortcut'
- 'dead end'
- 'looking away'
- 'unnoticed'
- 'out of stamina'
- name: Places
words:
- 'high road'
- 'checkpoint'
- 'bridge'
- 'castle'
- 'fort'
- 'city'
- 'ruins'
- 'church'
- 'tower'
- 'camp site'
- 'house'
- 'cemetery'
- 'underground tomb'
- 'tunnel'
- 'cave'
- 'evergaol'
- 'great tree'
- 'cellar'
- 'surface'
- 'underground'
- 'forest'
- 'river'
- 'lake'
- 'bog'
- 'mountain'
- 'valley'
- 'cliff'
- 'waterside'
- 'nest'
- 'hole'
- name: Directions
words:
- 'east'
- 'west'
- 'south'
- 'north'
- 'ahead'
- 'behind'
- 'left'
- 'right'
- 'center'
- 'up'
- 'down'
- 'edge'
- name: Body Parts
words:
- 'head'
- 'stomach'
- 'back'
- 'arms'
- 'legs'
- 'rump'
- 'tail'
- 'core'
- 'fingers'
- name: Affinities
words:
- 'physical'
- 'standard'
- 'striking'
- 'slashing'
- 'piercing'
- 'fire'
- 'lightning'
- 'magic'
- 'holy'
- 'poison'
- 'toxic'
- 'scarlet rot'
- 'blood loss'
- 'frost'
- 'sleep'
- 'madness'
- 'death'
- name: Concepts
words:
- 'life'
- 'Death'
- 'light'
- 'dark'
- 'stars'
- 'fire'
- 'Order'
- 'chaos'
- 'joy'
- 'wrath'
- 'suffering'
- 'sadness'
- 'comfort'
- 'bliss'
- 'misfortune'
- 'good fortune'
- 'bad luck'
- 'hope'
- 'despair'
- 'victory'
- 'defeat'
- 'research'
- 'faith'
- 'abundance'
- 'rot'
- 'loyalty'
- 'injustice'
- 'secret'
- 'opportunity'
- 'pickle'
- 'clue'
- 'friendship'
- 'love'
- 'bravery'
- 'vigor'
- 'fortitude'
- 'confidence'
- 'distracted'
- 'unguarded'
- 'introspection'
- 'regret'
- 'resignation'
- 'futility'
- 'on the brink'
- 'betrayal'
- 'revenge'
- 'destruction'
- 'recklessness'
- 'calmness'
- 'vigilance'
- 'tranquility'
- 'sound'
- 'tears'
- 'sleep'
- 'depths'
- 'dregs'
- 'fear'
- 'sacrifice'
- 'ruin'
- name: Phrases
words:
- 'good luck'
- 'look carefully'
- 'listen carefully'
- 'think carefully'
- 'well done'
- 'I did it!'
- 'I''ve failed...'
- 'here!'
- 'not here!'
- 'don''t you dare!'
- 'do it!'
- 'I can''t take this...'
- 'don''t think'
- 'so lonely...'
- 'here again...'
- 'just getting started'
- 'stay calm'
- 'keep moving'
- 'turn back'
- 'give up'
- 'don''t give up'
- 'help me...'
- 'I don''t believe it...'
- 'too high up'
- 'I want to go home...'
- 'it''s like a dream...'
- 'seems familiar...'
- 'beautiful...'
- 'you don''t have the right'
- 'are you ready?'

17
server/packs/ffxiv.yaml Normal file
View File

@ -0,0 +1,17 @@
name: FINAL FANTASY XIV
id: 7cd9e479-080a-4fec-9511-41a53034c2ad
templates:
- '{0} ahead'
- 'Be wary of {0}'
- 'Try {0}'
- 'Need {0}'
- 'Imminent {0}...'
- 'Weakness: {0}'
- '{0}'
- '{0}?'
- 'Good Luck'
- 'I did it!'
- 'Here!'
- 'I can''t take this...'
- 'Praise the Sun!'

95
server/src/main.rs Normal file
View File

@ -0,0 +1,95 @@
#![feature(let_chains)]
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use anyhow::{Context, Result};
use sqlx::{Executor, Pool, Sqlite};
use sqlx::migrate::Migrator;
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
use tokio::sync::RwLock;
use uuid::Uuid;
use crate::pack::Pack;
mod pack;
mod message;
mod web;
mod util;
static MIGRATOR: Migrator = sqlx::migrate!();
pub struct State {
pub db: Pool<Sqlite>,
pub packs: RwLock<HashMap<Uuid, Pack>>,
}
impl State {
pub async fn update_packs(&self) -> Result<()> {
let mut packs = HashMap::new();
let mut dir = tokio::fs::read_dir("packs").await?;
while let Ok(Some(entry)) = dir.next_entry().await {
if !entry.path().is_file() {
continue;
}
match entry.path().extension().and_then(|x| x.to_str()) {
Some("yaml") | Some("yml") => {}
_ => continue,
}
let text = match tokio::fs::read_to_string(entry.path()).await {
Ok(t) => t,
Err(e) => {
eprintln!("error reading pack: {:#?}", e);
continue;
}
};
match serde_yaml::from_str::<Pack>(&text) {
Ok(pack) => {
println!("added {}", pack.name);
packs.insert(pack.id, pack);
}
Err(e) => eprintln!("error parsing pack at {:?}: {:#?}", entry.path(), e),
}
}
*self.packs.write().await = packs;
Ok(())
}
}
#[tokio::main]
async fn main() -> Result<()> {
let options = SqliteConnectOptions::new();
// options.log_statements(LevelFilter::Debug);
let pool = SqlitePoolOptions::new()
.after_connect(|conn, _| Box::pin(async move {
conn.execute(
// language=sqlite
"PRAGMA foreign_keys = ON;"
).await?;
Ok(())
}))
// .connect_with(options.filename(&config.database.path))
.connect_with(options.filename("./database.sqlite"))
.await
.context("could not connect to database")?;
MIGRATOR.run(&pool)
.await
.context("could not run database migrations")?;
let state = Arc::new(State {
db: pool,
packs: Default::default(),
});
state.update_packs().await?;
warp::serve(web::routes(state)).run("127.0.0.1:8080".parse::<SocketAddr>()?).await;
Ok(())
}

41
server/src/message.rs Normal file
View File

@ -0,0 +1,41 @@
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Deserialize)]
pub struct Message {
pub territory: u32,
pub x: f32,
pub y: f32,
pub z: f32,
pub pack_id: Uuid,
pub template_1: usize,
pub word_1_list: Option<usize>,
pub word_1_word: Option<usize>,
pub conjugation: Option<usize>,
pub template_2: Option<usize>,
pub word_2_list: Option<usize>,
pub word_2_word: Option<usize>,
}
#[derive(Debug, Deserialize)]
pub struct VoteMessage {
pub post_id: Uuid,
pub vote: u8,
}
#[derive(Debug, Deserialize)]
pub struct DeleteMessage {
pub post_id: Uuid,
}
#[derive(Debug, Serialize)]
pub struct RetrievedMessage {
pub id: String,
pub x: f64,
pub y: f64,
pub z: f64,
pub message: String,
pub positive_votes: i32,
pub negative_votes: i32,
}

62
server/src/pack.rs Normal file
View File

@ -0,0 +1,62 @@
use serde::Deserialize;
use uuid::Uuid;
#[derive(Debug, Deserialize)]
pub struct Pack {
pub name: String,
pub id: Uuid,
pub templates: Vec<String>,
pub conjunctions: Vec<String>,
pub words: Vec<WordList>,
}
#[derive(Debug, Deserialize)]
pub struct WordList {
pub name: String,
pub words: Vec<String>,
}
impl Pack {
pub fn format(&self, template_1_idx: usize, word_1_idx: Option<(usize, usize)>, conjunction: Option<usize>, template_2_idx: Option<usize>, word_2_idx: Option<(usize, usize)>) -> Option<String> {
let template_1 = self.templates.get(template_1_idx)?;
if template_1.contains("{0}") && word_1_idx.is_none() {
return None;
}
let mut formatted = if template_1.contains("{0}") && let Some((w1_list, w1_word)) = word_1_idx {
let word_1 = self.words.get(w1_list)?.words.get(w1_word)?;
template_1.replace("{0}", word_1)
} else {
template_1.clone()
};
if let Some(conj_idx) = conjunction {
if let Some(template_2_idx) = template_2_idx {
let conj = self.conjunctions.get(conj_idx)?;
if conj.len() > 1 || conj.chars().next().map(|x| !x.is_ascii_punctuation()).unwrap_or(false) {
formatted.push('\n');
}
formatted.push_str(conj);
formatted.push(' ');
let template_2 = self.templates.get(template_2_idx)?;
if template_2.contains("{0}") && word_2_idx.is_none() {
return None;
}
let append = if let Some((w2_list, w2_word)) = word_2_idx {
let word_2 = self.words.get(w2_list)?.words.get(w2_word)?;
template_2.replace("{0}", word_2)
} else {
template_2.clone()
};
formatted.push_str(&append);
}
}
Some(formatted)
}
}

8
server/src/util.rs Normal file
View File

@ -0,0 +1,8 @@
use sha3::{Digest, Sha3_384};
pub fn hash(input: &str) -> String {
let mut hasher = Sha3_384::default();
hasher.update(input.as_bytes());
let result = hasher.finalize();
base64::encode(result)
}

84
server/src/web.rs Normal file
View File

@ -0,0 +1,84 @@
use std::convert::Infallible;
use std::sync::Arc;
use warp::{Filter, Rejection, Reply};
use warp::filters::BoxedFilter;
use warp::http::StatusCode;
use warp::reject::Reject;
use crate::State;
mod register;
mod unregister;
mod write;
mod erase;
mod get_location;
mod vote;
pub fn routes(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
register::register(Arc::clone(&state))
.or(unregister::unregister(Arc::clone(&state)))
.or(write::write(Arc::clone(&state)))
.or(erase::erase(Arc::clone(&state)))
.or(get_location::get_location(Arc::clone(&state)))
.or(vote::vote(Arc::clone(&state)))
.recover(handle_rejection)
.boxed()
}
pub fn get_id(state: Arc<State>) -> BoxedFilter<(i64, )> {
warp::cookie("access_token")
.or(warp::header("x-api-key"))
.unify()
.and_then(move |access_token: String| {
let state = Arc::clone(&state);
async move {
let hashed = crate::util::hash(&access_token);
let id = sqlx::query!(
// language=sqlite
"select id from users where auth = ?",
hashed,
)
.fetch_optional(&state.db)
.await;
match id {
Ok(Some(i)) => Ok(i.id),
Ok(None) => Err(warp::reject::custom(WebError::InvalidAuthToken)),
Err(e) => Err(warp::reject::custom(AnyhowRejection(e.into()))),
}
}
})
.boxed()
}
#[derive(Debug)]
pub enum WebError {
InvalidAuthToken,
InvalidPackId,
InvalidIndex,
}
impl Reject for WebError {}
#[derive(Debug)]
pub struct AnyhowRejection(anyhow::Error);
impl Reject for AnyhowRejection {}
async fn handle_rejection(err: Rejection) -> Result<impl Reply, Infallible> {
let status = if let Some(AnyhowRejection(e)) = err.find::<AnyhowRejection>() {
eprintln!("{:#?}", e);
StatusCode::INTERNAL_SERVER_ERROR
} else if let Some(e) = err.find::<WebError>() {
match e {
WebError::InvalidAuthToken => StatusCode::BAD_REQUEST,
WebError::InvalidPackId => StatusCode::NOT_FOUND,
WebError::InvalidIndex => StatusCode::NOT_FOUND,
}
} else {
eprintln!("{:#?}", err);
StatusCode::INTERNAL_SERVER_ERROR
};
Ok(warp::reply::with_status(warp::reply(), status))
}

35
server/src/web/erase.rs Normal file
View File

@ -0,0 +1,35 @@
use std::sync::Arc;
use anyhow::Context;
use uuid::Uuid;
use warp::{Filter, Rejection, Reply};
use warp::filters::BoxedFilter;
use crate::State;
use crate::web::AnyhowRejection;
pub fn erase(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
warp::delete()
.and(warp::path("messages"))
.and(warp::path::param())
.and(warp::path::end())
.and(super::get_id(Arc::clone(&state)))
.and_then(move |post_id: Uuid, id: i64| logic(Arc::clone(&state), id, post_id))
.boxed()
}
async fn logic(state: Arc<State>, id: i64, post_id: Uuid) -> Result<impl Reply, Rejection> {
let post_id = post_id.simple().to_string();
sqlx::query!(
// language=sqlite
"delete from messages where id = ? and user = ?",
post_id,
id,
)
.execute(&state.db)
.await
.context("could not delete message from database")
.map_err(AnyhowRejection)
.map_err(warp::reject::custom)?;
Ok(warp::reply())
}

View File

@ -0,0 +1,45 @@
use std::sync::Arc;
use anyhow::Context;
use warp::{Filter, Rejection, Reply};
use warp::filters::BoxedFilter;
use crate::message::RetrievedMessage;
use crate::State;
use crate::web::AnyhowRejection;
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_then(move |location: u32| logic(Arc::clone(&state), location))
.boxed()
}
async fn logic(state: Arc<State>, location: u32) -> Result<impl Reply, Rejection> {
let id = location as i64;
let messages = sqlx::query_as!(
RetrievedMessage,
// language=sqlite
r#"
select m.id,
m.x,
m.y,
m.z,
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
from messages m
left join votes v on m.id = v.message
where m.territory = ?
group by m.id"#,
id,
)
.fetch_all(&state.db)
.await
.context("could not get messages from database")
.map_err(AnyhowRejection)
.map_err(warp::reject::custom)?;
Ok(warp::reply::json(&messages))
}

View File

@ -0,0 +1,33 @@
use std::sync::Arc;
use anyhow::{Context, Result};
use rand::distributions::{Alphanumeric, DistString};
use warp::{Filter, Rejection, Reply};
use warp::filters::BoxedFilter;
use crate::State;
use crate::web::AnyhowRejection;
pub fn register(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
warp::post()
.and(warp::path("account"))
.and(warp::path::end())
.and_then(move || logic(Arc::clone(&state)))
.boxed()
}
async fn logic(state: Arc<State>) -> Result<impl Reply, Rejection> {
let auth = Alphanumeric.sample_string(&mut rand::thread_rng(), 32);
let hashed = crate::util::hash(&auth);
sqlx::query!(
// language=sqlite
"insert into users (auth) values (?)",
hashed,
)
.execute(&state.db)
.await
.context("could not insert user into database")
.map_err(AnyhowRejection)
.map_err(warp::reject::custom)?;
Ok(auth)
}

View File

@ -0,0 +1,31 @@
use std::sync::Arc;
use anyhow::Context;
use warp::{Filter, Rejection, Reply};
use warp::filters::BoxedFilter;
use crate::State;
use crate::web::AnyhowRejection;
pub fn unregister(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
warp::delete()
.and(warp::path("account"))
.and(warp::path::end())
.and(super::get_id(Arc::clone(&state)))
.and_then(move |id: i64| logic(Arc::clone(&state), id))
.boxed()
}
async fn logic(state: Arc<State>, id: i64) -> Result<impl Reply, Rejection> {
sqlx::query!(
// language=sqlite
"delete from users where id = ?",
id,
)
.execute(&state.db)
.await
.context("could not delete user from database")
.map_err(AnyhowRejection)
.map_err(warp::reject::custom)?;
Ok(warp::reply())
}

44
server/src/web/vote.rs Normal file
View File

@ -0,0 +1,44 @@
use std::sync::Arc;
use anyhow::Context;
use uuid::Uuid;
use warp::{Filter, Rejection, Reply};
use warp::filters::BoxedFilter;
use crate::State;
use crate::web::AnyhowRejection;
pub fn vote(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
warp::patch()
.and(warp::path("messages"))
.and(warp::path::param())
.and(warp::path("votes"))
.and(warp::path::end())
.and(super::get_id(Arc::clone(&state)))
.and(warp::body::json())
.and_then(move |message_id: Uuid, id: i64, vote: i8| logic(Arc::clone(&state), id, message_id, vote))
.boxed()
}
async fn logic(state: Arc<State>, id: i64, message_id: Uuid, vote: i8) -> Result<impl Reply, Rejection> {
let message_id = message_id.simple().to_string();
let vote = match vote.signum() {
-1 => -1,
0 => 1,
1 => 1,
_ => unreachable!(),
};
sqlx::query!(
// language=sqlite
"insert or ignore into votes (user, message, vote) values (?, ?, ?)",
id,
message_id,
vote,
)
.execute(&state.db)
.await
.context("could not insert vote into database")
.map_err(AnyhowRejection)
.map_err(warp::reject::custom)?;
Ok(warp::reply())
}

59
server/src/web/write.rs Normal file
View File

@ -0,0 +1,59 @@
use std::sync::Arc;
use anyhow::Context;
use uuid::Uuid;
use warp::{Filter, Rejection, Reply};
use warp::filters::BoxedFilter;
use crate::message::Message;
use crate::State;
use crate::web::{AnyhowRejection, WebError};
pub fn write(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
warp::post()
.and(warp::path("messages"))
.and(warp::path::end())
.and(super::get_id(Arc::clone(&state)))
.and(warp::body::json())
.and_then(move |id: i64, message: Message| logic(Arc::clone(&state), id, message))
.boxed()
}
async fn logic(state: Arc<State>, id: i64, message: Message) -> Result<impl Reply, Rejection> {
let text = {
let packs = state.packs.read().await;
let pack = packs.get(&message.pack_id)
.ok_or(WebError::InvalidPackId)
.map_err(warp::reject::custom)?;
pack.format(
message.template_1,
message.word_1_list.and_then(|list| message.word_1_word.map(|word| (list, word))),
message.conjugation,
message.template_2,
message.word_2_list.and_then(|list| message.word_2_word.map(|word| (list, word))),
)
.ok_or(WebError::InvalidIndex)
.map_err(warp::reject::custom)?
};
let message_id = Uuid::new_v4().simple().to_string();
let territory = message.territory as i64;
sqlx::query!(
// language=sqlite
"insert into messages (id, user, territory, x, y, z, message) values (?, ?, ?, ?, ?, ?, ?)",
message_id,
id,
territory,
message.x,
message.y,
message.z,
text,
)
.execute(&state.db)
.await
.context("could not insert message into database")
.map_err(AnyhowRejection)
.map_err(warp::reject::custom)?;
Ok(message_id)
}