forked from MightyPork/elefren-fork
add Dee_'s media builder version
This commit is contained in:
+66
-19
@@ -70,10 +70,11 @@
|
|||||||
unused_qualifications
|
unused_qualifications
|
||||||
)]
|
)]
|
||||||
#![cfg_attr(feature = "nightly", allow(broken_intra_doc_links))]
|
#![cfg_attr(feature = "nightly", allow(broken_intra_doc_links))]
|
||||||
|
#![allow(broken_intra_doc_links)]
|
||||||
|
|
||||||
use std::{borrow::Cow, io::BufRead, ops};
|
use std::{borrow::Cow, io::BufRead, ops};
|
||||||
|
|
||||||
use reqwest::blocking::{Client, RequestBuilder, Response};
|
use reqwest::blocking::{multipart, Client, RequestBuilder, Response};
|
||||||
use tap_reader::Tap;
|
use tap_reader::Tap;
|
||||||
use tungstenite::client::AutoStream;
|
use tungstenite::client::AutoStream;
|
||||||
|
|
||||||
@@ -619,35 +620,81 @@ impl MastodonClient for Mastodon {
|
|||||||
Ok(EventReader(WebSocket(client)))
|
Ok(EventReader(WebSocket(client)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Equivalent to /api/v1/media
|
/// Upload some media to the server for possible attaching to a new status
|
||||||
fn media(&self, media_builder: MediaBuilder) -> Result<Attachment> {
|
///
|
||||||
use reqwest::blocking::multipart::Form;
|
/// Upon successful upload of a media attachment, the server will assign it an id. To actually
|
||||||
|
/// use the attachment in a new status, you can use the `media_ids` field of
|
||||||
|
/// [`StatusBuilder`]
|
||||||
|
///
|
||||||
|
/// There are two ways of providing the data to be attached: by reading a file, or by using a
|
||||||
|
/// reader.
|
||||||
|
///
|
||||||
|
/// ## Files
|
||||||
|
/// If the `MediaBuilder` was supplied with a file path, the filename and mimetype will be
|
||||||
|
/// automatically populated from that file; their values set in the `MediaBuilder` will be
|
||||||
|
/// ignored. For example:
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// let client = Mastodon::from(data);
|
||||||
|
/// let builder = crate::MediaBuilder::from_file("/tmp/my_image.png".into());
|
||||||
|
///
|
||||||
|
/// let attachment = client.media(builder);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## Readers
|
||||||
|
/// The `MediaBuilder` can also be supplied with a reader. This is useful for uploading data
|
||||||
|
/// already in memory, for example from a `Vec<u8>` containing some image data. For example:
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use std::io::Cursor;
|
||||||
|
/// let client = Mastodon::from(data);
|
||||||
|
///
|
||||||
|
/// let mut image_data: Vec<u8> = Vec::new();
|
||||||
|
/// populate_image_data(&mut image_data);
|
||||||
|
///
|
||||||
|
/// let builder = crate::MediaBuilder::from_reader(Cursor::new(image_data));
|
||||||
|
/// let attachment = client.media(builder);
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## Errors
|
||||||
|
/// This function may return an `Error::Http` before sending anything over the network if the
|
||||||
|
/// `MediaBuilder` was supplied with a reader and a `mimetype` string which cannot be pasrsed.
|
||||||
|
fn media(&self, media: MediaBuilder) -> Result<Attachment> {
|
||||||
|
use media_builder::MediaBuilderData;
|
||||||
|
|
||||||
let mut form_data = Form::new().file("file", media_builder.file.as_ref())?;
|
let mut form = multipart::Form::new();
|
||||||
|
form = match media.data {
|
||||||
|
MediaBuilderData::Reader(reader) => {
|
||||||
|
let mut part = multipart::Part::reader(reader);
|
||||||
|
|
||||||
if let Some(description) = media_builder.description {
|
if let Some(filename) = media.filename {
|
||||||
form_data = form_data.text("description", description);
|
part = part.file_name(filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(focus) = media_builder.focus {
|
if let Some(mimetype) = media.mimetype {
|
||||||
let string = format!("{},{}", focus.0, focus.1);
|
part = part.mime_str(&mimetype)?;
|
||||||
form_data = form_data.text("focus", string);
|
}
|
||||||
|
|
||||||
|
form.part("file", part)
|
||||||
|
}
|
||||||
|
MediaBuilderData::File(file) => form.file("file", &file)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(description) = media.description {
|
||||||
|
form = form.text("description", description);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((x, y)) = media.focus {
|
||||||
|
form = form.text("focus", format!("{},{}", x, y));
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = self.send(
|
let response = self.send(
|
||||||
self.client
|
self.client
|
||||||
.post(&self.route("/api/v1/media"))
|
.post(&self.route("/api/v1/media"))
|
||||||
.multipart(form_data),
|
.multipart(form),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let status = response.status();
|
|
||||||
|
|
||||||
if status.is_client_error() {
|
|
||||||
return Err(Error::Client(status));
|
|
||||||
} else if status.is_server_error() {
|
|
||||||
return Err(Error::Server(status));
|
|
||||||
}
|
|
||||||
|
|
||||||
deserialise(response)
|
deserialise(response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+124
-48
@@ -1,72 +1,148 @@
|
|||||||
use serde::Serialize;
|
use std::fmt;
|
||||||
use std::borrow::Cow;
|
use std::io::Read;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
/// A builder pattern struct for constructing a media attachment.
|
#[derive(Debug)]
|
||||||
#[derive(Debug, Default, Clone, Serialize)]
|
/// A builder pattern struct for preparing a single attachment for upload.
|
||||||
|
///
|
||||||
|
/// For more details, see [`new_media()`](struct.Mastodon.html#method.new_media).
|
||||||
pub struct MediaBuilder {
|
pub struct MediaBuilder {
|
||||||
/// The file name of the attachment to be uploaded.
|
/// The media attachment itself
|
||||||
pub file: Cow<'static, str>,
|
pub data: MediaBuilderData,
|
||||||
/// The alt text of the attachment.
|
|
||||||
pub description: Option<Cow<'static, str>>,
|
/// The filename to send to the server
|
||||||
/// The focus point for images.
|
pub filename: Option<String>,
|
||||||
pub focus: Option<(f32, f32)>,
|
|
||||||
|
/// Mimetype to send to the server, identifying what is in the attachment.
|
||||||
|
///
|
||||||
|
/// The string should be a valid mimetype.
|
||||||
|
pub mimetype: Option<String>,
|
||||||
|
|
||||||
|
/// Plain text description of the attached piece of media, for accessibility
|
||||||
|
pub description: Option<String>,
|
||||||
|
|
||||||
|
/// (x, y) focus point, used by clients to determine how to crop an image
|
||||||
|
pub focus: Option<(f64, f64)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enum representing possible sources of attachments to upload
|
||||||
|
pub enum MediaBuilderData {
|
||||||
|
/// An arbitrary reader. It is useful for reading from media already in memory.
|
||||||
|
Reader(Box<dyn Read + Send>),
|
||||||
|
|
||||||
|
/// Variant represening a file path of the file to attach.
|
||||||
|
File(PathBuf),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for MediaBuilderData {
|
||||||
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
MediaBuilderData::File(f) => fmt.debug_tuple("File").field(&f).finish(),
|
||||||
|
MediaBuilderData::Reader(_) => fmt
|
||||||
|
.debug_tuple("Reader")
|
||||||
|
.field(&format_args!("..."))
|
||||||
|
.finish(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MediaBuilder {
|
impl MediaBuilder {
|
||||||
/// Create a new attachment from a file name.
|
/// Create a new MediaBuilder from a reader `data`
|
||||||
pub fn new(file: Cow<'static, str>) -> Self {
|
pub fn from_reader<R: Read + Send + 'static>(data: R) -> MediaBuilder {
|
||||||
MediaBuilder {
|
MediaBuilder {
|
||||||
file,
|
data: MediaBuilderData::Reader(Box::from(data)),
|
||||||
|
filename: None,
|
||||||
|
mimetype: None,
|
||||||
description: None,
|
description: None,
|
||||||
focus: None,
|
focus: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new MediaBuilder from a file under `path`
|
||||||
|
///
|
||||||
|
/// This function will not check whether the file exists or if it can be read. If the path is
|
||||||
|
/// not valid, [`add_media()`](trait.MastodonClient.html#method.add_media) will return an error when called with the `MediaBuilder`.
|
||||||
|
pub fn from_file(path: impl AsRef<Path>) -> MediaBuilder {
|
||||||
|
let pb = path.as_ref().to_owned();
|
||||||
|
let filename = pb
|
||||||
|
.file_name()
|
||||||
|
.expect("file name")
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
let mimetype = match pb.extension().map(|s| s.to_str()).flatten() {
|
||||||
|
Some("jpg") | Some("jpeg") => Some("image/jpeg".to_string()),
|
||||||
|
Some("png") => Some("image/png".to_string()),
|
||||||
|
Some("gif") => Some("image/gif".to_string()),
|
||||||
|
Some("txt") => Some("text/plain".to_string()),
|
||||||
|
// ...
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
MediaBuilder {
|
||||||
|
data: MediaBuilderData::File(pb),
|
||||||
|
filename: Some(filename),
|
||||||
|
mimetype,
|
||||||
|
description: None,
|
||||||
|
focus: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set filename
|
||||||
|
pub fn filename(&mut self, filename: impl ToString) {
|
||||||
|
self.filename = Some(filename.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set custom mime type
|
||||||
|
pub fn mimetype(&mut self, mimetype: impl ToString) {
|
||||||
|
self.mimetype = Some(mimetype.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
/// Set an alt text description for the attachment.
|
/// Set an alt text description for the attachment.
|
||||||
pub fn description(mut self, description: Cow<'static, str>) -> Self {
|
pub fn description(&mut self, description: impl ToString) {
|
||||||
self.description = Some(description);
|
self.description = Some(description.to_string());
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set a focus point for an image attachment.
|
/// Set a focus point for an image attachment.
|
||||||
pub fn focus(mut self, f1: f32, f2: f32) -> Self {
|
pub fn focus(&mut self, f1: f64, f2: f64) {
|
||||||
self.focus = Some((f1, f2));
|
self.focus = Some((f1, f2));
|
||||||
self
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convenience helper so that the mastodon.media() method can be called with a
|
#[cfg(test)]
|
||||||
// file name only (owned string).
|
mod tests {
|
||||||
impl From<String> for MediaBuilder {
|
use super::*;
|
||||||
fn from(file: String) -> MediaBuilder {
|
use std::io::Cursor;
|
||||||
MediaBuilder {
|
|
||||||
file: file.into(),
|
|
||||||
description: None,
|
|
||||||
focus: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convenience helper so that the mastodon.media() method can be called with a
|
#[test]
|
||||||
// file name only (borrowed string).
|
fn test_from_reader() {
|
||||||
impl From<&'static str> for MediaBuilder {
|
let source = vec![0u8, 1, 2];
|
||||||
fn from(file: &'static str) -> MediaBuilder {
|
let builder = MediaBuilder::from_reader(Cursor::new(source.clone()));
|
||||||
MediaBuilder {
|
|
||||||
file: file.into(),
|
|
||||||
description: None,
|
|
||||||
focus: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convenience helper so that the mastodon.media() method can be called with a
|
assert_eq!(builder.filename, None);
|
||||||
// file name only (Cow string).
|
assert_eq!(builder.mimetype, None);
|
||||||
impl From<Cow<'static, str>> for MediaBuilder {
|
assert_eq!(builder.description, None);
|
||||||
fn from(file: Cow<'static, str>) -> MediaBuilder {
|
assert_eq!(builder.focus, None);
|
||||||
MediaBuilder {
|
|
||||||
file,
|
if let MediaBuilderData::Reader(r) = builder.data {
|
||||||
description: None,
|
assert_eq!(r.bytes().map(|b| b.unwrap()).collect::<Vec<u8>>(), source);
|
||||||
focus: None,
|
} else {
|
||||||
|
panic!("Unable to destructure MediaBuilder.data into a reader");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_file() {
|
||||||
|
let builder = MediaBuilder::from_file("/fake/file/path.png".into());
|
||||||
|
|
||||||
|
assert_eq!(builder.filename, None);
|
||||||
|
assert_eq!(builder.mimetype, None);
|
||||||
|
assert_eq!(builder.description, None);
|
||||||
|
assert_eq!(builder.focus, None);
|
||||||
|
|
||||||
|
if let MediaBuilderData::File(f) = builder.data {
|
||||||
|
assert_eq!(f, "/fake/file/path.png");
|
||||||
|
} else {
|
||||||
|
panic!("Unable to destructure MediaBuilder.data into a filepath");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user