Complete error handling for Events

This commit is contained in:
Daniel Sockwell 2020-04-10 22:03:54 -04:00
parent 638364883f
commit b4488d14be
14 changed files with 57 additions and 146 deletions

2
Cargo.lock generated
View File

@ -453,7 +453,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "flodgatt" name = "flodgatt"
version = "0.8.0" version = "0.8.1"
dependencies = [ dependencies = [
"criterion 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "criterion 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"dotenv 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)", "dotenv 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)",

View File

@ -1,7 +1,7 @@
[package] [package]
name = "flodgatt" name = "flodgatt"
description = "A blazingly fast drop-in replacement for the Mastodon streaming api server" description = "A blazingly fast drop-in replacement for the Mastodon streaming api server"
version = "0.8.0" version = "0.8.1"
authors = ["Daniel Long Sockwell <daniel@codesections.com", "Julian Laubstein <contact@julianlaubstein.de>"] authors = ["Daniel Long Sockwell <daniel@codesections.com", "Julian Laubstein <contact@julianlaubstein.de>"]
edition = "2018" edition = "2018"

View File

@ -35,20 +35,9 @@ impl From<ReceiverErr> for FatalErr {
Self::ReceiverErr(e) Self::ReceiverErr(e)
} }
} }
pub fn die_with_msg2(msg: impl fmt::Display) {
eprintln!("{}", msg);
std::process::exit(1);
}
// TODO delete vvvv when postgres_cfg.rs has better error handling
pub fn die_with_msg(msg: impl fmt::Display) -> ! { pub fn die_with_msg(msg: impl fmt::Display) -> ! {
eprintln!("FATAL ERROR: {}", msg); eprintln!("FATAL ERROR: {}", msg);
std::process::exit(1); std::process::exit(1);
} }
#[macro_export]
macro_rules! log_fatal {
($str:expr, $var:expr) => {{
log::error!($str, $var);
panic!();
};};
}

View File

