Skip to content

Commit b6d81d9

Browse files
authored
Rename ImageData to DataUri and allow setting its mimetype (#3412)
Back in September 2024, Discord added [Soundboard][soundboard] APIs that allow a bot to create a new sound by uploading raw mp3 or ogg data. This data is specified as part of the JSON rather than a multipart upload (an odd decision IMO); however, that means that the semantics of `ImageData` are wrong as it makes too many assumptions about its contents. In particular, we assume that the base64 data will always be an image, and the `CreateAttachment::encode` method always assigns the `image/png` mimetype. This commit removes these assumptions by renaming the struct to simply `DataUri`, and now requiring the user to specify the mimetype when calling `CreateAttachment::encode`. [soundboard]: https://discord.com/developers/docs/resources/soundboard#soundboard-resource
1 parent 1d01e47 commit b6d81d9

File tree

12 files changed

+49
-49
lines changed

12 files changed

+49
-49
lines changed

src/builder/create_attachment.rs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -115,21 +115,21 @@ impl<'a> CreateAttachment<'a> {
115115
/// # Errors
116116
///
117117
/// See [`CreateAttachment::get_data`] for details.
118-
pub async fn encode(&self) -> Result<ImageData<'_>> {
118+
pub async fn encode(&self, mimetype: &str) -> Result<DataUri<'_>> {
119119
use base64::engine::{Config, Engine};
120120

121-
const PREFIX: &str = "data:image/png;base64,";
121+
let prefix = format!("data:{mimetype};base64,");
122122
let data = self.get_data().await?;
123123

124124
let engine = base64::prelude::BASE64_STANDARD;
125125
let encoded_size = base64::encoded_len(data.len(), engine.config().encode_padding())
126-
.and_then(|len| len.checked_add(PREFIX.len()))
126+
.and_then(|len| len.checked_add(prefix.len()))
127127
.expect("buffer capacity overflow");
128128

129129
let mut encoded = String::with_capacity(encoded_size);
130-
encoded.push_str(PREFIX);
130+
encoded.push_str(&prefix);
131131
engine.encode_string(&data, &mut encoded);
132-
Ok(ImageData(encoded.into()))
132+
Ok(DataUri(encoded.into()))
133133
}
134134

135135
/// Sets a description for the file (max 1024 characters).
@@ -139,27 +139,27 @@ impl<'a> CreateAttachment<'a> {
139139
}
140140
}
141141

142-
/// A wrapper around some base64-encoded image data. Used when an endpoint expects the image
143-
/// payload directly as part of the JSON body, instead of as a multipart upload.
142+
/// A wrapper around some base64-encoded data. Used when an endpoint expects a base64 payload
143+
/// directly as part of the JSON body, instead of as a multipart upload.
144144
#[derive(Clone, Debug, Serialize)]
145145
#[serde(transparent)]
146-
pub struct ImageData<'a>(Cow<'a, str>);
146+
pub struct DataUri<'a>(Cow<'a, str>);
147147

