diff options
author | Scott Boggs <scott@tams.tech> | 2024-04-11 07:21:51 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-04-11 07:21:51 -0400 |
commit | 32addb170c52a728b32e3fd3a0e06a62307c4ec1 (patch) | |
tree | ba751a066f0020193e208a93b42eacc44d0ce144 | |
parent | aa38f1808c8433c94dd08aac2d8d502de7ef8211 (diff) | |
parent | 18d8bfca8acf15be59a39342c19f6834d778cc39 (diff) |
Merge pull request #144 from dscottboggs/comb-methods/filterscomb
Comb methods: Filters
-rw-r--r-- | .github/workflows/rust.yml | 4 | ||||
-rw-r--r-- | entities/Cargo.toml | 2 | ||||
-rw-r--r-- | entities/src/filter.rs | 8 | ||||
-rw-r--r-- | entities/src/forms/filter.rs | 285 | ||||
-rw-r--r-- | entities/src/forms/mod.rs | 1 | ||||
-rw-r--r-- | entities/src/helpers.rs | 149 | ||||
-rw-r--r-- | entities/src/lib.rs | 3 | ||||
-rw-r--r-- | examples/list_following.rs | 48 | ||||
-rw-r--r-- | src/lib.rs | 2 | ||||
-rw-r--r-- | src/macros.rs | 118 | ||||
-rw-r--r-- | src/mastodon.rs | 42 | ||||
-rw-r--r-- | src/requests/filter.rs | 149 | ||||
-rw-r--r-- | src/requests/mod.rs | 3 |
13 files changed, 629 insertions, 185 deletions
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 24e2633..3d159dc 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -39,7 +39,7 @@ jobs: - uses: swatinem/rust-cache@v2 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.67.0 + toolchain: 1.70.0 components: clippy - run: cargo clippy --all-features -- -D warnings @@ -51,7 +51,7 @@ jobs: - uses: swatinem/rust-cache@v2 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.67.0 + toolchain: 1.70.0 components: rustfmt - run: cargo fmt --check diff --git a/entities/Cargo.toml b/entities/Cargo.toml index 91f9896..5839e27 100644 --- a/entities/Cargo.toml +++ b/entities/Cargo.toml @@ -16,7 +16,7 @@ static_assertions = "1" derive_is_enum_variant = "0.1.1" [dependencies.derive_builder] -version = "0.12.0" +version = "0.20.0" features = ["clippy"] [dependencies.log] diff --git a/entities/src/filter.rs b/entities/src/filter.rs index 3e859d0..2ff60b4 100644 --- a/entities/src/filter.rs +++ b/entities/src/filter.rs @@ -42,7 +42,7 @@ pub struct Filter { /// A title given by the user to name the filter. pub title: String, /// The contexts in which the filter should be applied. - pub context: Vec<FilterContext>, + pub context: Vec<Context>, /// When the filter should no longer be applied. #[serde(with = "iso8601::option")] pub expires_at: Option<OffsetDateTime>, @@ -57,7 +57,7 @@ pub struct Filter { /// Represents the various types of Filter contexts #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, is_enum_variant)] #[serde(rename_all = "lowercase")] -pub enum FilterContext { +pub enum Context { /// Represents the "home" context Home, /// Represents the "notifications" context @@ -154,7 +154,7 @@ pub struct Status { mod v1 { use crate::FilterId; - pub use super::FilterContext; + pub use super::Context; use serde::{Deserialize, Serialize}; use time::{serde::iso8601, OffsetDateTime}; @@ -166,7 +166,7 @@ mod v1 { /// The text to be filtered. pub phrase: String, /// The contexts in which the filter should be applied. - pub context: Vec<FilterContext>, + pub context: Vec<Context>, /// When the filter should no longer be applied. /// /// `None` indicates that the filter does not expire. diff --git a/entities/src/forms/filter.rs b/entities/src/forms/filter.rs new file mode 100644 index 0000000..65039ee --- /dev/null +++ b/entities/src/forms/filter.rs @@ -0,0 +1,285 @@ +use time::Duration; + +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; + +use crate::{helpers::serde_opt_duration_as_seconds, prelude::*}; + +#[derive(Builder, Debug, Default, Deserialize, Serialize, Clone)] +#[builder(derive(Debug), build_fn(error = "crate::Error"))] +/// Form for creating a Filter. +/// +/// ``` +/// use mastodon_async_entities::prelude::*; +/// use time::ext::NumericalDuration; +/// +/// let filter = forms::filter::Add::builder("test filter") +/// .add_context(filter::Context::Home) +/// .filter_action(filter::Action::Hide) +/// .expires_in(60.seconds()) +/// .keyword(forms::filter::add::Keyword::whole_word("test")) +/// .keyword(forms::filter::add::Keyword::substring("substring you really don't want to see")) +/// .build() +/// .unwrap(); +/// assert_eq!(serde_json::to_string_pretty(&filter).unwrap(), r#"{ +/// "title": "test filter", +/// "context": [ +/// "home" +/// ], +/// "filter_action": "hide", +/// "expires_in": 60, +/// "keywords_attributes": [ +/// { +/// "keyword": "test", +/// "whole_word": true +/// }, +/// { +/// "keyword": "substring you really don't want to see", +/// "whole_word": false +/// } +/// ] +/// }"#); +/// ``` +/// +/// See also [the API reference](https://docs.joinmastodon.org/methods/filters/#create) +pub struct Add { + /// The name of the filter group. + #[builder(setter(custom), default)] + title: String, + /// Where the filter should be applied. Specify at least one. + #[serde(serialize_with = "add::disallow_empty_context")] + #[builder(default, setter(into, strip_option))] + context: Vec<filter::Context>, + /// The policy to be applied when the filter is matched. + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] + filter_action: Option<filter::Action>, + /// How long from now should the filter expire? + #[serde( + with = "serde_opt_duration_as_seconds", + skip_serializing_if = "Option::is_none", + default + )] + #[builder(default, setter(into, strip_option))] + expires_in: Option<Duration>, + /// A list of keywords to be added to the newly-created filter + #[serde(skip_serializing_if = "Vec::is_empty")] + #[builder(default, setter(into, strip_option))] + keywords_attributes: Vec<add::Keyword>, +} + +impl Add { + pub fn builder(title: impl Into<String>) -> AddBuilder { + AddBuilder { + title: Some(title.into()), + ..Default::default() + } + } +} + +impl AddBuilder { + pub fn add_context(&mut self, context: filter::Context) -> &mut Self { + self.context + .get_or_insert_with(Default::default) + .push(context); + self + } + pub fn keyword(&mut self, keyword: add::Keyword) -> &mut Self { + self.keywords_attributes + .get_or_insert_with(Default::default) + .push(keyword); + self + } +} + +pub mod add { + use derive_builder::Builder; + use serde::{ser, Deserialize, Serialize, Serializer}; + + use crate::prelude::*; + + #[derive(Debug, Deserialize, Serialize, Builder, Clone)] + pub struct Keyword { + /// A keyword to be added to the newly-created filter group + keyword: String, + /// Whether the keyword should consider word boundaries. + whole_word: bool, + } + + impl Keyword { + pub fn new(keyword: String, whole_word: bool) -> Self { + Self { + keyword, + whole_word, + } + } + /// A filter keyword which should match only if it is seen as a word or + /// phrase among other phrases. + pub fn whole_word(keyword: impl Into<String>) -> Self { + Self { + keyword: keyword.into(), + whole_word: true, + } + } + /// A filter keyword which should match even if it's seen as part of + /// another word. + pub fn substring(keyword: impl Into<String>) -> Self { + Self { + keyword: keyword.into(), + whole_word: false, + } + } + } + + pub(super) fn disallow_empty_context<S>( + context: impl AsRef<[filter::Context]>, + s: S, + ) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + let context = context.as_ref(); + if context.is_empty() { + Err(ser::Error::custom("filter context cannot be empty")) + } else { + context.serialize(s) + } + } +} + +#[derive(Builder, Debug, Deserialize, Serialize, Clone)] +#[builder(derive(Debug), build_fn(error = "crate::Error"))] +/// Form for updating a Filter. +/// +/// ``` +/// use mastodon_async_entities::prelude::*; +/// use time::ext::NumericalDuration; +/// +/// let keyword = forms::filter::update::Keyword::builder() +/// .keyword("test") +/// .whole_word(false) +/// .id("this won't work") +/// .destroy(true) +/// .build() +/// .unwrap(); +/// let filter = forms::filter::Update::builder() +/// // note that the ID isn't here: it's in the URL, not passed as a part +/// // of the form +/// .title("test filter") +/// .add_context(filter::Context::Home) +/// .filter_action(filter::Action::Hide) +/// .expires_in(60.seconds()) +/// .keyword(keyword) +/// .build() +/// .unwrap(); +/// assert_eq!(serde_json::to_string_pretty(&filter).unwrap(), r#"{ +/// "title": "test filter", +/// "context": [ +/// "home" +/// ], +/// "filter_action": "hide", +/// "expires_in": 60, +/// "keywords_attributes": [ +/// { +/// "keyword": "test", +/// "whole_word": false, +/// "id": "this won't work", +/// "destroy": true +/// } +/// ] +/// }"#); +/// ``` +/// +/// See also [the API reference](https://docs.joinmastodon.org/methods/filters/#update) +pub struct Update { + /// The name of the filter group. + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option, into))] + title: Option<String>, + /// Where the filter should be applied. Specify at least one. + #[serde(skip_serializing_if = "Vec::is_empty")] + #[builder(default, setter(into))] + context: Vec<filter::Context>, + /// The policy to be applied when the filter is matched. + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option, into))] + filter_action: Option<filter::Action>, + /// How long from now should the filter expire? + #[serde( + with = "serde_opt_duration_as_seconds", + skip_serializing_if = "Option::is_none", + default + )] + #[builder(default, setter(strip_option, into))] + expires_in: Option<Duration>, + /// A list of keywords to be added to the newly-created filter + #[serde(skip_serializing_if = "Vec::is_empty")] + #[builder(default, setter(into))] + keywords_attributes: Vec<update::Keyword>, +} + +impl Update { + pub fn builder() -> UpdateBuilder { + Default::default() + } +} + +impl UpdateBuilder { + pub fn add_context(&mut self, context: filter::Context) -> &mut Self { + self.context + .get_or_insert_with(Default::default) + .push(context); + self + } + /// Add a `update::Keyword` to the list of keywords. May be specified multiple times + pub fn keyword(&mut self, keyword: update::Keyword) -> &mut Self { + self.keywords_attributes + .get_or_insert_with(Default::default) + .push(keyword); + self + } +} + +pub mod update { + use crate::helpers::is_false; + use derive_builder::Builder; + use serde::{Deserialize, Serialize}; + + #[derive(Default, Debug, Deserialize, Serialize, Builder, Clone)] + #[builder(derive(Debug), build_fn(error = "crate::Error"))] + pub struct Keyword { + /// A keyword to be added to the newly-created filter group + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option, into))] + keyword: Option<String>, + /// Whether the keyword should consider word boundaries. + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option, into))] + whole_word: Option<bool>, + /// Provide the ID of an existing keyword to modify it, instead of creating a new keyword. + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option, into))] + id: Option<String>, + /// If true, will remove the keyword with the given ID. + #[serde(skip_serializing_if = "is_false")] + #[builder(default)] + destroy: bool, + } + + impl Keyword { + pub fn builder() -> KeywordBuilder { + Default::default() + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Status { + status_id: StatusId, +} + +impl Status { + pub fn new(status_id: StatusId) -> Self { + Self { status_id } + } +} diff --git a/entities/src/forms/mod.rs b/entities/src/forms/mod.rs index 3e11e5f..dd96a12 100644 --- a/entities/src/forms/mod.rs +++ b/entities/src/forms/mod.rs @@ -1,3 +1,4 @@ pub mod application; +pub mod filter; pub use application::{Application, ApplicationBuilder}; diff --git a/entities/src/helpers.rs b/entities/src/helpers.rs new file mode 100644 index 0000000..4f82f26 --- /dev/null +++ b/entities/src/helpers.rs @@ -0,0 +1,149 @@ +/// Returns true if the given value refers to "false" +pub fn is_false(value: &bool) -> bool { + !*value +} + +pub(crate) mod serde_opt_duration_as_seconds { + use time::{ext::NumericalDuration, Duration}; + + use serde::de; + + pub(crate) fn serialize<S>( + duration: &Option<Duration>, + serializer: S, + ) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + if let Some(duration) = duration { + serializer.serialize_i64(duration.whole_seconds()) + } else { + serializer.serialize_none() + } + } + + pub(crate) fn deserialize<'de, D>( + deserializer: D, + ) -> Result<Option<Duration>, <D as serde::Deserializer<'de>>::Error> + where + D: serde::Deserializer<'de>, + { + use serde::de::Visitor; + + struct DurationVisitor; + + impl<'v> Visitor<'v> for DurationVisitor { + type Value = Option<Duration>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "signed 64-bit integer") + } + + fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + Ok(Some(v.seconds())) + } + + fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E> + where + E: de::Error, + { + i64::try_from(v) + .map(|v| Some(v.seconds())) + .map_err(|_| de::Error::invalid_value(de::Unexpected::Unsigned(v), &self)) + } + + fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + if v.is_empty() { + Ok(None) + } else { + v.parse() + .map(|n: i64| Some(n.seconds())) + .map_err(|_| de::Error::invalid_value(de::Unexpected::Str(v), &self)) + } + } + fn visit_none<E>(self) -> Result<Self::Value, E> + where + E: de::Error, + { + Ok(None) + } + } + deserializer.deserialize_any(DurationVisitor) + } +} + +#[cfg(test)] +mod tests { + use serde::{Deserialize, Serialize}; + use time::{ext::NumericalDuration, Duration}; + + use super::*; + + #[derive(Debug, Serialize, Deserialize)] + struct TestDuration { + #[serde( + with = "serde_opt_duration_as_seconds", + skip_serializing_if = "Option::is_none", + default + )] + dur: Option<Duration>, + } + + impl Default for TestDuration { + fn default() -> Self { + TestDuration { + dur: Some(10.seconds()), + } + } + } + + impl TestDuration { + fn empty() -> Self { + Self { dur: None } + } + } + + #[test] + fn test_serialize_duration() { + let it = TestDuration::default(); + let serialized = serde_json::to_string(&it).expect("serialize"); + assert_eq!(serialized, r#"{"dur":10}"#); + } + + #[test] + fn test_serialize_empty_duration() { + let it = TestDuration::empty(); + let ser = serde_json::to_string(&it).expect("serialize"); + assert_eq!("{}", ser); + } + + #[test] + fn test_deserialize_duration() { + let text = r#"{"dur": 10}"#; + let duration: TestDuration = serde_json::from_str(text).expect("deserialize"); + assert_eq!(duration.dur.unwrap().whole_seconds(), 10); + let text = r#"{"dur": "10"}"#; + let duration: TestDuration = serde_json::from_str(text).expect("deserialize"); + assert_eq!(duration.dur.unwrap().whole_seconds(), 10); + } + + #[test] + fn test_deserialize_empty_duration() { + let text = r#"{"dur": ""}"#; + let duration: TestDuration = serde_json::from_str(text).expect("deserialize"); + assert!(duration.dur.is_none()); + } + + #[test] + fn test_deserialize_null_duration() { + let text = r#"{}"#; + let duration: TestDuration = serde_json::from_str(text).expect("deserialize"); + assert!(duration.dur.is_none()); + } +} diff --git a/entities/src/lib.rs b/entities/src/lib.rs index 80693ea..d14d59b 100644 --- a/entities/src/lib.rs +++ b/entities/src/lib.rs @@ -4,6 +4,7 @@ use serde::Serialize; /// Error types for this crate pub mod error; +mod helpers; pub use error::Error; /// Data structures for ser/de of account-related resources @@ -93,7 +94,7 @@ pub mod prelude { conversation::Conversation, custom_emoji::CustomEmoji, event::Event, - filter::{self /* for Action, Keyword, Status, v1, Result */, Filter, FilterContext}, + filter::{self /* for Action, Keyword, Status, v1, Result, Context */, Filter}, forms, ids::*, instance::{ diff --git a/examples/list_following.rs b/examples/list_following.rs new file mode 100644 index 0000000..69fa30c --- /dev/null +++ b/examples/list_following.rs @@ -0,0 +1,48 @@ +#![cfg_attr(not(feature = "toml"), allow(dead_code))] +#![cfg_attr(not(feature = "toml"), allow(unused_imports))] +mod register; + +use mastodon_async::Result; + +#[cfg(feature = "toml")] +async fn run() -> Result<()> { + use futures_util::StreamExt; + + let mastodon = register::get_mastodon_data().await?; + let you = mastodon.verify_credentials().await?; + + mastodon + .following(you.id) + .await? + .items_iter() + .for_each(|acct| async move { + match acct.acct.chars().filter(|c| *c == '@').count() { + 0 => println!("@{}@tams.tech", acct.username), + 1 => println!("@{}", acct.acct), + other => panic!("found {other} '@' characters in account name {}", acct.acct), + }; + }) + .await; + + Ok(()) +} + +#[cfg(all(feature = "toml", feature = "mt"))] +#[tokio::main] +async fn main() -> Result<()> { + run().await +} + +#[cfg(all(feature = "toml", not(feature = "mt")))] +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<()> { + run().await +} + +#[cfg(not(feature = "toml"))] +fn main() { + println!( + "examples require the `toml` feature, run this command for this example:\n\ncargo run \ + --example print_your_profile --features toml\n" + ); +} @@ -96,7 +96,7 @@ pub use mastodon_async_entities::{ status::NewStatus, status::NewStatusBuilder, visibility::Visibility, }; pub use registration::Registration; -pub use requests::{AddFilterRequest, AddPushRequest, StatusesRequest, UpdatePushRequest}; +pub use requests::{AddPushRequest, StatusesRequest, UpdatePushRequest}; /// Contains the struct that holds the client auth data pub mod data; diff --git a/src/macros.rs b/src/macros.rs index 2ee50bc..5f95958 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -239,6 +239,63 @@ macro_rules! route_v2 { route_v2!{$($rest)*} }; + + (($method:ident) $name:ident: $url:expr => $ret:ty, $($rest:tt)*) => { + doc_comment! { + concat!( + "Equivalent to `", stringify!($method), " /api/v2/", + $url, + "`\n# Errors\nIf `access_token` is not set.", + "\n", + "```no_run", + "use mastodon_async::prelude::*;\n", + "let data = Data::default();\n", + "let client = Mastodon::from(data);\n", + "client.", stringify!($name), "();\n", + "```" + ), + pub async fn $name(&self) -> Result<$ret> { + self.$method(self.route(concat!("/api/v2/", $url))).await + } + } + + route_v2!{$($rest)*} + }; + + (($method:ident<-$typ:ty) $name:ident: $url:expr => $ret:ty, $($rest:tt)*) => { + doc_comment! { + concat!( + "Equivalent to `", stringify!($method), " /api/v2/", + $url, + "`\n# Errors\nIf `access_token` is not set.", + ), + pub async fn $name(&self, form: $typ) -> Result<$ret> { + use log::debug; + use uuid::Uuid; + + let call_id = Uuid::new_v4(); + + let form_data = serde_urlencoded::to_string(&form)?; + + let url = &self.route(format!("/api/v2/{}?{form_data}", $url)); + debug!( + url = url.as_str(), method = stringify!($method), + call_id:? = call_id, + form_data:serde = &form; + "making API request" + ); + + let response = self.authenticated(self.client.$method(url)) + .header("Accept", "application/json") + .send() + .await?; + + read_response(response).await + } + } + + route_v2!{$($rest)*} + }; () => {} } @@ -461,8 +518,69 @@ macro_rules! route_id { } )* } +} + +macro_rules! route_v2_id { + (($method:ident) $name:ident[$id_type:ty]: $url:expr => $ret:ty, $($rest:tt)*) => { + doc_comment! { + concat!( + "Equivalent to `", stringify!($method), " /api/v2/", + $url, + "`\n# Errors\nIf `access_token` is not set.", + "\n", + "```no_run", + "use mastodon_async::prelude::*;\n", + "let data = Data::default();\n", + "let client = Mastodon::from(data);\n", + "client.", stringify!($name), "(\"42\");\n", + "# Ok(())\n", + "# }\n", + "```" + ), + pub async fn $name(&self, id: &$id_type) -> Result<$ret> { + self.$method(self.route(&format!(concat!("/api/v2/", $url), id))).await + } + } + + route_v2_id!{$($rest)*} + }; + (($method:ident<-$typ:ty) $name:ident[$id_type:ty]: $url:expr => $ret:ty, $($rest:tt)*) => { + doc_comment! { + concat!( + "Equivalent to `", stringify!($method), " /api/v2/", + $url, + "`\n# Errors\nIf `access_token` is not set.", + ), + pub async fn $name(&self, id: $id_type, form: $typ) -> Result<$ret> { + use log::debug; + use uuid::Uuid; + + let call_id = Uuid::new_v4(); + + let form_data = serde_urlencoded::to_string(&form)?; + + let url = &self.route(format!("/api/v2/{}?{form_data}", format!($url, id))); + debug!( + url = url.as_str(), method = stringify!($method), + call_id:? = call_id, + form_data:serde = &form; + "making API request" + ); + let response = self.authenticated(self.client.$method(url)) + .header("Accept", "application/json") + .send() + .await?; + + read_response(response).await + } + } + + route_v2_id!{$($rest)*} + }; + () => {}; } + macro_rules! paged_routes_with_id { (($method:ident) $name:ident: $url:expr => $ret:ty, $($rest:tt)*) => { diff --git a/src/mastodon.rs b/src/mastodon.rs index 373d2e7..ac245e4 100644 --- a/src/mastodon.rs +++ b/src/mastodon.rs @@ -5,7 +5,7 @@ use crate::{ errors::{Error, Result}, helpers::read_response::read_response, polling_time::PollingTime, - AddFilterRequest, AddPushRequest, Data, NewStatus, Page, StatusesRequest, UpdatePushRequest, + AddPushRequest, Data, NewStatus, Page, StatusesRequest, UpdatePushRequest, }; use futures::TryStream; use log::{debug, error, trace}; @@ -85,7 +85,6 @@ impl Mastodon { (post) clear_notifications: "notifications/clear" => Empty, (get) get_push_subscription: "push/subscription" => Subscription, (delete) delete_push_subscription: "push/subscription" => Empty, - (get) get_filters: "filters" => Vec<Filter>, (get) get_follow_suggestions: "suggestions" => Vec<Account>, (post (app: forms::Application,)) create_app: "apps" => Application, (get) verify_app: "apps/verify_credentials" => Application, @@ -95,6 +94,8 @@ impl Mastodon { (get (q: &'a str, resolve: bool,)) search: "search" => SearchResult, (post multipart with description (file: impl AsRef<Path>,)) media: "media" => Attachment, (post multipart with description (file: impl AsRef<Path>, thumbnail: impl AsRef<Path>,)) media_with_thumbnail: "media" => Attachment, + (get) filters: "filters" => Vec<Filter>, + (post<-forms::filter::Add) add_filter: "filters" => Filter, } route_id! { @@ -115,14 +116,27 @@ impl Mastodon { (post) favourite[StatusId]: "statuses/{}/favourite" => Status, (post) unfavourite[StatusId]: "statuses/{}/unfavourite" => Status, (delete) delete_status[StatusId]: "statuses/{}" => Empty, - (get) get_filter[FilterId]: "filters/{}" => Filter, - (delete) delete_filter[FilterId]: "filters/{}" => Empty, (delete) delete_from_suggestions[AccountId]: "suggestions/{}" => Empty, (post) endorse_user[AccountId]: "accounts/{}/pin" => Relationship, (post) unendorse_user[AccountId]: "accounts/{}/unpin" => Relationship, (get) attachment[AttachmentId]: "media/{}" => Attachment, } + route_v2_id! { + (get) filter[FilterId]: "filters/{}" => Filter, + (delete) delete_filter[FilterId]: "filters/{}" => Empty, + (put<-forms::filter::Update) update_filter[FilterId]: "filters/{}" => Filter, + (get) filter_keywords[FilterId]: "filters/{}/keywords" => Vec<filter::Keyword>, + (post<-forms::filter::add::Keyword) add_keyword_to_filter[FilterId]: "filters/{}/keywords" => filter::Keyword, + (get) filter_keyword[KeywordId]: "filters/keywords/{}" => filter::Keyword, + (put<-forms::filter::add::Keyword) update_filter_keyword[KeywordId]: "filters/keywords/{}" => filter::Keyword, + (delete) delete_filter_keyword[KeywordId]: "filters/keywords/{}" => Empty, + (get) filter_statuses[FilterId]: "filters/{}/statuses" => Vec<filter::Status>, + (post<-forms::filter::Status) add_status_to_filter[FilterId]: "filters/{}/statuses" => filter::Status, + (get) filter_status[StatusId]: "filters/statuses/{}" => filter::Status, + (delete) disassociate_status_from_filter[StatusId]: "filters/statuses/{}" => Empty, + } + streaming! { "returns events that are relevant to the authorized user, i.e. home timeline & notifications" stream_user@"user", @@ -155,26 +169,6 @@ impl Mastodon { format!("{}{}", self.data.base, url.as_ref()) } - /// POST /api/v1/filters - pub async fn add_filter(&self, request: &mut AddFilterRequest) -> Result<Filter> { - let response = self - .client - .post(self.route("/api/v1/filters")) - .json(&request) - .send() - .await?; - - read_response(response).await - } - - /// PUT /api/v1/filters/:id - pub async fn update_filter(&self, id: &str, request: &mut AddFilterRequest) -> Result<Filter> { - let url = self.route(format!("/api/v1/filters/{id}")); - let response = self.client.put(&url).json(&request).send().await?; - - read_response(response).await - } - /// Update the user credentials pub async fn update_credentials( &self, diff --git a/src/requests/filter.rs b/src/requests/filter.rs deleted file mode 100644 index d3dc31b..0000000 --- a/src/requests/filter.rs +++ /dev/null @@ -1,149 +0,0 @@ -use crate::entities::filter::FilterContext; -use std::time::Duration; - -/// Form used to create a filter -/// -/// // Example -/// -/// ``` -/// use mastodon_async::{entities::filter::FilterContext, requests::AddFilterRequest}; -/// let request = AddFilterRequest::new("foo", FilterContext::Home); -/// ``` -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -pub struct AddFilterRequest { - phrase: String, - context: FilterContext, - irreversible: Option<bool>, - whole_word: Option<bool>, - #[serde(serialize_with = "serialize_duration::ser")] - expires_in: Option<Duration>, -} - -impl AddFilterRequest { - /// Create a new AddFilterRequest - pub fn new(phrase: &str, context: FilterContext) -> AddFilterRequest { - AddFilterRequest { - phrase: phrase.to_string(), - context, - irreversible: None, - whole_word: None, - expires_in: None, - } - } |