@ -2,10 +2,12 @@ use std::fmt;
#[derive(Debug)] #[derive(Debug)]
pub enum TimelineErr { pub enum TimelineErr {
RedisNamespaceMismatch, MissingHashtag,
InvalidInput, InvalidInput,
} }
impl std::error::Error for TimelineErr {}
impl From<std::num::ParseIntError> for TimelineErr { impl From<std::num::ParseIntError> for TimelineErr {
fn from(_error: std::num::ParseIntError) -> Self { fn from(_error: std::num::ParseIntError) -> Self {
Self::InvalidInput Self::InvalidInput
@ -16,8 +18,8 @@ impl fmt::Display for TimelineErr {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
use TimelineErr::*; use TimelineErr::*;
let msg = match self { let msg = match self {
RedisNamespaceMismatch => "TODO: Cut this error", InvalidInput => "The timeline text from Redis could not be parsed into a supported timeline. TODO: add incoming timeline text",
InvalidInput => "The timeline text from Redis could not be parsed into a supported timeline. TODO: add incoming timeline text" MissingHashtag => "Attempted to send a hashtag timeline without supplying a tag name",
}; };
write!(f, "{}", msg) write!(f, "{}", msg)
} }

View File

@ -66,6 +66,7 @@ fn main() -> Result<(), FatalErr> {
log::info!("Incoming websocket request for {:?}", subscription.timeline); log::info!("Incoming websocket request for {:?}", subscription.timeline);
{ {
let mut receiver = ws_receiver.lock().unwrap_or_else(Receiver::recover); let mut receiver = ws_receiver.lock().unwrap_or_else(Receiver::recover);
receiver.subscribe(&subscription).unwrap_or_else(|e| { receiver.subscribe(&subscription).unwrap_or_else(|e| {
log::error!("Could not subscribe to the Redis channel: {}", e) log::error!("Could not subscribe to the Redis channel: {}", e)
}); });

View File

@ -37,6 +37,12 @@ impl FromStr for Id {
} }
} }
impl fmt::Display for Id {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(f, "{}", self.0)
}
}
impl Serialize for Id { impl Serialize for Id {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where where

View File

@ -1,87 +0,0 @@
use crate::parse_client_request::Blocks;
use hashbrown::HashSet;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
pub struct DynamicEvent {
pub event: String,
pub payload: Value,
queued_at: Option<i64>,
}
impl DynamicEvent {
/// Returns `true` if the status is filtered out based on its language
pub fn language_not(&self, allowed_langs: &HashSet<String>) -> bool {
const ALLOW: bool = false;
const REJECT: bool = true;
if allowed_langs.is_empty() {
return ALLOW; // listing no allowed_langs results in allowing all languages
}
match self.payload["language"].as_str() {
Some(toot_language) if allowed_langs.contains(toot_language) => ALLOW,
None => ALLOW, // If toot language is unknown, toot is always allowed
Some(empty) if empty == String::new() => ALLOW,
Some(_toot_language) => REJECT,
}
}
/// Returns `true` if the toot contained in this Event originated from a blocked domain,
/// is from an account that has blocked the current user, or if the User's list of
/// blocked/muted users includes a user involved in the toot.
///
/// A user is involved in the toot if they:
/// * Are mentioned in this toot
/// * Wrote this toot
/// * Wrote a toot that this toot is replying to (if any)
/// * Wrote the toot that this toot is boosting (if any)
pub fn involves_any(&self, blocks: &Blocks) -> bool {
const ALLOW: bool = false;
const REJECT: bool = true;
let Blocks {
blocked_users,
blocking_users,
blocked_domains,
} = blocks;
let id = self.payload["account"]["id"].as_str().expect("TODO");
let username = self.payload["account"]["acct"].as_str().expect("TODO");
if self.involves(blocked_users) || blocking_users.contains(&id.parse().expect("TODO")) {
REJECT
} else {
let full_username = &username;
match full_username.split('@').nth(1) {
Some(originating_domain) if blocked_domains.contains(originating_domain) => REJECT,
Some(_) | None => ALLOW, // None means the local instance, which can't be blocked
}
}
}
// involved_users = mentioned_users + author + replied-to user + boosted user
fn involves(&self, blocked_users: &HashSet<i64>) -> bool {
// mentions
let mentions = self.payload["mentions"].as_array().expect("TODO");
let mut involved_users: HashSet<i64> = mentions
.iter()
.map(|mention| mention["id"].as_str().expect("TODO").parse().expect("TODO"))
.collect();
// author
let author_id = self.payload["account"]["id"].as_str().expect("TODO");
involved_users.insert(author_id.parse::<i64>().expect("TODO"));
// replied-to user
let replied_to_user = self.payload["in_reply_to_account_id"].as_str();
if let Some(user_id) = replied_to_user {
involved_users.insert(user_id.parse().expect("TODO"));
}
// boosted user
let id_of_boosted_user = self.payload["reblog"]["account"]["id"].as_str();
if let Some(user_id) = id_of_boosted_user {
involved_users.insert(user_id.parse().expect("TODO"));
}
!involved_users.is_disjoint(blocked_users)
}
}

View File

@ -21,6 +21,7 @@ pub enum EventKind {
Update(DynStatus), Update(DynStatus),
NonUpdate, NonUpdate,
} }
impl Default for EventKind { impl Default for EventKind {
fn default() -> Self { fn default() -> Self {
Self::NonUpdate Self::NonUpdate
@ -35,10 +36,9 @@ pub struct DynStatus {
pub mentioned_users: HashSet<Id>, pub mentioned_users: HashSet<Id>,
pub replied_to_user: Option<Id>, pub replied_to_user: Option<Id>,
pub boosted_user: Option<Id>, pub boosted_user: Option<Id>,
pub payload: Value,
} }
type Result<T> = std::result::Result<T, EventErr>; // TODO cut if not used more than once type Result<T> = std::result::Result<T, EventErr>;
impl DynEvent { impl DynEvent {
pub fn set_update(self) -> Result<Self> { pub fn set_update(self) -> Result<Self> {
@ -65,7 +65,6 @@ impl DynStatus {
mentioned_users: HashSet::new(), mentioned_users: HashSet::new(),
replied_to_user: Id::try_from(&payload["in_reply_to_account_id"]).ok(), replied_to_user: Id::try_from(&payload["in_reply_to_account_id"]).ok(),
boosted_user: Id::try_from(&payload["reblog"]["account"]["id"]).ok(), boosted_user: Id::try_from(&payload["reblog"]["account"]["id"]).ok(),
payload,
}) })
} }
/// Returns `true` if the status is filtered out based on its language /// Returns `true` if the status is filtered out based on its language

View File

@ -8,7 +8,6 @@ pub use {
err::EventErr, err::EventErr,
}; };
use crate::log_fatal;
use serde::Serialize; use serde::Serialize;
use std::{convert::TryFrom, string::String}; use std::{convert::TryFrom, string::String};
@ -26,8 +25,7 @@ impl Event {
Some(payload) => SendableEvent::WithPayload { event, payload }, Some(payload) => SendableEvent::WithPayload { event, payload },
None => SendableEvent::NoPayload { event }, None => SendableEvent::NoPayload { event },
}; };
serde_json::to_string(&sendable_event) serde_json::to_string(&sendable_event).expect("Guaranteed: SendableEvent is Serialize")
.unwrap_or_else(|_| log_fatal!("Could not serialize `{:?}`", &sendable_event))
} }
pub fn event_name(&self) -> String { pub fn event_name(&self) -> String {
@ -47,7 +45,7 @@ impl Event {
.. ..
}) => "update", }) => "update",
Self::Dynamic(DynEvent { event, .. }) => event, Self::Dynamic(DynEvent { event, .. }) => event,
Self::Ping => panic!("event_name() called on EventNotReady"), Self::Ping => panic!("event_name() called on Ping"),
}) })
} }
@ -65,7 +63,7 @@ impl Event {
FiltersChanged => None, FiltersChanged => None,
}, },
Self::Dynamic(DynEvent { payload, .. }) => Some(payload.to_string()), Self::Dynamic(DynEvent { payload, .. }) => Some(payload.to_string()),
Self::Ping => panic!("payload() called on EventNotReady"), Self::Ping => panic!("payload() called on Ping"),
} }
} }
} }
@ -105,6 +103,5 @@ enum SendableEvent<'a> {
} }
fn escaped<T: Serialize + std::fmt::Debug>(content: T) -> String { fn escaped<T: Serialize + std::fmt::Debug>(content: T) -> String {
serde_json::to_string(&content) serde_json::to_string(&content).expect("Guaranteed by Serialize trait bound")
.unwrap_or_else(|_| log_fatal!("Could not parse Event with: `{:?}`", &content))
} }

