use std::path::Path;
use tokio::fs::File;
use tokio::io::AsyncReadExt;
#[cfg(feature = "http")]
use url::Url;
use crate::all::Message;
#[cfg(feature = "http")]
use crate::error::Error;
use crate::error::Result;
#[cfg(feature = "http")]
use crate::http::Http;
use crate::model::id::AttachmentId;
#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
#[must_use]
pub struct CreateAttachment {
pub(crate) id: u64, pub filename: String,
pub description: Option<String>,
#[serde(skip)]
pub data: Vec<u8>,
}
impl CreateAttachment {
pub fn bytes(data: impl Into<Vec<u8>>, filename: impl Into<String>) -> CreateAttachment {
CreateAttachment {
data: data.into(),
filename: filename.into(),
description: None,
id: 0,
}
}
pub async fn path(path: impl AsRef<Path>) -> Result<CreateAttachment> {
let mut file = File::open(path.as_ref()).await?;
let mut data = Vec::new();
file.read_to_end(&mut data).await?;
let filename = path
.as_ref()
.file_name()
.ok_or_else(|| std::io::Error::other("attachment path must not be a directory"))?;
Ok(CreateAttachment::bytes(data, filename.to_string_lossy().to_string()))
}
pub async fn file(file: &File, filename: impl Into<String>) -> Result<CreateAttachment> {
let mut data = Vec::new();
file.try_clone().await?.read_to_end(&mut data).await?;
Ok(CreateAttachment::bytes(data, filename))
}
#[cfg(feature = "http")]
pub async fn url(http: impl AsRef<Http>, url: &str) -> Result<CreateAttachment> {
let url = Url::parse(url).map_err(|_| Error::Url(url.to_string()))?;
let response = http.as_ref().client.get(url.clone()).send().await?;
let data = response.bytes().await?.to_vec();
let filename = url
.path_segments()
.and_then(Iterator::last)
.ok_or_else(|| Error::Url(url.to_string()))?;
Ok(CreateAttachment::bytes(data, filename))
}
#[must_use]
pub fn to_base64(&self) -> String {
use base64::engine::{Config, Engine};
const PREFIX: &str = "data:image/png;base64,";
let engine = base64::prelude::BASE64_STANDARD;
let encoded_size = base64::encoded_len(self.data.len(), engine.config().encode_padding())
.and_then(|len| len.checked_add(PREFIX.len()))
.expect("buffer capacity overflow");
let mut encoded = String::with_capacity(encoded_size);
encoded.push_str(PREFIX);
engine.encode_string(&self.data, &mut encoded);
encoded
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
}
#[derive(Debug, Clone, serde::Serialize, PartialEq)]
struct ExistingAttachment {
id: AttachmentId,
}
#[derive(Debug, Clone, serde::Serialize, PartialEq)]
#[serde(untagged)]
enum NewOrExisting {
New(CreateAttachment),
Existing(ExistingAttachment),
}
#[derive(Default, Debug, Clone, serde::Serialize, PartialEq)]
#[serde(transparent)]
#[must_use]
pub struct EditAttachments {
new_and_existing_attachments: Vec<NewOrExisting>,
}
impl EditAttachments {
pub fn new() -> Self {
Self::default()
}
pub fn keep_all(msg: &Message) -> Self {
Self {
new_and_existing_attachments: msg
.attachments
.iter()
.map(|a| {
NewOrExisting::Existing(ExistingAttachment {
id: a.id,
})
})
.collect(),
}
}
pub fn keep(mut self, id: AttachmentId) -> Self {
self.new_and_existing_attachments.push(NewOrExisting::Existing(ExistingAttachment {
id,
}));
self
}
pub fn remove(mut self, id: AttachmentId) -> Self {
#[allow(clippy::match_like_matches_macro)] self.new_and_existing_attachments.retain(|a| match a {
NewOrExisting::Existing(a) if a.id == id => false,
_ => true,
});
self
}
#[allow(clippy::should_implement_trait)] pub fn add(mut self, attachment: CreateAttachment) -> Self {
self.new_and_existing_attachments.push(NewOrExisting::New(attachment));
self
}
pub(crate) fn take_files(&mut self) -> Vec<CreateAttachment> {
let mut id_placeholder = 0;
let mut files = Vec::new();
for attachment in &mut self.new_and_existing_attachments {
if let NewOrExisting::New(attachment) = attachment {
let mut cloned_attachment = CreateAttachment::bytes(
std::mem::take(&mut attachment.data),
attachment.filename.clone(),
);
attachment.id = id_placeholder;
cloned_attachment.id = id_placeholder;
files.push(cloned_attachment);
id_placeholder += 1;
}
}
files
}
#[cfg(feature = "cache")]
pub(crate) fn is_empty(&self) -> bool {
self.new_and_existing_attachments.is_empty()
}
}