use crate::log_fatal; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::boxed::Box; use std::{collections::HashSet, string::String}; pub enum Event { TypeSafe(CheckedEvent), Dynamic(DynamicEvent), } impl Event { pub fn to_json_string(&self) -> String { let event = &self.event_name(); let sendable_event = match self.payload() { Some(payload) => SendableEvent::WithPayload { event, payload }, None => SendableEvent::NoPayload { event }, }; serde_json::to_string(&sendable_event) .unwrap_or_else(|_| log_fatal!("Could not serialize `{:?}`", &sendable_event)) } pub fn event_name(&self) -> String { String::from(match self { Self::TypeSafe(checked) => match checked { CheckedEvent::Update { .. } => "update", CheckedEvent::Notification { .. } => "notification", CheckedEvent::Delete { .. } => "delete", CheckedEvent::Announcement { .. } => "announcement", CheckedEvent::AnnouncementReaction { .. } => "announcement.reaction", CheckedEvent::AnnouncementDelete { .. } => "announcement.delete", CheckedEvent::Conversation { .. } => "conversation", CheckedEvent::FiltersChanged => "filters_changed", }, Self::Dynamic(dyn_event) => &dyn_event.event, }) } pub fn payload(&self) -> Option { use CheckedEvent::*; match self { Self::TypeSafe(checked) => match checked { Update { payload, .. } => Some(escaped(payload)), Notification { payload, .. } => Some(escaped(payload)), Delete { payload, .. } => Some(payload.0.clone()), Announcement { payload, .. } => Some(escaped(payload)), AnnouncementReaction { payload, .. } => Some(escaped(payload)), AnnouncementDelete { payload, .. } => Some(payload.0.clone()), Conversation { payload, .. } => Some(escaped(payload)), FiltersChanged => None, }, Self::Dynamic(dyn_event) => Some(dyn_event.payload.to_string()), } } } #[derive(Deserialize, Debug, Clone, PartialEq)] pub struct DynamicEvent { pub event: String, payload: Value, queued_at: Option, } #[serde(rename_all = "snake_case", tag = "event", deny_unknown_fields)] #[rustfmt::skip] #[derive(Deserialize, Debug, Clone, PartialEq)] pub enum CheckedEvent { Update { payload: Status, queued_at: Option }, Notification { payload: Notification }, Delete { payload: DeletedId }, FiltersChanged, Announcement { payload: Announcement }, #[serde(rename(serialize = "announcement.reaction", deserialize = "announcement.reaction"))] AnnouncementReaction { payload: AnnouncementReaction }, #[serde(rename(serialize = "announcement.delete", deserialize = "announcement.delete"))] AnnouncementDelete { payload: DeletedId }, Conversation { payload: Conversation, queued_at: Option }, } #[derive(Serialize, Debug, Clone)] #[serde(untagged)] pub enum SendableEvent<'a> { WithPayload { event: &'a str, payload: String }, NoPayload { event: &'a str }, } fn escaped(content: T) -> String { serde_json::to_string(&content) .unwrap_or_else(|_| log_fatal!("Could not parse Event with: `{:?}`", &content)) } #[serde(deny_unknown_fields)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Conversation { id: String, accounts: Vec, unread: bool, last_status: Option, } #[serde(deny_unknown_fields)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct DeletedId(String); #[serde(deny_unknown_fields)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Status { id: String, uri: String, created_at: String, account: Account, content: String, visibility: Visibility, sensitive: bool, spoiler_text: String, media_attachments: Vec, application: Option, // Should be non-optional? mentions: Vec, tags: Vec, emojis: Vec, reblogs_count: i64, favourites_count: i64, replies_count: i64, url: Option, in_reply_to_id: Option, in_reply_to_account_id: Option, reblog: Option>, poll: Option, card: Option, language: Option, text: Option, // ↓↓↓ Only for authorized users favourited: Option, reblogged: Option, muted: Option, bookmarked: Option, pinned: Option, } #[serde(rename_all = "lowercase", deny_unknown_fields)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub enum Visibility { Public, Unlisted, Private, Direct, } #[serde(deny_unknown_fields)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Account { id: String, username: String, acct: String, url: String, display_name: String, note: String, avatar: String, avatar_static: String, header: String, header_static: String, locked: bool, emojis: Vec, discoverable: Option, // Shouldn't be option? created_at: String, statuses_count: i64, followers_count: i64, following_count: i64, moved: Option>, fields: Option>, bot: Option, source: Option, group: Option, // undocumented last_status_at: Option, // undocumented } #[serde(deny_unknown_fields)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] struct Attachment { id: String, r#type: AttachmentType, url: String, preview_url: String, remote_url: Option, text_url: Option, meta: Option, description: Option, blurhash: Option, } #[serde(rename_all = "lowercase", deny_unknown_fields)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] enum AttachmentType { Unknown, Image, Gifv, Video, Audio, } #[serde(deny_unknown_fields)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Application { name: String, website: Option, vapid_key: Option, client_id: Option, client_secret: Option, } #[serde(deny_unknown_fields)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] struct Emoji { shortcode: String, url: String, static_url: String, visible_in_picker: bool, category: Option, } #[serde(deny_unknown_fields)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] struct Field { name: String, value: String, verified_at: Option, } #[serde(deny_unknown_fields)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] struct Source { note: String, fields: Vec, privacy: Option, sensitive: bool, language: String, follow_requests_count: i64, } #[serde(deny_unknown_fields)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Mention { id: String, username: String, acct: String, url: String, } #[serde(deny_unknown_fields)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] struct Tag { name: String, url: String, history: Option>, } #[serde(deny_unknown_fields)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] struct Poll { id: String, expires_at: String, expired: bool, multiple: bool, votes_count: i64, voters_count: Option, voted: Option, own_votes: Option>, options: Vec, emojis: Vec, } #[serde(deny_unknown_fields)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] struct PollOptions { title: String, votes_count: Option, } #[serde(deny_unknown_fields)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] struct Card { url: String, title: String, description: String, r#type: CardType, author_name: Option, author_url: Option, provider_name: Option, provider_url: Option, html: Option, width: Option, height: Option, image: Option, embed_url: Option, } #[serde(rename_all = "lowercase", deny_unknown_fields)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] enum CardType { Link, Photo, Video, Rich, } #[serde(deny_unknown_fields)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] struct History { day: String, uses: String, accounts: String, } #[serde(deny_unknown_fields)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Notification { id: String, r#type: NotificationType, created_at: String, account: Account, status: Option, } #[serde(rename_all = "snake_case", deny_unknown_fields)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] enum NotificationType { Follow, FollowRequest, // Undocumented Mention, Reblog, Favourite, Poll, } #[serde(deny_unknown_fields)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Announcement { // Fully undocumented id: String, tags: Vec, all_day: bool, content: String, emojis: Vec, starts_at: Option, ends_at: Option, published_at: String, updated_at: String, mentions: Vec, reactions: Vec, } #[serde(deny_unknown_fields)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct AnnouncementReaction { #[serde(skip_serializing_if = "Option::is_none")] announcement_id: Option, count: i64, name: String, } impl Status { /// Returns `true` if the status is filtered out based on its language pub fn language_not_allowed(&self, allowed_langs: &HashSet) -> bool { const ALLOW: bool = false; const REJECT: bool = true; let reject_and_maybe_log = |toot_language| { log::info!("Filtering out toot from `{}`", &self.account.acct); log::info!("Toot language: `{}`", toot_language); log::info!("Recipient's allowed languages: `{:?}`", allowed_langs); REJECT }; if allowed_langs.is_empty() { return ALLOW; // listing no allowed_langs results in allowing all languages } match self.language.as_ref() { 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_and_maybe_log(toot_language), } } /// Returns `true` if this toot originated from a domain the User has blocked. pub fn from_blocked_domain(&self, blocked_domains: &HashSet) -> bool { let full_username = &self.account.acct; match full_username.split('@').nth(1) { Some(originating_domain) => blocked_domains.contains(originating_domain), None => false, // None means the user is on the local instance, which can't be blocked } } /// Returns `true` if the Status is from an account that has blocked the current user. pub fn from_blocking_user(&self, blocking_users: &HashSet) -> bool { const ALLOW: bool = false; const REJECT: bool = true; let err = |_| log_fatal!("Could not process `account.id` in {:?}", &self); if blocking_users.contains(&self.account.id.parse().unwrap_or_else(err)) { REJECT } else { ALLOW } } /// Returns `true` if the User's list of blocked and muted users includes a user /// involved in this toot. /// /// A user is involved 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_blocked_user(&self, blocked_users: &HashSet) -> bool { const ALLOW: bool = false; const REJECT: bool = true; let err = |_| log_fatal!("Could not process an `id` field in {:?}", &self); // involved_users = mentioned_users + author + replied-to user + boosted user let mut involved_users: HashSet = self .mentions .iter() .map(|mention| mention.id.parse().unwrap_or_else(err)) .collect(); involved_users.insert(self.account.id.parse::().unwrap_or_else(err)); if let Some(replied_to_account_id) = self.in_reply_to_account_id.clone() { involved_users.insert(replied_to_account_id.parse().unwrap_or_else(err)); } if let Some(boosted_status) = self.reblog.clone() { involved_users.insert(boosted_status.account.id.parse().unwrap_or_else(err)); } if involved_users.is_disjoint(blocked_users) { ALLOW } else { REJECT } } } #[cfg(test)] mod test;