View File

@ -6,17 +6,15 @@
// #[cfg(not(test))] // #[cfg(not(test))]
use super::postgres::PgPool; use super::postgres::PgPool;
use super::query;
use super::query::Query; use super::query::Query;
use crate::err::TimelineErr; use crate::err::TimelineErr;
use crate::log_fatal;
use crate::messages::Id; use crate::messages::Id;
use hashbrown::HashSet; use hashbrown::HashSet;
use lru::LruCache; use lru::LruCache;
use uuid::Uuid; use warp::{filters::BoxedFilter, path, reject::Rejection, Filter};
use warp::reject::Rejection;
use super::query;
use warp::{filters::BoxedFilter, path, Filter};
/// Helper macro to match on the first of any of the provided filters /// Helper macro to match on the first of any of the provided filters
macro_rules! any_of { macro_rules! any_of {
@ -52,7 +50,6 @@ macro_rules! parse_sse_query {
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct Subscription { pub struct Subscription {
pub id: Uuid,
pub timeline: Timeline, pub timeline: Timeline,
pub allowed_langs: HashSet<String>, pub allowed_langs: HashSet<String>,
pub blocks: Blocks, pub blocks: Blocks,
@ -70,7 +67,6 @@ pub struct Blocks {
impl Default for Subscription { impl Default for Subscription {
fn default() -> Self { fn default() -> Self {
Self { Self {
id: Uuid::new_v4(),
timeline: Timeline(Stream::Unset, Reach::Local, Content::Notification), timeline: Timeline(Stream::Unset, Reach::Local, Content::Notification),
allowed_langs: HashSet::new(), allowed_langs: HashSet::new(),
blocks: Blocks::default(), blocks: Blocks::default(),
@ -134,7 +130,6 @@ impl Subscription {
}; };
Ok(Subscription { Ok(Subscription {
id: Uuid::new_v4(),
timeline, timeline,
allowed_langs: user.allowed_langs, allowed_langs: user.allowed_langs,
blocks: Blocks { blocks: Blocks {
@ -183,30 +178,28 @@ impl Timeline {
Self(Unset, Local, Notification) Self(Unset, Local, Notification)
} }
pub fn to_redis_raw_timeline(&self, hashtag: Option<&String>) -> String { pub fn to_redis_raw_timeline(&self, hashtag: Option<&String>) -> Result<String, TimelineErr> {
use {Content::*, Reach::*, Stream::*}; use {Content::*, Reach::*, Stream::*};
match self { Ok(match self {
Timeline(Public, Federated, All) => "timeline:public".into(), Timeline(Public, Federated, All) => "timeline:public".into(),
Timeline(Public, Local, All) => "timeline:public:local".into(), Timeline(Public, Local, All) => "timeline:public:local".into(),
Timeline(Public, Federated, Media) => "timeline:public:media".into(), Timeline(Public, Federated, Media) => "timeline:public:media".into(),
Timeline(Public, Local, Media) => "timeline:public:local:media".into(), Timeline(Public, Local, Media) => "timeline:public:local:media".into(),
Timeline(Hashtag(id), Federated, All) => format!( Timeline(Hashtag(_id), Federated, All) => format!(
"timeline:hashtag:{}", "timeline:hashtag:{}",
hashtag.unwrap_or_else(|| log_fatal!("Did not supply a name for hashtag #{}", id)) hashtag.ok_or_else(|| TimelineErr::MissingHashtag)?
), ),
Timeline(Hashtag(id), Local, All) => format!( Timeline(Hashtag(_id), Local, All) => format!(
"timeline:hashtag:{}:local", "timeline:hashtag:{}:local",
hashtag.unwrap_or_else(|| log_fatal!("Did not supply a name for hashtag #{}", id)) hashtag.ok_or_else(|| TimelineErr::MissingHashtag)?
), ),
Timeline(User(id), Federated, All) => format!("timeline:{}", id), Timeline(User(id), Federated, All) => format!("timeline:{}", id),
Timeline(User(id), Federated, Notification) => format!("timeline:{}:notification", id), Timeline(User(id), Federated, Notification) => format!("timeline:{}:notification", id),
Timeline(List(id), Federated, All) => format!("timeline:list:{}", id), Timeline(List(id), Federated, All) => format!("timeline:list:{}", id),
Timeline(Direct(id), Federated, All) => format!("timeline:direct:{}", id), Timeline(Direct(id), Federated, All) => format!("timeline:direct:{}", id),
Timeline(one, _two, _three) => { Timeline(_one, _two, _three) => Err(TimelineErr::InvalidInput)?,
log_fatal!("Supposedly impossible timeline reached: {:?}", one) })
}
}
} }
pub fn from_redis_text( pub fn from_redis_text(
@ -226,10 +219,10 @@ impl Timeline {
["public", "local", "media"] => Timeline(Public, Local, Media), ["public", "local", "media"] => Timeline(Public, Local, Media),
["hashtag", tag] => Timeline(Hashtag(id_from_tag(tag)?), Federated, All), ["hashtag", tag] => Timeline(Hashtag(id_from_tag(tag)?), Federated, All),
["hashtag", tag, "local"] => Timeline(Hashtag(id_from_tag(tag)?), Local, All), ["hashtag", tag, "local"] => Timeline(Hashtag(id_from_tag(tag)?), Local, All),
[id] => Timeline(User(id.parse().unwrap()), Federated, All), [id] => Timeline(User(id.parse()?), Federated, All),
[id, "notification"] => Timeline(User(id.parse().unwrap()), Federated, Notification), [id, "notification"] => Timeline(User(id.parse()?), Federated, Notification),
["list", id] => Timeline(List(id.parse().unwrap()), Federated, All), ["list", id] => Timeline(List(id.parse()?), Federated, All),
["direct", id] => Timeline(Direct(id.parse().unwrap()), Federated, All), ["direct", id] => Timeline(Direct(id.parse()?), Federated, All),
// Other endpoints don't exist: // Other endpoints don't exist:
[..] => Err(TimelineErr::InvalidInput)?, [..] => Err(TimelineErr::InvalidInput)?,
}) })
@ -255,11 +248,11 @@ impl Timeline {
"hashtag" => Timeline(Hashtag(id_from_hashtag()?), Federated, All), "hashtag" => Timeline(Hashtag(id_from_hashtag()?), Federated, All),
"hashtag:local" => Timeline(Hashtag(id_from_hashtag()?), Local, All), "hashtag:local" => Timeline(Hashtag(id_from_hashtag()?), Local, All),
"user" => match user.scopes.contains(&Statuses) { "user" => match user.scopes.contains(&Statuses) {
true => Timeline(User(*user.id), Federated, All), true => Timeline(User(user.id), Federated, All),
false => Err(custom("Error: Missing access token"))?, false => Err(custom("Error: Missing access token"))?,
}, },
"user:notification" => match user.scopes.contains(&Statuses) { "user:notification" => match user.scopes.contains(&Statuses) {
true => Timeline(User(*user.id), Federated, Notification), true => Timeline(User(user.id), Federated, Notification),
false => Err(custom("Error: Missing access token"))?, false => Err(custom("Error: Missing access token"))?,
}, },
"list" => match user.scopes.contains(&Lists) && user_owns_list() { "list" => match user.scopes.contains(&Lists) && user_owns_list() {
@ -280,7 +273,8 @@ impl Timeline {
#[derive(Clone, Debug, Copy, Eq, Hash, PartialEq)] #[derive(Clone, Debug, Copy, Eq, Hash, PartialEq)]
pub enum Stream { pub enum Stream {
User(i64), User(Id),
// TODO consider whether List, Direct, and Hashtag should all be `id::Id`s
List(i64), List(i64),
Direct(i64), Direct(i64),
Hashtag(i64), Hashtag(i64),

View File

@ -133,7 +133,7 @@ impl SseStream {
.filter_map(move |(timeline, event)| { .filter_map(move |(timeline, event)| {
if target_timeline == timeline { if target_timeline == timeline {
use crate::messages::{ use crate::messages::{
CheckedEvent, CheckedEvent::Update, Event::*, EventKind, CheckedEvent, CheckedEvent::Update, DynEvent, Event::*, EventKind,
}; };
use crate::parse_client_request::Stream::Public; use crate::parse_client_request::Stream::Public;
@ -148,13 +148,16 @@ impl SseStream {
}, },
TypeSafe(non_update) => Self::reply_with(Event::TypeSafe(non_update)), TypeSafe(non_update) => Self::reply_with(Event::TypeSafe(non_update)),
Dynamic(dyn_event) => { Dynamic(dyn_event) => {
if let EventKind::Update(s) = dyn_event.kind.clone() { if let EventKind::Update(s) = dyn_event.kind {
match timeline { match timeline {
Timeline(Public, _, _) if s.language_not(&allowed_langs) => { Timeline(Public, _, _) if s.language_not(&allowed_langs) => {
None None
} }
_ if s.involves_any(&blocks) => None, _ if s.involves_any(&blocks) => None,
_ => Self::reply_with(Dynamic(dyn_event)), _ => Self::reply_with(Dynamic(DynEvent {
kind: EventKind::Update(s),
..dyn_event
})),
} }
} else { } else {
None None

View File

@ -59,7 +59,6 @@ impl Receiver {
pub fn subscribe(&mut self, subscription: &Subscription) -> Result<()> { pub fn subscribe(&mut self, subscription: &Subscription) -> Result<()> {
let (tag, tl) = (subscription.hashtag_name.clone(), subscription.timeline); let (tag, tl) = (subscription.hashtag_name.clone(), subscription.timeline);
if let (Some(hashtag), Timeline(Stream::Hashtag(id), _, _)) = (tag, tl) { if let (Some(hashtag), Timeline(Stream::Hashtag(id), _, _)) = (tag, tl) {
self.redis_connection.update_cache(hashtag, id); self.redis_connection.update_cache(hashtag, id);
}; };
@ -74,7 +73,6 @@ impl Receiver {
if *number_of_subscriptions == 1 { if *number_of_subscriptions == 1 {
self.redis_connection.send_cmd(Subscribe, &tl)? self.redis_connection.send_cmd(Subscribe, &tl)?
}; };
log::info!("Started stream for {:?}", tl);
Ok(()) Ok(())
} }

View File

@ -1,3 +1,4 @@
use crate::err::TimelineErr;
use std::fmt; use std::fmt;
#[derive(Debug)] #[derive(Debug)]
@ -8,6 +9,7 @@ pub enum RedisConnErr {
IncorrectPassword(String), IncorrectPassword(String),
MissingPassword, MissingPassword,
NotRedis(String), NotRedis(String),
TimelineErr(TimelineErr),
} }
impl RedisConnErr { impl RedisConnErr {
@ -49,11 +51,18 @@ impl fmt::Display for RedisConnErr {
REDIS_PORT environmental variables and try again.", REDIS_PORT environmental variables and try again.",
addr addr
), ),
TimelineErr(inner) => format!("{}", inner),
}; };
write!(f, "{}", msg) write!(f, "{}", msg)
} }
} }
impl From<TimelineErr> for RedisConnErr {
fn from(e: TimelineErr) -> RedisConnErr {
RedisConnErr::TimelineErr(e)
}
}
impl From<std::io::Error> for RedisConnErr { impl From<std::io::Error> for RedisConnErr {
fn from(e: std::io::Error) -> RedisConnErr { fn from(e: std::io::Error) -> RedisConnErr {
RedisConnErr::UnknownRedisErr(e) RedisConnErr::UnknownRedisErr(e)

View File

@ -166,8 +166,8 @@ impl RedisConn {
Timeline(Stream::Hashtag(id), _, _) => self.tag_name_cache.get(id), Timeline(Stream::Hashtag(id), _, _) => self.tag_name_cache.get(id),
_non_hashtag_timeline => None, _non_hashtag_timeline => None,
}; };
let tl = timeline.to_redis_raw_timeline(hashtag);
let tl = timeline.to_redis_raw_timeline(hashtag)?;
let (primary_cmd, secondary_cmd) = match cmd { let (primary_cmd, secondary_cmd) = match cmd {
RedisCmd::Subscribe => ( RedisCmd::Subscribe => (
format!("*2\r\n$9\r\nsubscribe\r\n${}\r\n{}\r\n", tl.len(), tl), format!("*2\r\n$9\r\nsubscribe\r\n${}\r\n{}\r\n", tl.len(), tl),