Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add client method to wait for attachment processing #62

Merged
merged 4 commits into from
Jan 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ futures-util = "0.3.25"
static_assertions = "1.1.0"
percent-encoding = "2.2.0"
thiserror = "1.0.38"
derive_deref = "1.1.1"

[dependencies.parse_link_header]
version = "0.3.3"
Expand Down
36 changes: 36 additions & 0 deletions entities/src/attachment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ pub struct Attachment {
/// Noop will be removed.
pub description: Option<String>,
}

impl Attachment {
/// If this is an attachment which was either processed synchronously or
/// in some other way has finished processing before being deserialized,
/// `url` will be present. This is a convenience method to indicate that
/// state.
///
/// If possible, it's recommended instead to use
/// [`Mastodon::wait_for_processing()`](https://docs.rs/mastodon-async/latest/mastodon_async/mastodon/struct.Mastodon.html#method.wait_for_processing).
pub fn is_done_processing(&self) -> bool {
self.url.is_some()
}
}
/// Wrapper type for a attachment ID string
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(transparent)]
Expand Down Expand Up @@ -99,3 +112,26 @@ pub enum MediaType {
#[serde(rename = "unknown")]
Unknown,
}

/// A media attachment which has been processed and has a URL.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct ProcessedAttachment {
/// ID of the attachment.
pub id: AttachmentId,
/// The media type of an attachment.
#[serde(rename = "type")]
pub media_type: MediaType,
/// URL of the locally hosted version of the image.
pub url: String,
/// For remote images, the remote URL of the original image.
pub remote_url: Option<String>,
/// URL of the preview image.
pub preview_url: String,
/// Shorter URL for the image, for insertion into text
/// (only present on local images)
pub text_url: Option<String>,
/// Meta information about the attachment.
pub meta: Option<Meta>,
/// Noop will be removed.
pub description: Option<String>,
}
6 changes: 5 additions & 1 deletion examples/register/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ pub async fn register() -> Result<Mastodon> {

#[cfg(feature = "toml")]
pub fn read_line(message: impl AsRef<str>) -> Result<String> {
println!("{}", message.as_ref());
use std::io::Write;

print!("{}", message.as_ref());
io::stdout().flush()?;

let mut input = String::new();
io::stdin().read_line(&mut input)?;
Expand All @@ -63,6 +66,7 @@ pub fn read_line(message: impl AsRef<str>) -> Result<String> {
}

#[cfg(feature = "toml")]
#[allow(dead_code)]
pub fn bool_input(message: impl AsRef<str>, default: bool) -> Result<bool> {
let input = read_line(message.as_ref())?;
if let Some(first_char) = input.chars().next() {
Expand Down
6 changes: 5 additions & 1 deletion examples/upload_photo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use mastodon_async::{Result, StatusBuilder, Visibility};
#[cfg(feature = "toml")]
async fn run() -> Result<()> {
use register::bool_input;
femme::with_level(femme::LevelFilter::Trace);
femme::with_level(femme::LevelFilter::Info);
let mastodon = register::get_mastodon_data().await?;
let input = register::read_line("Enter the path to the photo you'd like to post: ")?;
let description = register::read_line("describe the media? ")?;
Expand All @@ -17,6 +17,10 @@ async fn run() -> Result<()> {
};

let media = mastodon.media(input, description).await?;
let media = mastodon
.wait_for_processing(media, Default::default())
.await?;
println!("media upload available at: {}", media.url);
let status = StatusBuilder::new()
.status("Mastodon-async photo upload example/demo (automated post)")
.media_ids([media.id])
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ pub mod scopes;
pub mod status_builder;
#[macro_use]
mod macros;
/// How much time to wait before checking an endpoint again.
pub mod polling_time;
/// Automatically import the things you need
pub mod prelude {
pub use crate::{
Expand Down
63 changes: 61 additions & 2 deletions src/mastodon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ use crate::{
errors::{Error, Result},
event_stream::event_stream,
helpers::read_response::read_response,
log_serde, AddFilterRequest, AddPushRequest, Data, NewStatus, Page, StatusesRequest,
UpdateCredsRequest, UpdatePushRequest,
log_serde,
polling_time::PollingTime,
AddFilterRequest, AddPushRequest, Data, NewStatus, Page, StatusesRequest, UpdateCredsRequest,
UpdatePushRequest,
};
use futures::TryStream;
use log::{as_debug, as_serde, debug, error, trace};
use mastodon_async_entities::attachment::ProcessedAttachment;
use reqwest::{multipart::Part, Client, RequestBuilder};
use url::Url;
use uuid::Uuid;
Expand Down Expand Up @@ -119,6 +122,7 @@ impl Mastodon {
(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,
}

streaming! {
Expand Down Expand Up @@ -326,6 +330,61 @@ impl Mastodon {
self.following(&me.id).await
}

/// Wait for the media to be done processing and return it with the URL.
///
/// `Default::default()` may be passed as the polling time to select a
/// polling time of 500ms.
///
/// ## Example
/// ```rust,no_run
/// use mastodon_async::prelude::*;
/// let mastodon = Mastodon::from(Data::default());
/// tokio_test::block_on(async {
/// let attachment = mastodon.media("/path/to/some/file.jpg", None).await.expect("upload");
/// let attachment = mastodon.wait_for_processing(attachment, Default::default()).await.expect("processing");
/// println!("{}", attachment.url);
/// });
/// ```
///
/// For a different polling time, use `.into()` on a `std::time::Duration`.
/// ```rust,no_run
/// use mastodon_async::prelude::*;
/// use std::time::Duration;
/// let mastodon = Mastodon::from(Data::default());
/// tokio_test::block_on(async {
/// let attachment = mastodon.media("/path/to/some/file.jpg", None).await.expect("upload");
/// let attachment = mastodon.wait_for_processing(
/// attachment,
/// Duration::from_secs(1).into(),
/// ).await.expect("processing");
/// println!("{}", attachment.url);
/// });
/// ```
pub async fn wait_for_processing(
&self,
mut attachment: Attachment,
polling_time: PollingTime,
) -> Result<ProcessedAttachment> {
let id = attachment.id;
loop {
if let Some(url) = attachment.url {
return Ok(ProcessedAttachment {
id,
media_type: attachment.media_type,
url,
remote_url: attachment.remote_url,
preview_url: attachment.preview_url,
text_url: attachment.text_url,
meta: attachment.meta,
description: attachment.description,
});
} else {
attachment = self.attachment(&id).await?;
tokio::time::sleep(*polling_time).await;
}
}
}

/// Set the bearer authentication token
fn authenticated(&self, request: RequestBuilder) -> RequestBuilder {
request.bearer_auth(&self.data.token)
Expand Down
18 changes: 18 additions & 0 deletions src/polling_time.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use derive_deref::Deref;
use std::time::Duration;

/// How long to wait before checking an endpoint again.
#[derive(Debug, Deref, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct PollingTime(Duration);

impl Default for PollingTime {
fn default() -> Self {
Self(Duration::from_millis(500))
}
}

impl From<Duration> for PollingTime {
fn from(value: Duration) -> Self {
Self(value)
}
}