mastodon_async_entities/
filter.rs

1use std::fmt::Display;
2
3use serde::{de::Visitor, Deserialize, Deserializer, Serialize};
4use time::{serde::iso8601, OffsetDateTime};
5
6/// Represents a user-defined filter for determining which statuses should not
7/// be shown to the user.
8///
9/// ## Example
10/// ```rust
11/// use mastodon_async_entities::prelude::*;
12/// let subject = r#"{
13///     "id": "19972",
14///     "title": "Test filter",
15///     "context": [
16///         "home"
17///     ],
18///     "expires_at": "2022-09-20T17:27:39.296Z",
19///     "filter_action": "warn",
20///     "keywords": [
21///         {
22///             "id": "1197",
23///             "keyword": "bad word",
24///             "whole_word": false
25///         }
26///     ],
27///     "statuses": [
28///         {
29///             "id": "1",
30///             "status_id": "109031743575371913"
31///         }
32///     ]
33/// }"#;
34/// let subject: Filter = serde_json::from_str(subject).expect("deserialize");
35/// assert_eq!(subject.id, FilterId::new("19972"));
36/// ```
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct Filter {
39    /// The ID of the Filter in the database.
40    pub id: FilterId,
41    /// A title given by the user to name the filter.
42    pub title: String,
43    /// The contexts in which the filter should be applied.
44    pub context: Vec<FilterContext>,
45    /// When the filter should no longer be applied.
46    #[serde(with = "iso8601::option")]
47    pub expires_at: Option<OffsetDateTime>,
48    /// The action to be taken when a status matches this filter.
49    pub filter_action: Action,
50    /// The keywords grouped under this filter.
51    pub keywords: Vec<Keyword>,
52    /// The statuses grouped under this filter.
53    pub statuses: Vec<Status>,
54}
55
56/// Wrapper type for a filter ID string
57#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
58#[serde(transparent)]
59pub struct FilterId(String);
60
61impl AsRef<str> for FilterId {
62    fn as_ref(&self) -> &str {
63        &self.0
64    }
65}
66
67impl FilterId {
68    pub fn new(value: impl Into<String>) -> Self {
69        Self(value.into())
70    }
71}
72
73impl Display for FilterId {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        write!(f, "{}", self.0)
76    }
77}
78
79static_assertions::assert_not_impl_any!(
80    FilterId: PartialEq<crate::account::AccountId>,
81    PartialEq<crate::attachment::AttachmentId>,
82    PartialEq<crate::list::ListId>,
83    PartialEq<crate::mention::MentionId>,
84    PartialEq<crate::notification::NotificationId>,
85    PartialEq<crate::relationship::RelationshipId>,
86    PartialEq<crate::push::SubscriptionId>,
87    PartialEq<crate::report::ReportId>,
88    PartialEq<crate::status::StatusId>,
89);
90
91/// Represents the various types of Filter contexts
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
93#[serde(rename_all = "lowercase")]
94pub enum FilterContext {
95    /// Represents the "home" context
96    Home,
97    /// Represents the "notifications" context
98    Notifications,
99    /// Represents the "public" context
100    Public,
101    /// Represents the "thread" context
102    Thread,
103    /// Represents the "account" context
104    Account,
105}
106
107/// The action the filter should take
108///
109/// Please note that the spec requests that any unknown value be interpreted
110/// as "warn".
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
112#[serde(rename_all = "lowercase")]
113pub enum Action {
114    /// Indicates filtered toots should show up, but with a warning
115    Warn,
116    /// Indicates filtered toots should be hidden.
117    Hide,
118}
119
120impl<'de> Deserialize<'de> for Action {
121    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
122    where
123        D: Deserializer<'de>,
124    {
125        struct FilterActionDeserializer;
126
127        impl<'v> Visitor<'v> for FilterActionDeserializer {
128            type Value = Action;
129
130            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
131                formatter.write_str(r#""warn" or "hide" (or really any string; any string other than "hide" will deserialize to "warn")"#)
132            }
133
134            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
135            where
136                E: serde::de::Error,
137            {
138                Ok(if v == "hide" {
139                    Action::Hide
140                } else {
141                    Action::Warn
142                })
143            }
144        }
145
146        deserializer.deserialize_str(FilterActionDeserializer)
147    }
148}
149
150/// Represents a keyword that, if matched, should cause the filter action to be taken.
151///
152/// ## Example
153/// ```json
154/// {
155///     "id": "1197",
156///     "keyword": "bad word",
157///     "whole_word": false
158/// }
159/// ```
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
161pub struct Keyword {
162    /// The ID of the FilterKeyword in the database.
163    id: String,
164    /// The phrase to be matched against.
165    keyword: String,
166    /// Should the filter consider word boundaries? See [implementation guidelines
167    /// for filters](https://docs.joinmastodon.org/api/guidelines/#filters).
168    whole_word: bool,
169}
170
171/// Represents a status ID that, if matched, should cause the filter action to be taken.
172///
173/// ## Example
174/// ```json
175/// {
176///     "id": "1",
177///     "status_id": "109031743575371913"
178/// }
179/// ```
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181pub struct Status {
182    /// The ID of the FilterStatus in the database.
183    id: String,
184    /// The ID of the filtered Status in the database.
185    status_id: String,
186}
187
188mod v1 {
189    pub use super::FilterContext;
190    use serde::{Deserialize, Serialize};
191    use time::{serde::iso8601, OffsetDateTime};
192
193    /// Represents a single v1 Filter
194    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195    pub struct Filter {
196        /// The ID of the Filter in the database.
197        pub id: String,
198        /// The text to be filtered.
199        pub phrase: String,
200        /// The contexts in which the filter should be applied.
201        pub context: Vec<FilterContext>,
202        /// When the filter should no longer be applied.
203        ///
204        /// `None` indicates that the filter does not expire.
205        #[serde(with = "iso8601::option")]
206        pub expires_at: Option<OffsetDateTime>,
207        /// Should matching entities in home and notifications be dropped by the server?
208        pub irreversible: bool,
209        /// Should the filter consider word boundaries?
210        pub whole_word: bool,
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    #[cfg(feature = "json")]
217    use super::*;
218
219    #[cfg(feature = "json")]
220    #[test]
221    fn test_filter_action_serialize_and_deserialize() {
222        use Action::*;
223        let hide = r#""hide""#;
224        let warn = r#""warn""#;
225        let subject = serde_json::to_string(&Hide).expect("serialize hide");
226        assert_eq!(subject, hide);
227        let subject = serde_json::to_string(&Warn).expect("serialize warn");
228        assert_eq!(subject, warn);
229        let subject: Action = serde_json::from_str(hide).expect("deserialize hide");
230        assert_eq!(subject, Hide);
231        let subject: Action = serde_json::from_str(warn).expect("deserialize warn");
232        assert_eq!(subject, Warn);
233        let subject: Action =
234            serde_json::from_str(r#""something else""#).expect("deserialize something else");
235        assert_eq!(subject, Warn);
236        // This 👆 further implies...
237        let subject: Action = serde_json::from_str(r#""Hide""#).expect("deserialize Hide");
238        assert_eq!(subject, Warn /* 👈 !             capital H */);
239        // This behavior is specified by the spec https://docs.joinmastodon.org/entities/Filter/#filter_action
240
241        // improper value
242        let subject: Result<Action, _> = serde_json::from_str("[1, 2, 3]");
243        let subject = subject.expect_err("value was not expected to be valid");
244        assert_eq!(
245            subject.to_string(),
246            r#"invalid type: sequence, expected "warn" or "hide" (or really any string; any string other than "hide" will deserialize to "warn") at line 1 column 0"#
247        );
248    }
249}