Skip to content

Commit

Permalink
Add support for adding attachments in message edits (#1378)
Browse files Browse the repository at this point in the history
This commit makes it possible to add new attachments to a message when editing it.

This commit:
- creates a new method `Http::edit_message_and_attachments` (that method is to `edit_message` what `send_files` is to `send_message`);
  - there's a lot of duplication between that new method and `Http::edit_message_and_attachments` and `Http::send_files`, but I don't know enough about serenity's http design to improve the situation;
- changes calls of `Http::edit_message` to `Http::edit_message_and_attachments` where appropriate;
- creates a new method `EditMessage::attachment` to add a new attachment;
- adds a new field to `EditMessage` which stores new attachments (breaking change);
- adds a lifetime parameter to `EditMessage` (breaking change);
- moves the `AttachmentType` serialization into an HTTP form part into a function to avoid code duplication.
  • Loading branch information
kangalio authored and arqunis committed Mar 15, 2022
1 parent 3f985b5 commit 27bf301
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 59 deletions.
13 changes: 11 additions & 2 deletions src/builder/edit_message.rs
Expand Up @@ -3,6 +3,7 @@ use std::collections::HashMap;
use super::{CreateAllowedMentions, CreateEmbed};
#[cfg(feature = "unstable_discord_api")]
use crate::builder::CreateComponents;
use crate::http::AttachmentType;
use crate::internal::prelude::*;
use crate::json::from_number;
use crate::model::channel::MessageFlags;
Expand Down Expand Up @@ -32,9 +33,9 @@ use crate::utils;
///
/// [`Message`]: crate::model::channel::Message
#[derive(Clone, Debug, Default)]
pub struct EditMessage(pub HashMap<&'static str, Value>);
pub struct EditMessage<'a>(pub HashMap<&'static str, Value>, pub Vec<AttachmentType<'a>>);

impl EditMessage {
impl<'a> EditMessage<'a> {
/// Set the content of the message.
///
/// **Note**: Message contents must be under 2000 unicode code points.
Expand Down Expand Up @@ -171,4 +172,12 @@ impl EditMessage {
self.0.insert("flags", from_number(flags.bits));
self
}

/// Add a new attachment for the message.
///
/// This can be called multiple times.
pub fn attachment(&mut self, attachment: impl Into<AttachmentType<'a>>) -> &mut Self {
self.1.push(attachment.into());
self
}
}
99 changes: 49 additions & 50 deletions src/http/client.rs
Expand Up @@ -1435,6 +1435,53 @@ impl Http {
.await
}

/// Edits a message and its attachments by Id.
///
/// **Note**: Only the author of a message can modify it.
pub async fn edit_message_and_attachments(
&self,
channel_id: u64,
message_id: u64,
map: &Value,
new_attachments: impl IntoIterator<Item = AttachmentType<'_>>,
) -> Result<Message> {
// Note: if you need to copy this code for a new method, extract this code into a function
// instead and call it in here and in send_files(), to avoid duplication (see Rule Of Three)

let uri = api!("/channels/{}/messages/{}", channel_id, message_id);
let mut url = Url::parse(&uri).map_err(|_| Error::Url(uri))?;

if let Some(proxy) = &self.proxy {
url.set_host(proxy.host_str()).map_err(HttpError::Url)?;
url.set_scheme(proxy.scheme()).map_err(|_| HttpError::InvalidScheme)?;
url.set_port(proxy.port()).map_err(|_| HttpError::InvalidPort)?;
}

let mut multipart = reqwest::multipart::Form::new();

for (i, attachment) in new_attachments.into_iter().enumerate() {
multipart =
multipart.part(i.to_string(), attachment.into_http_form_part(&self.client).await?);
}

multipart = multipart.text("payload_json", serde_json::to_string(map)?);

let response = self
.client
.patch(url)
.header(AUTHORIZATION, HeaderValue::from_str(&self.token)?)
.header(USER_AGENT, HeaderValue::from_static(&constants::USER_AGENT))
.multipart(multipart)
.send()
.await?;

if !response.status().is_success() {
return Err(HttpError::from_response(response).await.into());
}

response.json::<Message>().await.map_err(From::from)
}

/// Crossposts a message by Id.
///
/// **Note**: Only available on announcements channels.
Expand Down Expand Up @@ -3096,56 +3143,8 @@ impl Http {
let mut multipart = reqwest::multipart::Form::new();

for (file_num, file) in files.into_iter().enumerate() {
match file.into() {
AttachmentType::Bytes {
data,
filename,
} => {
multipart = multipart.part(
file_num.to_string(),
Part::bytes(data.into_owned()).file_name(filename),
);
},
AttachmentType::File {
file,
filename,
} => {
let mut buf = Vec::new();
file.try_clone().await?.read_to_end(&mut buf).await?;

multipart =
multipart.part(file_num.to_string(), Part::stream(buf).file_name(filename));
},
AttachmentType::Path(path) => {
let filename =
path.file_name().map(|filename| filename.to_string_lossy().into_owned());
let mut file = File::open(path).await?;
let mut buf = vec![];
file.read_to_end(&mut buf).await?;

let part = match filename {
Some(filename) => Part::bytes(buf).file_name(filename),
None => Part::bytes(buf),
};

multipart = multipart.part(file_num.to_string(), part);
},
AttachmentType::Image(url) => {
let url = Url::parse(url).map_err(|_| Error::Url(url.to_string()))?;
let filename = url
.path_segments()
.and_then(|segments| segments.last().map(ToString::to_string))
.ok_or_else(|| Error::Url(url.to_string()))?;
let response = self.client.get(url).send().await?;
let mut bytes = response.bytes().await?;
let mut picture: Vec<u8> = vec![0; bytes.len()];
bytes.copy_to_slice(&mut picture[..]);
multipart = multipart.part(
file_num.to_string(),
Part::bytes(picture).file_name(filename.to_string()),
);
},
}
multipart = multipart
.part(file_num.to_string(), file.into().into_http_form_part(&self.client).await?);
}

multipart = multipart.text("payload_json", to_string(&map)?);
Expand Down
52 changes: 52 additions & 0 deletions src/http/mod.rs
Expand Up @@ -201,6 +201,58 @@ pub enum AttachmentType<'a> {
Image(&'a str),
}

impl AttachmentType<'_> {
pub(crate) async fn into_http_form_part(
self,
client: &reqwest::Client,
) -> Result<reqwest::multipart::Part, crate::Error> {
use bytes::buf::Buf;
use tokio::io::AsyncReadExt;

Ok(match self {
AttachmentType::Bytes {
data,
filename,
} => reqwest::multipart::Part::bytes(data.into_owned()).file_name(filename),
AttachmentType::File {
file,
filename,
} => {
let mut buf = Vec::new();
file.try_clone().await?.read_to_end(&mut buf).await?;

reqwest::multipart::Part::stream(buf).file_name(filename)
},
AttachmentType::Path(path) => {
let filename =
path.file_name().map(|filename| filename.to_string_lossy().into_owned());
let mut file = File::open(path).await?;
let mut buf = vec![];
file.read_to_end(&mut buf).await?;

match filename {
Some(filename) => reqwest::multipart::Part::bytes(buf).file_name(filename),
None => reqwest::multipart::Part::bytes(buf),
}
},
AttachmentType::Image(url) => {
let url =
reqwest::Url::parse(url).map_err(|_| crate::Error::Url(url.to_string()))?;
let filename = url
.path_segments()
.and_then(|segments| segments.last().map(ToString::to_string))
.ok_or_else(|| crate::Error::Url(url.to_string()))?;
let response = client.get(url).send().await?;
let mut bytes = response.bytes().await?;
let mut picture: Vec<u8> = vec![0; bytes.len()];
bytes.copy_to_slice(&mut picture[..]);

reqwest::multipart::Part::bytes(picture).file_name(filename.to_string())
},
})
}
}

impl<'a> From<(&'a [u8], &str)> for AttachmentType<'a> {
fn from(params: (&'a [u8], &str)) -> AttachmentType<'a> {
AttachmentType::Bytes {
Expand Down
6 changes: 4 additions & 2 deletions src/model/channel/channel_id.rs
Expand Up @@ -374,7 +374,7 @@ impl ChannelId {
f: F,
) -> Result<Message>
where
F: FnOnce(&mut EditMessage) -> &mut EditMessage,
F: for<'a, 'b> FnOnce(&'a mut EditMessage<'b>) -> &'a mut EditMessage<'b>,
{
let mut msg = EditMessage::default();
f(&mut msg);
Expand All @@ -387,7 +387,9 @@ impl ChannelId {

let map = utils::hashmap_to_json_map(msg.0);

http.as_ref().edit_message(self.0, message_id.into().0, &Value::from(map)).await
http.as_ref()
.edit_message_and_attachments(self.0, message_id.into().0, &Value::from(map), msg.1)
.await
}

/// Attempts to find a [`Channel`] by its Id in the cache.
Expand Down
2 changes: 1 addition & 1 deletion src/model/channel/guild_channel.rs
Expand Up @@ -472,7 +472,7 @@ impl GuildChannel {
f: F,
) -> Result<Message>
where
F: FnOnce(&mut EditMessage) -> &mut EditMessage,
F: for<'a, 'b> FnOnce(&'a mut EditMessage<'b>) -> &'a mut EditMessage<'b>,
{
self.id.edit_message(&http, message_id, f).await
}
Expand Down
13 changes: 10 additions & 3 deletions src/model/channel/message.rs
Expand Up @@ -337,7 +337,7 @@ impl Message {
#[cfg(feature = "utils")]
pub async fn edit<F>(&mut self, cache_http: impl CacheHttp, f: F) -> Result<()>
where
F: FnOnce(&mut EditMessage) -> &mut EditMessage,
F: for<'a, 'b> FnOnce(&'a mut EditMessage<'b>) -> &'a mut EditMessage<'b>,
{
#[cfg(feature = "cache")]
{
Expand All @@ -361,8 +361,15 @@ impl Message {

let map = crate::utils::hashmap_to_json_map(builder.0);

*self =
cache_http.http().edit_message(self.channel_id.0, self.id.0, &Value::from(map)).await?;
*self = cache_http
.http()
.edit_message_and_attachments(
self.channel_id.0,
self.id.0,
&Value::from(map),
builder.1,
)
.await?;

Ok(())
}
Expand Down
2 changes: 1 addition & 1 deletion src/model/channel/private_channel.rs
Expand Up @@ -170,7 +170,7 @@ impl PrivateChannel {
f: F,
) -> Result<Message>
where
F: FnOnce(&mut EditMessage) -> &mut EditMessage,
F: for<'a, 'b> FnOnce(&'a mut EditMessage<'b>) -> &'a mut EditMessage<'b>,
{
self.id.edit_message(&http, message_id, f).await
}
Expand Down

0 comments on commit 27bf301

Please sign in to comment.