flodgatt/old

448 lines
13 KiB
Plaintext

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<String> {
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<i64>,
}
#[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<i64> },
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<i64> },
}
#[derive(Serialize, Debug, Clone)]
#[serde(untagged)]
pub enum SendableEvent<'a> {
WithPayload { event: &'a str, payload: String },
NoPayload { event: &'a str },
}
fn escaped<T: Serialize + std::fmt::Debug>(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<Account>,
unread: bool,
last_status: Option<Status>,
}
#[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<Attachment>,
application: Option<Application>, // Should be non-optional?
mentions: Vec<Mention>,
tags: Vec<Tag>,
emojis: Vec<Emoji>,
reblogs_count: i64,
favourites_count: i64,
replies_count: i64,
url: Option<String>,
in_reply_to_id: Option<String>,
in_reply_to_account_id: Option<String>,
reblog: Option<Box<Status>>,
poll: Option<Poll>,
card: Option<Card>,
language: Option<String>,
text: Option<String>,
// ↓↓↓ Only for authorized users
favourited: Option<bool>,
reblogged: Option<bool>,
muted: Option<bool>,
bookmarked: Option<bool>,
pinned: Option<bool>,
}
#[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<Emoji>,
discoverable: Option<bool>, // Shouldn't be option?
created_at: String,
statuses_count: i64,
followers_count: i64,
following_count: i64,
moved: Option<Box<String>>,
fields: Option<Vec<Field>>,
bot: Option<bool>,
source: Option<Source>,
group: Option<bool>, // undocumented
last_status_at: Option<String>, // 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<String>,
text_url: Option<String>,
meta: Option<serde_json::Value>,
description: Option<String>,
blurhash: Option<String>,
}
#[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<String>,
vapid_key: Option<String>,
client_id: Option<String>,
client_secret: Option<String>,
}
#[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<String>,
}
#[serde(deny_unknown_fields)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct Field {
name: String,
value: String,
verified_at: Option<String>,
}
#[serde(deny_unknown_fields)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct Source {
note: String,
fields: Vec<Field>,
privacy: Option<Visibility>,
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<Vec<History>>,
}
#[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<i64>,
voted: Option<bool>,
own_votes: Option<Vec<i64>>,
options: Vec<PollOptions>,
emojis: Vec<Emoji>,
}
#[serde(deny_unknown_fields)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct PollOptions {
title: String,
votes_count: Option<i32>,
}
#[serde(deny_unknown_fields)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct Card {
url: String,
title: String,
description: String,
r#type: CardType,
author_name: Option<String>,
author_url: Option<String>,
provider_name: Option<String>,
provider_url: Option<String>,
html: Option<String>,
width: Option<i64>,
height: Option<i64>,
image: Option<String>,
embed_url: Option<String>,
}
#[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<Status>,
}
#[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<Tag>,
all_day: bool,
content: String,
emojis: Vec<Emoji>,
starts_at: Option<String>,
ends_at: Option<String>,
published_at: String,
updated_at: String,
mentions: Vec<Mention>,
reactions: Vec<AnnouncementReaction>,
}
#[serde(deny_unknown_fields)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct AnnouncementReaction {
#[serde(skip_serializing_if = "Option::is_none")]
announcement_id: Option<String>,
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<String>) -> 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<String>) -> 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<i64>) -> 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<i64>) -> 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<i64> = self
.mentions
.iter()
.map(|mention| mention.id.parse().unwrap_or_else(err))
.collect();
involved_users.insert(self.account.id.parse::<i64>().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;