Skip to content

Commit

Permalink
Implement voice messages (serenity-rs#2392)
Browse files Browse the repository at this point in the history
No breaking changes.

Newly added:
- `Attachment` fields: `duration_secs`, `waveform`
- `MessageFlags` bitflag: `IS_VOICE_MESSAGE`
- `model::Error` variant: `CannotEditVoiceMessage`
- `Permissions` bitflag: `SEND_VOICE_MESSAGES`

discord/discord-api-docs#6082
  • Loading branch information
kangalio authored and arqunis committed Oct 24, 2023
1 parent ceb85f3 commit 6ce660c
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 3 deletions.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ url = { version = "^2.1", features = ["serde"] }
tokio = { version = "1", features = ["fs", "macros", "rt", "sync", "time", "io-util"] }
futures = { version = "0.3", default-features = false, features = ["std"] }
dep_time = { version = "0.3.20", package = "time", features = ["formatting", "parsing", "serde-well-known"] }
base64 = { version = "0.21" }
# Optional dependencies
fxhash = { version = "0.2.1", optional = true }
simd-json = { version = "0.7", optional = true }
uwl = { version = "0.6.0", optional = true }
base64 = { version = "0.21", optional = true }
levenshtein = { version = "1.0.5", optional = true }
chrono = { version = "0.4.22", default-features = false, features = ["clock", "serde"], optional = true }
flate2 = { version = "1.0.13", optional = true }
Expand Down Expand Up @@ -76,7 +76,7 @@ default_no_backend = [

# Enables builder structs to configure Discord HTTP requests. Without this feature, you have to
# construct JSON manually at some places.
builder = ["base64"]
builder = []
# Enables the cache, which stores the data received from Discord gateway to provide access to
# complete guild data, channels, users and more without needing HTTP requests.
cache = ["fxhash", "dashmap", "parking_lot"]
Expand Down
16 changes: 16 additions & 0 deletions examples/testing/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,22 @@ async fn message(ctx: &Context, msg: Message) -> Result<(), serenity::Error> {
});
let _ = tokio::time::timeout(Duration::from_millis(2000), message_updates.next()).await;
msg.edit(&ctx, EditMessage::new().suppress_embeds(true)).await?;
} else if msg.content == "voicemessage" {
let audio_url =
"https://upload.wikimedia.org/wikipedia/commons/8/81/Short_Silent%2C_Empty_Audio.ogg";
// As of 2023-04-20, bots are still not allowed to sending voice messages
msg.author
.id
.create_dm_channel(ctx)
.await?
.id
.send_message(
ctx,
CreateMessage::new()
.flags(MessageFlags::IS_VOICE_MESSAGE)
.add_file(CreateAttachment::url(ctx, audio_url).await?),
)
.await?;
} else {
return Ok(());
}
Expand Down
32 changes: 31 additions & 1 deletion src/model/channel/attachment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,26 @@ use reqwest::Client as ReqwestClient;

#[cfg(feature = "model")]
use crate::internal::prelude::*;
use crate::model::id::AttachmentId;
use crate::model::prelude::*;
use crate::model::utils::is_false;

fn base64_bytes<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>
where
D: serde::Deserializer<'de>,
{
use base64::Engine as _;
use serde::de::Error;

let base64 = <Option<String>>::deserialize(deserializer)?;
let bytes = match base64 {
Some(base64) => {
Some(base64::prelude::BASE64_STANDARD.decode(base64).map_err(D::Error::custom)?)
},
None => None,
};
Ok(bytes)
}

/// A file uploaded with a message. Not to be confused with [`Embed`]s.
///
/// [Discord docs](https://discord.com/developers/docs/resources/channel#attachment-object).
Expand Down Expand Up @@ -41,6 +58,19 @@ pub struct Attachment {
/// itself exists.
#[serde(default, skip_serializing_if = "is_false")]
pub ephemeral: bool,
/// The duration of the audio file (present if [`MessageFlags::IS_VOICE_MESSAGE`]).
pub duration_secs: Option<f64>,
/// List of bytes representing a sampled waveform (present if
/// [`MessageFlags::IS_VOICE_MESSAGE`]).
///
/// The waveform is intended to be a preview of the entire voice message, with 1 byte per
/// datapoint. Clients sample the recording at most once per 100 milliseconds, but will
/// downsample so that no more than 256 datapoints are in the waveform.
///
/// The waveform details are a Discord implementation detail and may change without warning or
/// documentation.
#[serde(deserialize_with = "base64_bytes")]
pub waveform: Option<Vec<u8>>,
}

#[cfg(feature = "model")]
Expand Down
18 changes: 18 additions & 0 deletions src/model/channel/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,11 @@ impl Message {
}
}
}
if let Some(flags) = self.flags {
if flags.contains(MessageFlags::IS_VOICE_MESSAGE) {
return Err(Error::Model(ModelError::CannotEditVoiceMessage));
}
}

*self = builder.execute(cache_http, (self.channel_id, self.id)).await?;
Ok(())
Expand Down Expand Up @@ -1039,6 +1044,19 @@ bitflags! {
const FAILED_TO_MENTION_SOME_ROLES_IN_THREAD = 1 << 8;
/// This message will not trigger push and desktop notifications.
const SUPPRESS_NOTIFICATIONS = 1 << 12;
/// This message is a voice message.
///
/// Voice messages have the following properties:
/// - They cannot be edited.
/// - Only a single audio attachment is allowed. No content, stickers, etc...
/// - The [`Attachment`] has additional fields: `duration_secs` and `waveform`.
///
/// As of 2023-04-14, clients upload a 1 channel, 48000 Hz, 32kbps Opus stream in an OGG container.
/// The encoding is a Discord implementation detail and may change without warning or documentation.
///
/// As of 2023-04-20, bots are currently not able to send voice messages
/// ([source](https://github.com/discord/discord-api-docs/pull/6082)).
const IS_VOICE_MESSAGE = 1 << 13;
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/model/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ pub enum Error {
NoStickerFileSet,
/// When attempting to send a message with over 3 stickers.
StickerAmount,
/// When attempting to edit a voice message.
CannotEditVoiceMessage,
}

impl Error {
Expand Down Expand Up @@ -205,6 +207,7 @@ impl fmt::Display for Error {
Self::DeleteNitroSticker => f.write_str("Cannot delete an official sticker."),
Self::NoStickerFileSet => f.write_str("Sticker file is not set."),
Self::StickerAmount => f.write_str("Too many stickers in a message."),
Self::CannotEditVoiceMessage => f.write_str("Cannot edit voice message."),
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/model/permissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,9 @@ bitflags::bitflags! {
/// Allows for timing out users to prevent them from sending or reacting to messages in
/// chat and threads, and from speaking in voice and stage channels.
const MODERATE_MEMBERS = 1 << 40;
// MISSING: VIEW_CREATOR_MONETIZATION_ANALYTICS (1 << 41), USE_SOUNDBOARD (1 << 42)
/// Allows sending voice messages.
const SEND_VOICE_MESSAGES = 1 << 46;
}
}

Expand Down

0 comments on commit 6ce660c

Please sign in to comment.