148-
impl<'a> ImageData<'a> {
149-
/// Accesses the stored base64-encoded image data.
148+
impl<'a> DataUri<'a> {
149+
/// Accesses the stored base64-encoded data.
150150
#[must_use]
151151
pub fn as_base64(&self) -> &str {
152152
&self.0
153153
}
154154

155-
/// Constructs image data from a base64-encoded blob of data. The string must be a valid data
155+
/// Constructs a [`DataUri`] from a base64-encoded blob of data. The string must be a valid data
156156
/// URI, for example:
157157
///
158158
/// ```
159-
/// use serenity::builder::ImageData;
159+
/// use serenity::builder::DataUri;
160160
///
161161
/// let s = "data:image/png;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=";
162-
/// assert!(ImageData::from_base64(s).is_ok());
162+
/// assert!(DataUri::from_base64(s).is_ok());
163163
/// ```
164164
///
165165
/// # Errors

src/builder/create_scheduled_event.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::borrow::Cow;
22

3-
use super::ImageData;
3+
use super::DataUri;
44
#[cfg(feature = "http")]
55
use crate::http::Http;
66
#[cfg(feature = "http")]
@@ -24,7 +24,7 @@ pub struct CreateScheduledEvent<'a> {
2424
description: Option<Cow<'a, str>>,
2525
entity_type: ScheduledEventType,
2626
#[serde(skip_serializing_if = "Option::is_none")]
27-
image: Option<ImageData<'a>>,
27+
image: Option<DataUri<'a>>,
2828

2929
#[serde(skip)]
3030
audit_log_reason: Option<&'a str>,
@@ -110,7 +110,7 @@ impl<'a> CreateScheduledEvent<'a> {
110110
}
111111

112112
/// Sets the cover image for the scheduled event.
113-
pub fn image(mut self, image: ImageData<'a>) -> Self {
113+
pub fn image(mut self, image: DataUri<'a>) -> Self {
114114
self.image = Some(image);
115115
self
116116
}

src/builder/create_soundboard.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::ImageData;
1+
use super::DataUri;
22
#[cfg(feature = "http")]
33
use crate::http::CacheHttp;
44
#[cfg(feature = "http")]
@@ -12,7 +12,7 @@ use crate::model::prelude::*;
1212
#[must_use]
1313
pub struct CreateSoundboard<'a> {
1414
name: String,
15-
sound: ImageData<'a>,
15+
sound: DataUri<'a>,
1616
volume: f64,
1717
#[serde(skip_serializing_if = "Option::is_none")]
1818
emoji_id: Option<EmojiId>,
@@ -25,7 +25,7 @@ pub struct CreateSoundboard<'a> {
2525

2626
impl<'a> CreateSoundboard<'a> {
2727
/// Creates a new builder with the given data.
28-
pub fn new(name: impl Into<String>, sound: ImageData<'a>) -> Self {
28+
pub fn new(name: impl Into<String>, sound: DataUri<'a>) -> Self {
2929
Self {
3030
name: name.into(),
3131
sound,
@@ -48,7 +48,7 @@ impl<'a> CreateSoundboard<'a> {
4848
///
4949
/// **Note**: Must be audio that is encoded in MP3 or OGG, max 512 KB, max
5050
/// duration 5.2 seconds.
51-
pub fn sound(mut self, sound: ImageData<'a>) -> Self {
51+
pub fn sound(mut self, sound: DataUri<'a>) -> Self {
5252
self.sound = sound;
5353
self
5454
}

src/builder/create_webhook.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::borrow::Cow;
22

3-
use super::ImageData;
3+
use super::DataUri;
44
#[cfg(feature = "http")]
55
use crate::http::Http;
66
#[cfg(feature = "http")]
@@ -14,7 +14,7 @@ use crate::model::prelude::*;
1414
pub struct CreateWebhook<'a> {
1515
name: Cow<'a, str>,
1616
#[serde(skip_serializing_if = "Option::is_none")]
17-
avatar: Option<ImageData<'a>>,
17+
avatar: Option<DataUri<'a>>,
1818

1919
#[serde(skip)]
2020
audit_log_reason: Option<&'a str>,
@@ -39,7 +39,7 @@ impl<'a> CreateWebhook<'a> {
3939
}
4040

4141
/// Set the webhook's default avatar.
42-
pub fn avatar(mut self, avatar: ImageData<'a>) -> Self {
42+
pub fn avatar(mut self, avatar: DataUri<'a>) -> Self {
4343
self.avatar = Some(avatar);
4444
self
4545
}

src/builder/edit_guild.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::borrow::Cow;
22

3-
use super::ImageData;
3+
use super::DataUri;
44
#[cfg(feature = "http")]
55
use crate::http::Http;
66
#[cfg(feature = "http")]
@@ -27,15 +27,15 @@ pub struct EditGuild<'a> {
2727
#[serde(skip_serializing_if = "Option::is_none")]
2828
afk_timeout: Option<AfkTimeout>,
2929
#[serde(skip_serializing_if = "Option::is_none")]
30-
icon: Option<Option<ImageData<'a>>>,
30+
icon: Option<Option<DataUri<'a>>>,
3131
#[serde(skip_serializing_if = "Option::is_none")]
3232
owner_id: Option<UserId>,
3333
#[serde(skip_serializing_if = "Option::is_none")]
3434
splash: Option<Option<Cow<'a, str>>>,
3535
#[serde(skip_serializing_if = "Option::is_none")]
3636
discovery_splash: Option<Option<Cow<'a, str>>>,
3737
#[serde(skip_serializing_if = "Option::is_none")]
38-
banner: Option<Option<ImageData<'a>>>,
38+
banner: Option<Option<DataUri<'a>>>,
3939
#[serde(skip_serializing_if = "Option::is_none")]
4040
system_channel_id: Option<Option<ChannelId>>,
4141
#[serde(skip_serializing_if = "Option::is_none")]
@@ -91,7 +91,7 @@ impl<'a> EditGuild<'a> {
9191
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
9292
/// # let http: Http = unimplemented!();
9393
/// # let mut guild = GuildId::new(1).to_partial_guild(&http).await?;
94-
/// let icon = CreateAttachment::path("./guild_icon.png".as_ref())?.encode().await?;
94+
/// let icon = CreateAttachment::path("./guild_icon.png".as_ref())?.encode("image/png").await?;
9595
///
9696
/// // assuming a `guild` has already been bound
9797
/// let builder = EditGuild::new().icon(Some(icon));
@@ -101,7 +101,7 @@ impl<'a> EditGuild<'a> {
101101
/// ```
102102
///
103103
/// [`CreateAttachment`]: super::CreateAttachment
104-
pub fn icon(mut self, icon: Option<ImageData<'a>>) -> Self {
104+
pub fn icon(mut self, icon: Option<DataUri<'a>>) -> Self {
105105
self.icon = Some(icon);
106106
self
107107
}
@@ -184,7 +184,7 @@ impl<'a> EditGuild<'a> {
184184
/// guild's [`features`] list.
185185
///
186186
/// [`features`]: Guild::features
187-
pub fn banner(mut self, banner: Option<ImageData<'a>>) -> Self {
187+
pub fn banner(mut self, banner: Option<DataUri<'a>>) -> Self {
188188
self.banner = Some(banner);
189189
self
190190
}

src/builder/edit_profile.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::borrow::Cow;
22

3-
use super::ImageData;
3+
use super::DataUri;
44
#[cfg(feature = "http")]
55
use crate::http::Http;
66
#[cfg(feature = "http")]
@@ -18,9 +18,9 @@ pub struct EditProfile<'a> {
1818
#[serde(skip_serializing_if = "Option::is_none")]
1919
username: Option<Cow<'a, str>>,
2020
#[serde(skip_serializing_if = "Option::is_none")]
21-
avatar: Option<Option<ImageData<'a>>>,
21+
avatar: Option<Option<DataUri<'a>>>,
2222
#[serde(skip_serializing_if = "Option::is_none")]
23-
banner: Option<Option<ImageData<'a>>>,
23+
banner: Option<Option<DataUri<'a>>>,
2424
}
2525

2626
impl<'a> EditProfile<'a> {
@@ -43,13 +43,13 @@ impl<'a> EditProfile<'a> {
4343
/// # async fn run() -> Result<(), SerenityError> {
4444
/// # let http: Http = unimplemented!();
4545
/// # let mut user = CurrentUser::default();
46-
/// let avatar = CreateAttachment::path("./my_image.jpg".as_ref())?.encode().await?;
46+
/// let avatar = CreateAttachment::path("./my_image.jpg".as_ref())?.encode("image/jpeg").await?;
4747
/// let builder = EditProfile::new().avatar(avatar);
4848
/// user.edit(&http, builder).await?;
4949
/// # Ok(())
5050
/// # }
5151
/// ```
52-
pub fn avatar(mut self, avatar: ImageData<'a>) -> Self {
52+
pub fn avatar(mut self, avatar: DataUri<'a>) -> Self {
5353
self.avatar = Some(Some(avatar));
5454
self
5555
}
@@ -71,7 +71,7 @@ impl<'a> EditProfile<'a> {
7171
}
7272

7373
/// Sets the banner of the current user.
74-
pub fn banner(mut self, banner: ImageData<'a>) -> Self {
74+
pub fn banner(mut self, banner: DataUri<'a>) -> Self {
7575
self.banner = Some(Some(banner));
7676
self
7777
}

src/builder/edit_role.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::borrow::Cow;
22

3-
use super::ImageData;
3+
use super::DataUri;
44
#[cfg(feature = "http")]
55
use crate::http::Http;
66
use crate::model::prelude::*;
@@ -50,7 +50,7 @@ pub struct EditRole<'a> {
5050
#[serde(skip_serializing_if = "Option::is_none")]
5151
hoist: Option<bool>,
5252
#[serde(skip_serializing_if = "Option::is_none")]
53-
icon: Option<Option<ImageData<'a>>>,
53+
icon: Option<Option<DataUri<'a>>>,
5454
#[serde(skip_serializing_if = "Option::is_none")]
5555
unicode_emoji: Option<Option<Cow<'a, str>>>,
5656

@@ -131,7 +131,7 @@ impl<'a> EditRole<'a> {
131131
}
132132

133133
/// Set the role icon to a custom image.
134-
pub fn icon(mut self, icon: Option<ImageData<'a>>) -> Self {
134+
pub fn icon(mut self, icon: Option<DataUri<'a>>) -> Self {
135135
self.icon = Some(icon);
136136
self.unicode_emoji = Some(None);
137137
self

src/builder/edit_scheduled_event.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::borrow::Cow;
22

3-
use super::{CreateScheduledEventMetadata, ImageData};
3+
use super::{CreateScheduledEventMetadata, DataUri};
44
#[cfg(feature = "http")]
55
use crate::http::Http;
66
#[cfg(feature = "http")]
@@ -30,7 +30,7 @@ pub struct EditScheduledEvent<'a> {
3030
#[serde(skip_serializing_if = "Option::is_none")]
3131
status: Option<ScheduledEventStatus>,
3232
#[serde(skip_serializing_if = "Option::is_none")]
33-
image: Option<ImageData<'a>>,
33+
image: Option<DataUri<'a>>,
3434

3535
#[serde(skip)]
3636
audit_log_reason: Option<&'a str>,
@@ -152,7 +152,7 @@ impl<'a> EditScheduledEvent<'a> {
152152
}
153153

154154
/// Sets the cover image for the scheduled event.
155-
pub fn image(mut self, image: ImageData<'a>) -> Self {
155+
pub fn image(mut self, image: DataUri<'a>) -> Self {
156156
self.image = Some(image);
157157
self
158158
}

src/builder/edit_webhook.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::borrow::Cow;
22

3-
use super::ImageData;
3+
use super::DataUri;
44
#[cfg(feature = "http")]
55
use crate::http::Http;
66
#[cfg(feature = "http")]
@@ -14,7 +14,7 @@ pub struct EditWebhook<'a> {
1414
#[serde(skip_serializing_if = "Option::is_none")]
1515
name: Option<Cow<'a, str>>,
1616
#[serde(skip_serializing_if = "Option::is_none")]
17-
avatar: Option<Option<ImageData<'a>>>,
17+
avatar: Option<Option<DataUri<'a>>>,
1818
#[serde(skip_serializing_if = "Option::is_none")]
1919
channel_id: Option<ChannelId>,
2020

@@ -43,7 +43,7 @@ impl<'a> EditWebhook<'a> {
4343
}
4444

4545
/// Set the webhook's default avatar.
46-
pub fn avatar(mut self, avatar: ImageData<'a>) -> Self {
46+
pub fn avatar(mut self, avatar: DataUri<'a>) -> Self {
4747
self.avatar = Some(Some(avatar));
4848
self
4949
}

src/model/guild/guild_id.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use crate::builder::{
1313
CreateScheduledEvent,
1414
CreateSoundboard,
1515
CreateSticker,
16+
DataUri,
1617
EditAutoModRule,
1718
EditCommand,
1819
EditCommandPermissions,
@@ -24,7 +25,6 @@ use crate::builder::{
2425
EditScheduledEvent,
2526
EditSoundboard,
2627
EditSticker,
27-
ImageData,
2828
};
2929
#[cfg(all(feature = "cache", feature = "model"))]
3030
use crate::cache::{Cache, GuildRef};
@@ -363,14 +363,14 @@ impl GuildId {
363363
self,
364364
http: &Http,
365365
name: &str,
366-
image: ImageData<'_>,
366+
image: DataUri<'_>,
367367
roles: Option<Vec<RoleId>>,
368368
reason: Option<&str>,
369369
) -> Result<Emoji> {
370370
#[derive(serde::Serialize)]
371371
struct CreateEmoji<'a> {
372372
name: &'a str,
373-
image: ImageData<'a>,
373+
image: DataUri<'a>,
374374
#[serde(skip_serializing_if = "Option::is_none")]
375375
roles: Option<Vec<RoleId>>,
376376
}

0 commit comments

Comments
 (0)