gourami

[UNMAINTAINED] Activitypub server in Rust
Log | Files | Refs | README | LICENSE

commit 25cd9034950236d3ec30120bd2decd6c9db2ec2e
parent e553a7e0cf3066536f2b39476616851a21e3757d
Author: alex wennerberg <alex@alexwennerberg.com>
Date:   Sun,  3 May 2020 10:25:37 -0500

Run cargo fmt

Diffstat:
Msrc/ap.rs | 145+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Msrc/db/conn.rs | 7++++---
Msrc/db/mod.rs | 4++--
Msrc/db/note.rs | 93+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msrc/db/notification.rs | 35++++++++++++++++++-----------------
Msrc/db/schema.rs | 2+-
Msrc/db/user.rs | 56++++++++++++++++++++++++--------------------------------
Msrc/lib.rs | 455++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Msrc/main.rs | 14++++----------
Msrc/routes.rs | 109++++++++++++++++++++++++++++++++++++++-----------------------------------------
Msrc/session.rs | 44++++++++++++++++++++------------------------
11 files changed, 553 insertions(+), 411 deletions(-)

diff --git a/src/ap.rs b/src/ap.rs @@ -1,3 +1,12 @@ +use crate::db::conn::POOL; +use crate::db::note::{NoteInput, RemoteNoteInput}; +use crate::db::user::{NewRemoteUser, User}; +use diesel::insert_into; +use diesel::prelude::*; +use diesel::sqlite::SqliteConnection; +use serde_json::json; +use serde_json::Value; +use std::env; /// Users don't follow users in Gourami. Instead the server does hte following /// There are a number of reasons for this: /// Gives it a more 'community' feel -- everyone shares the same timeline @@ -7,27 +16,17 @@ /// This is a somewhat eccentric activitypub implementation, but it is as consistent with the spec /// as I can make it! use std::fs; -use serde_json::json; -use serde_json::{Value}; -use crate::db::note::{NoteInput, RemoteNoteInput}; -use crate::db::user::{User, NewRemoteUser}; -use crate::db::conn::POOL; -use diesel::insert_into; -use diesel::prelude::*; -use diesel::sqlite::SqliteConnection; -use std::env; lazy_static! { // const SERVER_ACTOR = "gourami.social" } - -// ActivityPub outbox +// ActivityPub outbox fn send_to_outbox(activity: bool) { // activitystreams object - // fetch/store from db. - // db objects need to serialize/deserialize this object - // if get -> fetch from db - // if post -> put to db, send to inbox of followers - // send to inbox of followers + // fetch/store from db. + // db objects need to serialize/deserialize this object + // if get -> fetch from db + // if post -> put to db, send to inbox of followers + // send to inbox of followers } enum Action { @@ -63,68 +62,91 @@ fn categorize_input_message(v: Value) -> Action { Action::DoNothing } -pub fn process_create_note(conn: &SqliteConnection, v: Value) -> Result<(), Box<dyn std::error::Error>>{ +pub fn process_create_note( + conn: &SqliteConnection, + v: Value, +) -> Result<(), Box<dyn std::error::Error>> { // Actions usually associated with notes // maybe there's a cleaner way to do this. cant iterate over types // TODO inbox forwarding https://www.w3.org/TR/activitypub/#inbox-forwarding let object = v.get("object").ok_or("No object found")?; let _type = object.get("type").ok_or("No object type found")?; - let content = object.get("content").ok_or("No content found")?.as_str().ok_or("Not a string")?; - // clean content + let content = object + .get("content") + .ok_or("No content found")? + .as_str() + .ok_or("Not a string")?; + // clean content // let in_reply_to = match object.get("inReplyTo") { // Some(v) => Some(v.as_str().ok_or("Not a string")?), // TODO -- get reply from database - // None => None + // None => None // }; - let remote_creator = object.get("attributedTo").ok_or("No attributedTo found")?.as_str().ok_or("Not a string")?; - let remote_url = object.get("url").ok_or("No url Found")?.as_str().ok_or("Not a string")?; - let remote_id = object.get("id").ok_or("No ID found")?.as_str().ok_or("Not a string")?; + let remote_creator = object + .get("attributedTo") + .ok_or("No attributedTo found")? + .as_str() + .ok_or("Not a string")?; + let remote_url = object + .get("url") + .ok_or("No url Found")? + .as_str() + .ok_or("Not a string")?; + let remote_id = object + .get("id") + .ok_or("No ID found")? + .as_str() + .ok_or("Not a string")?; - use crate::db::schema::users::dsl as u; use crate::db::schema::notes::dsl as n; + use crate::db::schema::users::dsl as u; // if user not in db, insert // let new_user = NewRemoteUser { - username: String::from(remote_creator), - remote_url: Some(String::from(remote_creator)), + username: String::from(remote_creator), + remote_url: Some(String::from(remote_creator)), }; // - insert_into(u::users) - .values(&new_user) - .execute(conn).ok(); // TODO only check unique constraint error + insert_into(u::users).values(&new_user).execute(conn).ok(); // TODO only check unique constraint error - let new_user_id: i32 = u::users.select(u::id) + let new_user_id: i32 = u::users + .select(u::id) .filter(u::remote_url.eq(remote_creator)) - .first(conn).unwrap(); + .first(conn) + .unwrap(); let new_remote_note = RemoteNoteInput { - content: String::from(content), - in_reply_to: None, // TODO - neighborhood: true, - is_remote: true, - user_id: new_user_id, // for remote. placeholder. not sure what to do with this ultimately - remote_creator: String::from(remote_creator), - remote_id: String::from(remote_id), - remote_url: String::from(remote_url) } ; + content: String::from(content), + in_reply_to: None, // TODO + neighborhood: true, + is_remote: true, + user_id: new_user_id, // for remote. placeholder. not sure what to do with this ultimately + remote_creator: String::from(remote_creator), + remote_id: String::from(remote_id), + remote_url: String::from(remote_url), + }; println!("{:?}", new_remote_note); - insert_into(n::notes).values(&new_remote_note) - .execute(conn).unwrap(); + insert_into(n::notes) + .values(&new_remote_note) + .execute(conn) + .unwrap(); return Ok(()); } - -pub fn get_destinations() -> Vec<String> { // maybe lazy static this +pub fn get_destinations() -> Vec<String> { + // maybe lazy static this use crate::db::schema::server_mutuals::dsl::*; let conn = &POOL.get().unwrap(); server_mutuals.select(inbox_url).load(conn).unwrap() } -pub async fn send_ap_message(ap_message: &Value, destinations: Vec<String>) -> Result<(), reqwest::Error> { +pub async fn send_ap_message( + ap_message: &Value, + destinations: Vec<String>, +) -> Result<(), reqwest::Error> { // Right now we have only once delivery for destination in destinations { let client = reqwest::Client::new(); - client.post(&destination) - .json(&ap_message) - .send().await?; + client.post(&destination).json(&ap_message).send().await?; } Ok(()) } @@ -134,7 +156,7 @@ fn follow_remote_server(remote_url: String) { } fn generate_server_follow(remote_url: String) -> Value { - json!({ + json!({ "@context": "https://www.w3.org/ns/activitystreams", "id": "https://my-example.com/my-first-follow", "type": "Follow", @@ -229,15 +251,22 @@ mod tests { } } }"#).unwrap(); - assert_eq!(process_create_note(create_note_mastodon).unwrap(), RemoteNoteInput { - content: String::from("hello world"), - in_reply_to: None, - neighborhood: true, - is_remote: true, - user_id: -1, // for remote. placeholder. not sure what to do with this ultimately - remote_creator: String::from("https://mastodon.social/users/alexwennerberg"), - remote_id: String::from("https://mastodon.social/users/alexwennerberg/statuses/104028309437021899"), - remote_url: String::from("https://mastodon.social/@alexwennerberg/104028309437021899"), - }) + assert_eq!( + process_create_note(create_note_mastodon).unwrap(), + RemoteNoteInput { + content: String::from("hello world"), + in_reply_to: None, + neighborhood: true, + is_remote: true, + user_id: -1, // for remote. placeholder. not sure what to do with this ultimately + remote_creator: String::from("https://mastodon.social/users/alexwennerberg"), + remote_id: String::from( + "https://mastodon.social/users/alexwennerberg/statuses/104028309437021899" + ), + remote_url: String::from( + "https://mastodon.social/@alexwennerberg/104028309437021899" + ), + } + ) } } diff --git a/src/db/conn.rs b/src/db/conn.rs @@ -1,15 +1,16 @@ -use std; use diesel::r2d2::{ConnectionManager, Pool}; use diesel::sqlite::SqliteConnection; +use std; type SqlitePool = Pool<ConnectionManager<SqliteConnection>>; -// We use a global shared sqlite connection because it's simple and performance is not +// We use a global shared sqlite connection because it's simple and performance is not // very important lazy_static! { pub static ref POOL: SqlitePool = pooled_sqlite(); } fn pooled_sqlite() -> SqlitePool { - let manager = ConnectionManager::<SqliteConnection>::new(std::env::var("DATABASE_URL").unwrap()); + let manager = + ConnectionManager::<SqliteConnection>::new(std::env::var("DATABASE_URL").unwrap()); Pool::new(manager).expect("Postgres connection pool could not be created") } diff --git a/src/db/mod.rs b/src/db/mod.rs @@ -1,5 +1,5 @@ +pub mod conn; pub mod note; +pub mod notification; pub mod schema; pub mod user; -pub mod notification; -pub mod conn; diff --git a/src/db/note.rs b/src/db/note.rs @@ -1,10 +1,10 @@ -use std::env; -use maplit::hashset; use super::schema::notes; -use serde::{de::Error, Deserialize, Serialize, Deserializer}; -use regex::Regex; +use crate::db::user::User; use ammonia; -use crate::db::user::User; // weird import +use maplit::hashset; +use regex::Regex; +use serde::{de::Error, Deserialize, Deserializer, Serialize}; +use std::env; // weird import /// This isn't queryable directly, /// It only works when joined with the users table @@ -12,17 +12,18 @@ use crate::db::user::User; // weird import #[derive(Queryable, Debug, QueryableByName, Associations, Clone, Deserialize, Serialize)] #[belongs_to(User)] #[table_name = "notes"] -pub struct Note { // rename RenderedNote - pub id: i32, - pub user_id: i32, - pub in_reply_to: Option<i32>, - pub content: String, - pub created_time: String, - pub neighborhood: bool, - pub is_remote: bool, - pub remote_url: Option<String>, - pub remote_creator: Option<String>, - pub remote_id: Option<String> +pub struct Note { + // rename RenderedNote + pub id: i32, + pub user_id: i32, + pub in_reply_to: Option<i32>, + pub content: String, + pub created_time: String, + pub neighborhood: bool, + pub is_remote: bool, + pub remote_url: Option<String>, + pub remote_creator: Option<String>, + pub remote_id: Option<String>, } impl Note { @@ -33,7 +34,7 @@ impl Note { /// Content in the DB is stored in plaintext (WILL BE) /// We want to render it so that it is rendered in HTML -/// This basically just means escaping characters and adding +/// This basically just means escaping characters and adding /// automatic URL parsing /// @@ -49,29 +50,29 @@ fn remove_unnacceptable_html(input_text: &str) -> String { .tags(ok_tags) .clean(input_text) .to_string(); - return html_clean + return html_clean; } #[derive(Insertable, Clone, Debug)] #[table_name = "notes"] pub struct NoteInput { - //pub id: i32, //unsigned? - pub user_id: i32, - pub content: String, - pub in_reply_to: Option<i32>, - pub neighborhood: bool, + //pub id: i32, //unsigned? + pub user_id: i32, + pub content: String, + pub in_reply_to: Option<i32>, + pub neighborhood: bool, } #[derive(Insertable, Eq, PartialEq, Clone, Debug)] #[table_name = "notes"] -pub struct RemoteNoteInput{ +pub struct RemoteNoteInput { pub user_id: i32, pub content: String, pub in_reply_to: Option<i32>, pub neighborhood: bool, pub is_remote: bool, - pub remote_creator: String, - pub remote_url: String, + pub remote_creator: String, + pub remote_url: String, pub remote_id: String, } @@ -80,13 +81,15 @@ pub fn get_reply(note_text: &str) -> Option<i32> { let re = Regex::new(r"\B(📝|>>)(\d+)").unwrap(); match re.captures(note_text) { Some(t) => t.get(2).unwrap().as_str().parse().ok(), - None => None - } + None => None, + } } pub fn get_mentions(note_text: &str) -> Vec<String> { let re = Regex::new(r"\B(@)(\w+)").unwrap(); - re.captures_iter(note_text).map(|c| String::from(&c[2])).collect() + re.captures_iter(note_text) + .map(|c| String::from(&c[2])) + .collect() } /// used for user-input @@ -107,16 +110,19 @@ pub fn parse_note_text(text: &str) -> String { ) .unwrap(); let replace_str = "<a href=\"$0\">$0</a>"; - let urls_parsed = re.replace_all(&html_clean, &replace_str as &str).to_string(); - let note_regex = Regex::new( - r"\B(📝|&gt;&gt;)(\d+)", - ).unwrap(); - let replace_str = "<a href=\"/note/$2\">$0</a>"; - let notes_parsed = note_regex.replace_all(&urls_parsed, &replace_str as &str).to_string(); - let person_regex = Regex::new( - r"\B(@)(\w+)").unwrap(); - let replace_str = "<a href=\"/user/$2\">$0</a>"; - let people_parsed = person_regex.replace_all(&notes_parsed, &replace_str as &str).to_string(); + let urls_parsed = re + .replace_all(&html_clean, &replace_str as &str) + .to_string(); + let note_regex = Regex::new(r"\B(📝|&gt;&gt;)(\d+)").unwrap(); + let replace_str = "<a href=\"/note/$2\">$0</a>"; + let notes_parsed = note_regex + .replace_all(&urls_parsed, &replace_str as &str) + .to_string(); + let person_regex = Regex::new(r"\B(@)(\w+)").unwrap(); + let replace_str = "<a href=\"/user/$2\">$0</a>"; + let people_parsed = person_regex + .replace_all(&notes_parsed, &replace_str as &str) + .to_string(); // TODO get mentions too return people_parsed; } @@ -143,9 +149,11 @@ mod tests { } #[test] - fn test_string_with_http_urls() { // TODO fix test + fn test_string_with_http_urls() { + // TODO fix test let src = "Check this out: https://doc.rust-lang.org"; - let linked = "Check this out: <a href=\"https://doc.rust-lang.org\">https://doc.rust-lang.org</a>"; + let linked = + "Check this out: <a href=\"https://doc.rust-lang.org\">https://doc.rust-lang.org</a>"; assert!(parse_note_text(src) == linked) } @@ -168,7 +176,8 @@ mod tests { #[test] fn test_note_replace() { let src = "📝123 cool post >>456"; - let linked = "<a href=\"/note/123\">📝123</a> cool post <a href=\"/note/456\">&gt;&gt;456</a>"; + let linked = + "<a href=\"/note/123\">📝123</a> cool post <a href=\"/note/456\">&gt;&gt;456</a>"; assert!(parse_note_text(src) == linked) } diff --git a/src/db/notification.rs b/src/db/notification.rs @@ -1,35 +1,36 @@ -use serde::{de::Error, Deserialize, Serialize, Deserializer}; -use super::schema::{notifications, notification_viewers}; +use super::schema::{notification_viewers, notifications}; +use serde::{de::Error, Deserialize, Deserializer, Serialize}; #[derive(Queryable, Clone, Deserialize, Serialize)] -pub struct Notification { // rename RenderedNote - pub id: i32, - pub notification_html: String, - pub server_message: bool, // messages sent to everyone. maybe not necc - pub created_time: String, +pub struct Notification { + // rename RenderedNote + pub id: i32, + pub notification_html: String, + pub server_message: bool, // messages sent to everyone. maybe not necc + pub created_time: String, } #[derive(Queryable, Clone, Deserialize, Serialize)] -pub struct NotificationViewer { // rename RenderedNote +pub struct NotificationViewer { + // rename RenderedNote pub notification_id: i32, pub user_id: i32, pub viewed: bool, } - #[derive(Insertable)] -#[table_name="notifications"] -pub struct NewNotification { // rename RenderedNote - pub notification_html: String, - pub server_message: bool, // messages sent to everyone. maybe not necc +#[table_name = "notifications"] +pub struct NewNotification { + // rename RenderedNote + pub notification_html: String, + pub server_message: bool, // messages sent to everyone. maybe not necc } #[derive(Insertable)] -#[table_name="notification_viewers"] -pub struct NewNotificationViewer { // rename RenderedNote +#[table_name = "notification_viewers"] +pub struct NewNotificationViewer { + // rename RenderedNote pub notification_id: i32, pub user_id: i32, pub viewed: bool, } - - diff --git a/src/db/schema.rs b/src/db/schema.rs @@ -58,7 +58,7 @@ table! { } } -table! { +table! { server_mutuals (id) { id -> Integer, inbox_url -> Varchar, diff --git a/src/db/user.rs b/src/db/user.rs @@ -1,9 +1,9 @@ -use diesel::sqlite::SqliteConnection; -use std::env; use super::schema::users; -use diesel::prelude::*; -use serde::{Deserialize}; use bcrypt; +use diesel::prelude::*; +use diesel::sqlite::SqliteConnection; +use serde::Deserialize; +use std::env; #[derive(Debug, Clone, Default, Queryable, Deserialize)] pub struct RegistrationKey { @@ -20,25 +20,23 @@ impl RegistrationKey { .ok(); match key { Some(_) => true, - None => false + None => false, } } pub fn clear_key(conn: &SqliteConnection, key: &str) { use crate::db::schema::registration_keys::dsl::*; - diesel::delete( - registration_keys - .filter(value.eq(key))) - .execute(conn).ok(); + diesel::delete(registration_keys.filter(value.eq(key))) + .execute(conn) + .ok(); } } - // a hack #[derive(QueryableByName)] #[table_name = "users"] -pub struct Username{ - pub username: String +pub struct Username { + pub username: String, } #[derive(Debug, Clone, Default, Queryable, QueryableByName, Deserialize)] @@ -52,24 +50,21 @@ pub struct User { pub password: Option<String>, pub admin: bool, pub remote_url: Option<String>, -} +} impl User { pub fn get_url(&self) -> String { - format!("{}/user/{}", env::var("GOURAMI_DOMAIN").unwrap(), self.username) + format!( + "{}/user/{}", + env::var("GOURAMI_DOMAIN").unwrap(), + self.username + ) // remote url? } - pub fn authenticate( - conn: &SqliteConnection, - user: &str, - pass: &str, - ) -> Option<Self> { + pub fn authenticate(conn: &SqliteConnection, user: &str, pass: &str) -> Option<Self> { use crate::db::schema::users::dsl::*; // TODO -- allow email login as well - let user = match users - .filter(username.eq(user)) - .first::<Self>(conn) - { + let user = match users.filter(username.eq(user)).first::<Self>(conn) { Ok(user) => user, Err(e) => { error!("Failed to load hash for {:?}: {:?}", user, e); @@ -79,7 +74,7 @@ impl User { let u_pass = match &user.password { Some(p) => p, - None => return None + None => return None, }; match bcrypt::verify(&pass, &u_pass) { @@ -94,7 +89,7 @@ impl User { } #[derive(Insertable, Deserialize)] -#[table_name="users"] +#[table_name = "users"] pub struct NewUser<'a> { pub username: &'a str, pub password: &'a str, @@ -102,7 +97,7 @@ pub struct NewUser<'a> { } #[derive(Insertable, Deserialize)] -#[table_name="users"] +#[table_name = "users"] pub struct NewRemoteUser { pub username: String, pub remote_url: Option<String>, @@ -111,11 +106,8 @@ pub struct NewRemoteUser { // } // impl validate -fn validate_username() { -} +fn validate_username() {} -fn validate_password() { -} +fn validate_password() {} -fn validate_email() { -} +fn validate_email() {} diff --git a/src/lib.rs b/src/lib.rs @@ -1,48 +1,49 @@ #[macro_use] extern crate diesel; -#[macro_use] extern crate log; -#[macro_use] extern crate lazy_static; -#[macro_use] extern crate maplit; +#[macro_use] +extern crate log; +#[macro_use] +extern crate lazy_static; +#[macro_use] +extern crate maplit; -use serde_json::{Value}; +use serde_json::Value; use std::convert::Infallible; use zxcvbn::zxcvbn; -use warp::{reject::Reject, Reply, Filter, Rejection}; -use warp::{redirect::redirect}; use warp::filters::path::FullPath; use warp::http; use warp::hyper::Body; +use warp::redirect::redirect; +use warp::{reject::Reject, Filter, Rejection, Reply}; -use hyper; use askama::Template; -use db::note::{NoteInput, Note}; use db::conn::POOL; use db::note; -use db::user::{RegistrationKey, User, NewUser, Username}; +use db::note::{Note, NoteInput}; use db::notification::{NewNotification, NewNotificationViewer, Notification, NotificationViewer}; -use diesel::prelude::*; +use db::user::{NewUser, RegistrationKey, User, Username}; use diesel::insert_into; -use serde::{Deserialize}; -use session::{Session}; - +use diesel::prelude::*; +use hyper; +use serde::Deserialize; +use session::Session; -mod db; -mod session; mod ap; +mod db; pub mod routes; - +mod session; #[derive(Template)] -#[template(path = "user.html")] -struct UserTemplate<'a>{ +#[template(path = "user.html")] +struct UserTemplate<'a> { global: Global<'a>, notes: Vec<UserNote>, user: User, -} +} #[derive(Template)] -#[template(path = "note.html")] +#[template(path = "note.html")] struct NoteTemplate<'a> { global: Global<'a>, note_thread: Vec<UserNote>, @@ -50,51 +51,51 @@ struct NoteTemplate<'a> { } #[derive(Template)] -#[template(path = "error.html")] +#[template(path = "error.html")] struct ErrorTemplate<'a> { global: Global<'a>, - error_message: &'a str + error_message: &'a str, } #[derive(Template)] -#[template(path = "edit_user.html")] +#[template(path = "edit_user.html")] struct UserEditTemplate<'a> { global: Global<'a>, user: User, } #[derive(Template)] -#[template(path = "neighborhood.html")] -struct NeighborhoodTemplate<'a>{ +#[template(path = "neighborhood.html")] +struct NeighborhoodTemplate<'a> { global: Global<'a>, notes: Vec<UserNote>, } // TODO reconsider structure // TODO split into separate templates. not sure how #[derive(Template)] -#[template(path = "timeline.html")] -struct TimelineTemplate<'a>{ +#[template(path = "timeline.html")] +struct TimelineTemplate<'a> { global: Global<'a>, notes: Vec<UserNote>, -} +} #[derive(Template)] -#[template(path = "notifications.html")] -struct NotificationTemplate<'a>{ +#[template(path = "notifications.html")] +struct NotificationTemplate<'a> { notifs: Vec<RenderedNotif>, // required for redirects. global: Global<'a>, -} +} #[derive(Template)] -#[template(path = "login.html")] -struct LoginTemplate<'a>{ +#[template(path = "login.html")] +struct LoginTemplate<'a> { login_failed: bool, // required for redirects. global: Global<'a>, -} +} #[derive(Template)] -#[template(path = "register.html")] -struct RegisterTemplate<'a>{ +#[template(path = "register.html")] +struct RegisterTemplate<'a> { keyed: bool, key: &'a str, global: Global<'a>, @@ -106,7 +107,8 @@ struct ServerInfoTemplate<'a> { global: Global<'a>, } -struct Global<'a> { // variables used on all pages w header +struct Global<'a> { + // variables used on all pages w header title: &'a str, page: &'a str, page_title: &'a str, @@ -119,26 +121,28 @@ impl<'a> Global<'a> { fn create(user: Option<User>, page: &'a str) -> Self { use db::schema::notification_viewers::dsl::*; use diesel::dsl::count; - match user { - Some(u) => { - let conn = POOL.get().unwrap(); - let unread: i64 = notification_viewers - .select(count(user_id)) - .filter(user_id.eq(u.id)) - .filter(viewed.eq(false)) - .first(&conn).unwrap(); - Self { - me: u, - page: page, // remove leading slash - page_title: &page[1..], // remove leading slash - logged_in: true, - unread_notifications: unread, - ..Default::default() - }}, - None => Self { - page: page, - ..Default::default() - } + match user { + Some(u) => { + let conn = POOL.get().unwrap(); + let unread: i64 = notification_viewers + .select(count(user_id)) + .filter(user_id.eq(u.id)) + .filter(viewed.eq(false)) + .first(&conn) + .unwrap(); + Self { + me: u, + page: page, // remove leading slash + page_title: &page[1..], // remove leading slash + logged_in: true, + unread_notifications: unread, + ..Default::default() + } + } + None => Self { + page: page, + ..Default::default() + }, } } } @@ -172,10 +176,10 @@ pub fn render_template<T: askama::Template>(t: &T) -> http::Response<hyper::body #[derive(Deserialize)] struct DeleteNoteRequest { note_id: i32, // has to be String - redirect_url: String + redirect_url: String, } -fn delete_note(note_id: i32)-> Result<(), Box<dyn std::error::Error>> { +fn delete_note(note_id: i32) -> Result<(), Box<dyn std::error::Error>> { use db::schema::notes::dsl::*; diesel::delete(notes.filter(id.eq(note_id))).execute(&POOL.get()?)?; Ok(()) @@ -190,23 +194,29 @@ struct NewNoteRequest { async fn handle_new_note_form(u: Option<User>, f: NewNoteRequest) -> Result<impl Reply, Rejection> { match u { - Some(u) => { - let n = new_note(&u, &f.note_input, f.neighborhood.is_some()).unwrap(); - if n.neighborhood { - let nj = ap::new_note_to_ap_message(&n, &u); - let destinations = ap::get_destinations(); - ap::send_ap_message(&nj, destinations).await.unwrap(); // TODO error handling + Some(u) => { + let n = new_note(&u, &f.note_input, f.neighborhood.is_some()).unwrap(); + if n.neighborhood { + let nj = ap::new_note_to_ap_message(&n, &u); + let destinations = ap::get_destinations(); + ap::send_ap_message(&nj, destinations).await.unwrap(); // TODO error handling + } + let red_url: http::Uri = f.redirect_url.parse().unwrap(); + Ok(redirect(red_url)) } - let red_url: http::Uri = f.redirect_url.parse().unwrap(); - Ok(redirect(red_url))}, - None => Ok(redirect(http::Uri::from_static("error")))} + None => Ok(redirect(http::Uri::from_static("error"))), + } } -pub fn new_note(auth_user: &User, note_input: &str, neighborhood: bool) -> Result<NoteInput, Box<dyn std::error::Error>> { +pub fn new_note( + auth_user: &User, + note_input: &str, + neighborhood: bool, +) -> Result<NoteInput, Box<dyn std::error::Error>> { use db::schema::notes::dsl as notes; - use db::schema::users::dsl as u; - use db::schema::notifications::dsl as notifs; use db::schema::notification_viewers::dsl as nv; + use db::schema::notifications::dsl as notifs; + use db::schema::users::dsl as u; // create activitypub activity object // TODO -- micropub? // if its in reply to something @@ -214,27 +224,33 @@ pub fn new_note(auth_user: &User, note_input: &str, neighborhood: bool) -> Resul let reply = note::get_reply(note_input); let mentions = note::get_mentions(note_input); let parsed_note_text = note::parse_note_text(note_input); - let new_note = NoteInput{ + let new_note = NoteInput { user_id: auth_user.id, in_reply_to: reply, content: parsed_note_text, - neighborhood: neighborhood + neighborhood: neighborhood, }; // TODO fix potential multithreading issue insert_into(notes::notes).values(&new_note).execute(conn)?; let note_id: i32 = notes::notes .order(notes::id.desc()) .select(notes::id) - .first(conn).unwrap(); + .first(conn) + .unwrap(); // notify person u reply to if mentions.len() > 0 { - let message = format!("@{} mentioned you in a note 📝{}", auth_user.username, note_id); + let message = format!( + "@{} mentioned you in a note 📝{}", + auth_user.username, note_id + ); let new_notification = NewNotification { - // reusing the same parser for now. rename maybe + // reusing the same parser for now. rename maybe notification_html: note::parse_note_text(&message), - server_message: false + server_message: false, }; - insert_into(notifs::notifications).values(new_notification).execute(conn)?; + insert_into(notifs::notifications) + .values(new_notification) + .execute(conn)?; for mention in mentions { // create reply notification // I thinks this may work but worry about multithreading @@ -242,21 +258,24 @@ pub fn new_note(auth_user: &User, note_input: &str, neighborhood: bool) -> Resul .select(u::id) .filter(u::username.eq(mention)) .first(conn) - .ok(); // TODO + .ok(); // TODO if let Some(u_id) = user_id { let notif_id = notifs::notifications .order(notifs::id.desc()) .select(notifs::id) - .first(conn).unwrap(); + .first(conn) + .unwrap(); let new_nv = NewNotificationViewer { - notification_id: notif_id, - user_id: u_id, - viewed: false - }; - insert_into(nv::notification_viewers).values(new_nv).execute(conn).ok(); // work with conn failures + notification_id: notif_id, + user_id: u_id, + viewed: false, + }; + insert_into(nv::notification_viewers) + .values(new_nv) + .execute(conn) + .ok(); // work with conn failures } } - } // generate activitypub object from post request // send to outbox @@ -277,14 +296,20 @@ fn register_page(query_params: QueryParams) -> impl warp::Reply { if let Some(k) = query_params.key { key_str = k.as_str(); keyed = RegistrationKey::is_valid(&POOL.get().unwrap(), &key_str); - render_template(&RegisterTemplate{keyed: keyed, key: key_str, global: global}) - } - else { - render_template(&RegisterTemplate{keyed: keyed, key: key_str, global: global}) + render_template(&RegisterTemplate { + keyed: keyed, + key: key_str, + global: global, + }) + } else { + render_template(&RegisterTemplate { + keyed: keyed, + key: key_str, + global: global, + }) } } - #[derive(Deserialize)] struct RegisterForm { username: String, @@ -292,11 +317,10 @@ struct RegisterForm { email: String, } - impl RegisterForm { fn validate(&self) -> Result<(), &'static str> { if zxcvbn(&self.password, &[]).unwrap().score() < 1 { - return Err("Please come up with a more secure password.") + return Err("Please come up with a more secure password."); } Ok(()) } @@ -306,8 +330,12 @@ impl RegisterForm { fn do_register(form: RegisterForm, query_params: serde_json::Value) -> impl Reply { let conn = &POOL.get().unwrap(); use db::schema::users::dsl::*; - if form.validate().is_err(){ // TODO catch better - return do_login(LoginForm{username: form.username, password: form.password}) + if form.validate().is_err() { + // TODO catch better + return do_login(LoginForm { + username: form.username, + password: form.password, + }); } if let Some(k) = query_params.get("key") { let k_string = &k.as_str().unwrap(); @@ -315,17 +343,22 @@ fn do_register(form: RegisterForm, query_params: serde_json::Value) -> impl Repl RegistrationKey::clear_key(conn, k_string); if keyed { let hash = bcrypt::hash(&form.password, bcrypt::DEFAULT_COST).unwrap(); - let new_user = NewUser {username: &form.username, password: &hash, email: &form.email}; + let new_user = NewUser { + username: &form.username, + password: &hash, + email: &form.email, + }; // todo data validation - insert_into(users) - .values(new_user) - .execute(conn).unwrap(); + insert_into(users).values(new_user).execute(conn).unwrap(); - // insert into database + // insert into database } } // not good - do_login(LoginForm{username: form.username, password: form.password}) + do_login(LoginForm { + username: form.username, + password: form.password, + }) } #[derive(Deserialize)] @@ -334,31 +367,50 @@ struct LoginForm { password: String, } - fn login_page() -> impl Reply { // dont let you access this page if logged in - render_template(&LoginTemplate{login_failed: false, global: Global{page: "login", ..Default::default()}}) + render_template(&LoginTemplate { + login_failed: false, + global: Global { + page: "login", + ..Default::default() + }, + }) } fn do_login(form: LoginForm) -> impl Reply { - if let Some(cookie) = Session::authenticate(&POOL.get().unwrap(), &form.username, &form.password) { + if let Some(cookie) = + Session::authenticate(&POOL.get().unwrap(), &form.username, &form.password) + { // 1 year cookie expiration http::Response::builder() .status(http::StatusCode::FOUND) .header(http::header::LOCATION, "/") .header( http::header::SET_COOKIE, - format!("EXAUTH={}; MAX-AGE=31536000; SameSite=Strict; HttpOpnly", cookie), - ) - .body(Body::empty()).unwrap() + format!( + "EXAUTH={}; MAX-AGE=31536000; SameSite=Strict; HttpOpnly", + cookie + ), + ) + .body(Body::empty()) + .unwrap() } else { - render_template(&LoginTemplate{login_failed: true, global:Global{page: "login", ..Default::default()}}) + render_template(&LoginTemplate { + login_failed: true, + global: Global { + page: "login", + ..Default::default() + }, + }) } } fn do_logout(cookie: String) -> impl Reply { use db::schema::sessions::dsl::*; - diesel::delete(sessions.filter(cookie.eq(cookie))).execute(&POOL.get().unwrap()).unwrap(); + diesel::delete(sessions.filter(cookie.eq(cookie))) + .execute(&POOL.get().unwrap()) + .unwrap(); redirect(warp::http::Uri::from_static("/")) } @@ -391,28 +443,47 @@ fn get_single_note(note_id: i32) -> Option<Vec<UserNote>> { let conn = &POOL.get().unwrap(); // TODO -- it isnt serializing ids right // Username is a hack because there are two ID columns and it doesnt work right - let results: Vec<(Note, Username)> = diesel::sql_query(format!(r"with recursive tc( p ) + let results: Vec<(Note, Username)> = diesel::sql_query(format!( + r"with recursive tc( p ) as ( values({}) union select id from notes, tc where notes.in_reply_to = tc.p ) select notes.*, users.username from notes join users on notes.user_id = users.id - where notes.id in tc", note_id)).load(conn).unwrap(); - Some(results.into_iter().map(|mut a| { - // the ids are swapped for some reason - UserNote{note: a.0, username: a.1.username}}).collect()) + where notes.id in tc", + note_id + )) + .load(conn) + .unwrap(); + Some( + results + .into_iter() + .map(|mut a| { + // the ids are swapped for some reason + UserNote { + note: a.0, + username: a.1.username, + } + }) + .collect(), + ) } /// We have to do a join here -fn get_notes(params: GetPostsParams, neighborhood: Option<bool>) -> Result<Vec<UserNote>, diesel::result::Error> { +fn get_notes( + params: GetPostsParams, + neighborhood: Option<bool>, +) -> Result<Vec<UserNote>, diesel::result::Error> { use db::schema::notes::dsl as n; use db::schema::users::dsl as u; const PAGE_SIZE: i64 = 250; - let mut query = n::notes.inner_join(u::users) + let mut query = n::notes + .inner_join(u::users) .order(n::id.desc()) .limit(PAGE_SIZE) - .offset((params.page_num - 1) * PAGE_SIZE).into_boxed(); + .offset((params.page_num - 1) * PAGE_SIZE) + .into_boxed(); if let Some(u_id) = params.user_id { query = query.filter(u::id.eq(u_id)); } @@ -423,90 +494,133 @@ fn get_notes(params: GetPostsParams, neighborhood: Option<bool>) -> Result<Vec<U None => (), } let results = query.load::<(Note, User)>(&POOL.get().unwrap()).unwrap(); // TODO get rid of unwrap - Ok(results.into_iter().map(|a| UserNote{note: a.0, username: a.1.username}).collect()) + Ok(results + .into_iter() + .map(|a| UserNote { + note: a.0, + username: a.1.username, + }) + .collect()) } struct RenderedNotif { notif: Notification, - viewed: bool + viewed: bool, } fn render_notifications(auth_user: Option<User>) -> impl Reply { - use db::schema::notifications::dsl as n; use db::schema::notification_viewers::dsl as nv; + use db::schema::notifications::dsl as n; let global = Global::create(auth_user.clone(), "/notifications"); let conn = &POOL.get().unwrap(); let my_id = auth_user.unwrap().id; - let notifs = n::notifications.inner_join(nv::notification_viewers) + let notifs = n::notifications + .inner_join(nv::notification_viewers) .order(n::id.desc()) .filter(nv::user_id.eq(my_id)) .limit(1000) // arbitrary TODO cleanup / paginate - .load::<(Notification, NotificationViewer)>(conn).unwrap() + .load::<(Notification, NotificationViewer)>(conn) + .unwrap() .into_iter() - .map(|(n, nv)| RenderedNotif{notif: n, viewed: nv.viewed}).collect(); + .map(|(n, nv)| RenderedNotif { + notif: n, + viewed: nv.viewed, + }) + .collect(); // mark notifications as read diesel::update( nv::notification_viewers - .filter(nv::user_id.eq(my_id)) - .filter(nv::viewed.eq(false))) - .set(nv::viewed.eq(true) - ).execute(conn).unwrap(); - render_template(&NotificationTemplate{global: global, notifs: notifs}) -} - -fn render_timeline(auth_user: Option<User>, params:GetPostsParams, url_path: FullPath) -> impl Reply { + .filter(nv::user_id.eq(my_id)) + .filter(nv::viewed.eq(false)), + ) + .set(nv::viewed.eq(true)) + .execute(conn) + .unwrap(); + render_template(&NotificationTemplate { + global: global, + notifs: notifs, + }) +} + +fn render_timeline( + auth_user: Option<User>, + params: GetPostsParams, + url_path: FullPath, +) -> impl Reply { // no session -- anonymous // pulls a bunch of data i dont really need let header = Global::create(auth_user, url_path.as_str()); // TODO -- ignore neighborhood replies let notes = get_notes(params, Some(false)); match notes { - Ok(n) => render_template(&TimelineTemplate{ + Ok(n) => render_template(&TimelineTemplate { global: header, notes: n, }), - _ => render_template(&ErrorTemplate{global: header, error_message: "Could not fetch notes", ..Default::default()}) + _ => render_template(&ErrorTemplate { + global: header, + error_message: "Could not fetch notes", + ..Default::default() + }), } - } -fn render_neighborhood(auth_user: Option<User>, params:GetPostsParams, url_path: FullPath) -> impl Reply{ +fn render_neighborhood( + auth_user: Option<User>, + params: GetPostsParams, + url_path: FullPath, +) -> impl Reply { let header = Global::create(auth_user, url_path.as_str()); let notes = get_notes(params, Some(true)); match notes { - Ok(n) => render_template(&TimelineTemplate{ - global: header, - notes: n, + Ok(n) => render_template(&TimelineTemplate { + global: header, + notes: n, + }), + _ => render_template(&ErrorTemplate { + global: header, + error_message: "Could not fetch notes", + ..Default::default() }), - _ => render_template(&ErrorTemplate{global: header, error_message: "Could not fetch notes", ..Default::default()}) } - } -impl<'a> Default for ErrorTemplate<'a> { +impl<'a> Default for ErrorTemplate<'a> { fn default() -> Self { Self { global: Global::default(), - error_message: "An error occured. Please report to site admin." + error_message: "An error occured. Please report to site admin.", } } } fn server_info_page(auth_user: Option<User>) -> impl Reply { - render_template(&ServerInfoTemplate{global: Global::create(auth_user, "/server")}) + render_template(&ServerInfoTemplate { + global: Global::create(auth_user, "/server"), + }) } fn note_page(auth_user: Option<User>, note_id: i32, path: FullPath) -> impl Reply { let note_thread = get_single_note(note_id); - if let Some(n) = note_thread { - render_template(&NoteTemplate{global: Global::create(auth_user, path.as_str()), note_thread: n}) - } - else { - render_template(&ErrorTemplate{global: Global::create(auth_user, path.as_str()), error_message: "Note not found"}) + if let Some(n) = note_thread { + render_template(&NoteTemplate { + global: Global::create(auth_user, path.as_str()), + note_thread: n, + }) + } else { + render_template(&ErrorTemplate { + global: Global::create(auth_user, path.as_str()), + error_message: "Note not found", + }) } } -fn user_page(auth_user: Option<User>, user_name: String, mut params: GetPostsParams, path: FullPath) -> impl Reply { - let header = Global::create(auth_user, path.as_str()); // maybe if i'm clever i can abstract this away +fn user_page( + auth_user: Option<User>, + user_name: String, + mut params: GetPostsParams, + path: FullPath, +) -> impl Reply { + let header = Global::create(auth_user, path.as_str()); // maybe if i'm clever i can abstract this away use db::schema::users::dsl::*; let conn = &POOL.get().unwrap(); let user: Option<User> = users @@ -516,26 +630,34 @@ fn user_page(auth_user: Option<User>, user_name: String, mut params: GetPostsPar if let Some(u) = user { params.user_id = Some(u.id); let notes = get_notes(params, None).unwrap(); - render_template(&UserTemplate{ + render_template(&UserTemplate { global: header, user: u.clone(), // TODO stop cloning - notes: notes + notes: notes, + }) + } else { + render_template(&ErrorTemplate { + global: header, + error_message: "User not found", + ..Default::default() }) - } - else { - render_template(&ErrorTemplate{global: header, error_message: "User not found", ..Default::default()}) } } - fn render_user_edit_page(user: Option<User>, user_name: String) -> impl Reply { let u = user.clone().unwrap(); let global = Global::create(user, "/edit"); if u.username == user_name || u.admin { - render_template(&UserEditTemplate{global: global, user: u}) - } - else { - render_template(&ErrorTemplate{global: global, error_message: "You don't have permission to edit this page", ..Default::default()}) + render_template(&UserEditTemplate { + global: global, + user: u, + }) + } else { + render_template(&ErrorTemplate { + global: global, + error_message: "You don't have permission to edit this page", + ..Default::default() + }) } } @@ -568,16 +690,19 @@ fn edit_user(user: Option<User>, user_name: String, f: EditForm) -> impl Reply { let conn = &POOL.get().unwrap(); if u.username == user_name || u.admin { use db::schema::users::dsl::*; - diesel::update( - users - .find(u.id)) + diesel::update(users.find(u.id)) .set(bio.eq(&f.bio.unwrap_or(String::new()))) - .execute(conn).unwrap(); + .execute(conn) + .unwrap(); } let red: http::Uri = f.redirect_url.parse().unwrap(); redirect(red) } async fn handle_rejection(err: Rejection) -> Result<impl Reply, Infallible> { - Ok(render_template(&ErrorTemplate{global: Global::create(None, "error"), error_message: "You do not have access to this page, it does not exist, or something went wrong."})) + Ok(render_template(&ErrorTemplate { + global: Global::create(None, "error"), + error_message: + "You do not have access to this page, it does not exist, or something went wrong.", + })) } diff --git a/src/main.rs b/src/main.rs @@ -7,18 +7,12 @@ async fn main() { .version("0.0.0") .author("Alex Wennerberg <alex@alexwennerberg.com>") .about("Gourami server and admin tools") - .subcommand( - App::new("run") - .about("Run server") - ) - .subcommand( - App::new("admin") - .about("Admin Tools") - ).get_matches(); + .subcommand(App::new("run").about("Run server")) + .subcommand(App::new("admin").about("Admin Tools")) + .get_matches(); if let Some(m) = matches.subcommand_matches("run") { run_server().await; - } - else if let Some(m) = matches.subcommand_matches("admin") { + } else if let Some(m) = matches.subcommand_matches("admin") { // write admin commands here // reset password // follow remote diff --git a/src/routes.rs b/src/routes.rs @@ -1,7 +1,7 @@ -use crate::*; use crate::session; +use crate::*; use env_logger; -use warp::{filters::cookie, path, body::json, body::form, filters::query::query}; +use warp::{body::form, body::json, filters::cookie, filters::query::query, path}; // I had trouble decoupling routes from server -- couldnt figure out the return type pub async fn run_server() { @@ -30,8 +30,8 @@ pub async fn run_server() { .and(path::full()) .map(user_page); - let user_edit_page = private_session_filter() - .and(path!("user" / String / "edit" )) + let user_edit_page = private_session_filter() + .and(path!("user" / String / "edit")) .map(render_user_edit_page); let edit_user = private_session_filter() @@ -48,31 +48,19 @@ pub async fn run_server() { .and(path("notifications")) .map(render_notifications); - let server_info_page = session_filter() - .and(path("server")) - .map(server_info_page); + let server_info_page = session_filter().and(path("server")).map(server_info_page); // auth functions - let register_page = path("register") - .and(query()) - .map(register_page); + let register_page = path("register").and(query()).map(register_page); - let do_register = path("register") - .and(form()) - .and(query()) - .map(do_register); + let do_register = path("register").and(form()).and(query()).map(do_register); - let login_page = path("login") - .map(|| login_page()); + let login_page = path("login").map(|| login_page()); // TODO redirect these login pages - let do_login = path("login") - .and(form()) - .map(do_login); + let do_login = path("login").and(form()).map(do_login); - let do_logout = path("logout") - .and(cookie::cookie("EXAUTH")) - .map(do_logout); + let do_logout = path("logout").and(cookie::cookie("EXAUTH")).map(do_logout); // CRUD actions let create_note = path("create_note") @@ -81,70 +69,77 @@ pub async fn run_server() { // Verbose -- see if you can refactor .and_then(handle_new_note_form); - let delete_note = path("delete_note") - .and(session_filter()) - .and(form()) - .map(|u: Option<User>, f: DeleteNoteRequest | match u { + let delete_note = path("delete_note").and(session_filter()).and(form()).map( + |u: Option<User>, f: DeleteNoteRequest| match u { Some(u) => { delete_note(f.note_id).unwrap(); // TODO fix unwrap let red_url: http::Uri = f.redirect_url.parse().unwrap(); - redirect(red_url)}, - None => redirect(http::Uri::from_static("error"))}); - + redirect(red_url) + } + None => redirect(http::Uri::from_static("error")), + }, + ); - let static_files = warp::path("static") - .and(warp::fs::dir("./static")); + let static_files = warp::path("static").and(warp::fs::dir("./static")); // activityPub stuff // This stuff should filter based on the application headers // setup authentication // POST // TODO -- setup proper replies - let post_server_inbox = path!("inbox" ) - .and(json()) - .map(post_inbox); + let post_server_inbox = path!("inbox").and(json()).map(post_inbox); - let post_server_inbox = path!("inbox" ) - .and(json()) - .map(post_inbox); + let post_server_inbox = path!("inbox").and(json()).map(post_inbox); - let get_server_outbox = path!("outbox" ) - .map(get_outbox); + let get_server_outbox = path!("outbox").map(get_outbox); // https://github.com/seanmonstar/warp/issues/42 -- how to set up diesel - // TODO set content length limit + // TODO set content length limit // TODO redirect via redirect in request // TODO secure against xss - // used for api based authentication + // used for api based authentication // let api_filter = session::create_session_filter(&POOL.get()); - let html_renders = home.or(login_page).or(register_page).or(user_page).or(note_page).or(server_info_page).or(notification_page).or(user_edit_page).or(neighborhood); - let forms = do_register.or(do_login).or(do_logout).or(create_note).or(delete_note).or(edit_user); + let html_renders = home + .or(login_page) + .or(register_page) + .or(user_page) + .or(note_page) + .or(server_info_page) + .or(notification_page) + .or(user_edit_page) + .or(neighborhood); + let forms = do_register + .or(do_login) + .or(do_logout) + .or(create_note) + .or(delete_note) + .or(edit_user); let api_post = post_server_inbox; // let api // catch all for any other paths - let routes = warp::get().and(html_renders) - .or( - warp::post() + let routes = warp::get() + .and(html_renders) + .or(warp::post() .and(warp::body::content_length_limit(1024 * 32)) .and(forms)) - .or( - warp::post() + .or(warp::post() .and(warp::body::content_length_limit(1024 * 64)) - .and(api_post) - ) + .and(api_post)) .or(static_files) .with(warp::log("server")) .recover(handle_rejection) .boxed(); env_logger::init(); match std::env::var("GOURAMI_ENV").unwrap().as_str() { - "PROD" => warp::serve(routes) - .tls() - .cert_path(&std::env::var("CERT_PATH").unwrap()) - .key_path(&std::env::var("KEY_PATH").unwrap()) - .run(([0, 0, 0, 0], 443)) - .await , - _ => warp::serve(routes).run(([127,0,0,1], 3030)).await + "PROD" => { + warp::serve(routes) + .tls() + .cert_path(&std::env::var("CERT_PATH").unwrap()) + .key_path(&std::env::var("KEY_PATH").unwrap()) + .run(([0, 0, 0, 0], 443)) + .await + } + _ => warp::serve(routes).run(([127, 0, 0, 1], 3030)).await, } } diff --git a/src/session.rs b/src/session.rs @@ -1,13 +1,12 @@ use crate::*; use db::user::User; +use diesel::sqlite::SqliteConnection; use log::{debug, error}; +use rand::distributions::Alphanumeric; use rand::thread_rng; use rand::Rng; -use rand::distributions::Alphanumeric; -use diesel::sqlite::SqliteConnection; use warp::filters::{cookie, BoxedFilter}; - #[derive(Queryable)] pub struct Session { // dbpool maybe @@ -17,7 +16,7 @@ pub struct Session { created_time: String, } -// TODO -- figure out if database pooling is strictly necessary for security +// TODO -- figure out if database pooling is strictly necessary for security impl Session { /// Attempt to authenticate a user for this session. @@ -47,40 +46,37 @@ impl Session { if let Some(sessionkey) = sess { use db::schema::sessions::dsl as s; use db::schema::users::dsl as u; - let result = u::users - .inner_join(s::sessions) - .filter(s::cookie.eq(sessionkey)) - .first::<(User,Session)>(&POOL.get().unwrap()) - .ok(); + let result = u::users + .inner_join(s::sessions) + .filter(s::cookie.eq(sessionkey)) + .first::<(User, Session)>(&POOL.get().unwrap()) + .ok(); match result { Some(r) => Some(r.0), - None => None + None => None, } - } - else { + } else { // so we don't have to query db when key isnt present None } - } } pub fn create_session_filter(optional: bool) -> BoxedFilter<(Option<User>,)> { if optional { cookie::optional("EXAUTH") - .map(move |key: Option<String>| {Session::from_key(key)}) - .boxed() + .map(move |key: Option<String>| Session::from_key(key)) + .boxed() } else { cookie::cookie("EXAUTH") - .and_then(|key: String| async move { - let s = Session::from_key(Some(key)); - if s.is_none() { - Err(warp::reject::reject()) // todo -- add custom rejection - } - else { - Ok(Some(s.unwrap())) - } + .and_then(|key: String| async move { + let s = Session::from_key(Some(key)); + if s.is_none() { + Err(warp::reject::reject()) // todo -- add custom rejection + } else { + Ok(Some(s.unwrap())) + } }) - .boxed() + .boxed() } }