summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorScott Boggs <scott@tams.tech>2024-04-11 07:21:51 -0400
committerGitHub <noreply@github.com>2024-04-11 07:21:51 -0400
commit32addb170c52a728b32e3fd3a0e06a62307c4ec1 (patch)
treeba751a066f0020193e208a93b42eacc44d0ce144
parentaa38f1808c8433c94dd08aac2d8d502de7ef8211 (diff)
parent18d8bfca8acf15be59a39342c19f6834d778cc39 (diff)
Merge pull request #144 from dscottboggs/comb-methods/filterscomb
Comb methods: Filters
-rw-r--r--.github/workflows/rust.yml4
-rw-r--r--entities/Cargo.toml2
-rw-r--r--entities/src/filter.rs8
-rw-r--r--entities/src/forms/filter.rs285
-rw-r--r--entities/src/forms/mod.rs1
-rw-r--r--entities/src/helpers.rs149
-rw-r--r--entities/src/lib.rs3
-rw-r--r--examples/list_following.rs48
-rw-r--r--src/lib.rs2
-rw-r--r--src/macros.rs118
-rw-r--r--src/mastodon.rs42
-rw-r--r--src/requests/filter.rs149
-rw-r--r--src/requests/mod.rs3
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"
+ );
+}
diff --git a/src/lib.rs b/src/lib.rs
index db4a4d5..e6cd450 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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,
- }
- }