Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support sending voice messages #2738

Merged
merged 3 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/main/java/net/dv8tion/jda/api/entities/Message.java
Original file line number Diff line number Diff line change
Expand Up @@ -2253,6 +2253,13 @@ default MessageCreateAction replyFiles(@Nonnull Collection<? extends FileUpload>
*/
boolean isSuppressedNotifications();

/**
* Whether this message is a voice message.
*
* @return True, if this is a voice message
*/
boolean isVoiceMessage();

/**
* Returns a possibly {@code null} {@link ThreadChannel ThreadChannel} that was started from this message.
* This can be {@code null} due to no ThreadChannel being started from it or the ThreadChannel later being deleted.
Expand Down
81 changes: 79 additions & 2 deletions src/main/java/net/dv8tion/jda/api/utils/FileUpload.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Base64;
import java.util.function.Supplier;

/**
Expand All @@ -51,6 +54,9 @@ public class FileUpload implements Closeable, AttachedFile
private String name;
private TypedBody<?> body;
private String description;
private MediaType mediaType = Requester.MEDIA_TYPE_OCTET;
private byte[] waveform;
private double durationSeconds;

protected FileUpload(InputStream resource, String name)
{
Expand Down Expand Up @@ -358,6 +364,70 @@ public FileUpload setDescription(@Nullable String description)
return this;
}

/**
* Turns this attachment into a voice message with the provided waveform.
*
* @param mediaType
* The audio type for the attached audio file. Should be {@code audio/ogg} or similar.
* @param waveform
* The waveform of the audio, which is a low frequency sampling up to 256 bytes.
MinnDevelopment marked this conversation as resolved.
Show resolved Hide resolved
* @param duration
* The actual duration of the audio data.
*
* @throws IllegalArgumentException
* If null is provided or the waveform is not between 1 and 256 bytes long.
*
* @return The same FileUpload instance configured as a voice message attachment
*/
@Nonnull
public FileUpload asVoiceMessage(@Nonnull MediaType mediaType, @Nonnull byte[] waveform, @Nonnull Duration duration)
{
Checks.notNull(duration, "Duration");
return this.asVoiceMessage(mediaType, waveform, duration.toNanos() / 1_000_000_000.0);
}

/**
* Turns this attachment into a voice message with the provided waveform.
*
* @param mediaType
* The audio type for the attached audio file. Should be {@code audio/ogg} or similar.
* @param waveform
* The waveform of the audio, which is a low frequency sampling up to 256 bytes.
* @param durationSeconds
* The actual duration of the audio data in seconds.
*
* @throws IllegalArgumentException
* If null is provided or the waveform is not between 1 and 256 bytes long.
*
* @return The same FileUpload instance configured as a voice message attachment
*/
@Nonnull
public FileUpload asVoiceMessage(@Nonnull MediaType mediaType, @Nonnull byte[] waveform, double durationSeconds)
MinnDevelopment marked this conversation as resolved.
Show resolved Hide resolved
{
Checks.notNull(mediaType, "Media type");
Checks.notNull(waveform, "Waveform");
Checks.check(waveform.length > 0 && waveform.length <= 256, "Waveform must be between 1 and 256 bytes long");
Checks.check(Double.isFinite(durationSeconds), "Duration must be a finite number");
Checks.check(durationSeconds > 0, "Duration must be positive");
this.waveform = waveform;
this.durationSeconds = durationSeconds;
this.mediaType = mediaType;
return this;
}

/**
* Whether this attachment is a valid voice message attachment.
*
* @return True, if this is a voice message attachment.
*/
public boolean isVoiceMessage()
{
return this.mediaType.type().equals("audio")
&& this.durationSeconds > 0.0
&& this.waveform != null
&& this.waveform.length > 0;
}

/**
* The filename for the file.
*
Expand Down Expand Up @@ -425,17 +495,24 @@ public synchronized RequestBody getRequestBody(@Nonnull MediaType type)
@SuppressWarnings("ConstantConditions")
public synchronized void addPart(@Nonnull MultipartBody.Builder builder, int index)
{
builder.addFormDataPart("files[" + index + "]", name, getRequestBody(Requester.MEDIA_TYPE_OCTET));
builder.addFormDataPart("files[" + index + "]", name, getRequestBody(mediaType));
}

@Nonnull
@Override
public DataObject toAttachmentData(int index)
{
return DataObject.empty()
DataObject attachment = DataObject.empty()
.put("id", index)
.put("description", description == null ? "" : description)
.put("content_type", mediaType.toString())
.put("filename", name);
if (waveform != null && durationSeconds > 0)
{
attachment.put("waveform", new String(Base64.getEncoder().encode(waveform), StandardCharsets.UTF_8));
attachment.put("duration_secs", durationSeconds);
}
return attachment;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import net.dv8tion.jda.internal.utils.Checks;
import net.dv8tion.jda.internal.utils.Helpers;
import net.dv8tion.jda.internal.utils.IOUtil;
import org.jetbrains.annotations.NotNull;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
Expand Down Expand Up @@ -181,7 +182,10 @@ public MessageCreateBuilder setFiles(@Nullable Collection<? extends FileUpload>
Checks.noneNull(files, "Files");
this.files.clear();
if (files != null)
{
this.files.addAll(files);
this.setVoiceMessageIfApplicable(files);
}
return this;
}

Expand Down Expand Up @@ -213,6 +217,7 @@ public MessageCreateBuilder addFiles(@Nonnull Collection<? extends FileUpload> f
{
Checks.noneNull(files, "Files");
this.files.addAll(files);
this.setVoiceMessageIfApplicable(files);
return this;
}

Expand All @@ -228,13 +233,24 @@ public MessageCreateBuilder setTTS(boolean tts)
@Override
public MessageCreateBuilder setSuppressedNotifications(boolean suppressed)
{
if(suppressed)
if (suppressed)
messageFlags |= Message.MessageFlag.NOTIFICATIONS_SUPPRESSED.getValue();
else
messageFlags &= ~Message.MessageFlag.NOTIFICATIONS_SUPPRESSED.getValue();
return this;
}

@Nonnull
@Override
public MessageCreateBuilder setVoiceMessage(boolean voiceMessage)
{
if (voiceMessage)
messageFlags |= Message.MessageFlag.IS_VOICE_MESSAGE.getValue();
else
messageFlags &= ~Message.MessageFlag.IS_VOICE_MESSAGE.getValue();
return this;
}

@Override
public boolean isEmpty()
{
Expand Down Expand Up @@ -291,4 +307,10 @@ public MessageCreateBuilder closeFiles()
files.clear();
return this;
}

private void setVoiceMessageIfApplicable(@NotNull Collection<? extends FileUpload> files)
{
if (files.stream().anyMatch(FileUpload::isVoiceMessage))
this.setVoiceMessage(true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -270,13 +270,23 @@ public boolean isTTS()
/**
* Whether this message is silent.
*
* @return True, if the message will not trigger push and desktop notifications
* @return True, if the message will not trigger push and desktop notifications.
*/
public boolean isSuppressedNotifications()
{
return (flags & Message.MessageFlag.NOTIFICATIONS_SUPPRESSED.getValue()) != 0;
}

/**
* Whether this message is intended as a voice message.
*
* @return True, if this message is intended as a voice message.
*/
public boolean isVoiceMessage()
{
return (flags & Message.MessageFlag.IS_VOICE_MESSAGE.getValue()) != 0;
}

/**
* The IDs for users which are allowed to be mentioned, or an empty list.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import net.dv8tion.jda.api.requests.RestAction;
import net.dv8tion.jda.api.utils.FileUpload;
import net.dv8tion.jda.internal.utils.Checks;
import okhttp3.MediaType;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
Expand Down Expand Up @@ -339,11 +340,24 @@ default R addFiles(@Nonnull FileUpload... files)
* @param suppressed
* True, if this message should not trigger push/desktop notifications
*
* @return The same reply action, for chaining convenience
* @return The same instance for chaining
*/
@Nonnull
R setSuppressedNotifications(boolean suppressed);

/**
* Whether this message should be considered a voice message.
* <br>Voice messages must upload a valid voice message attachment, using {@link FileUpload#asVoiceMessage(MediaType, byte[], double)}.
*
* @param voiceMessage
* True, if this message is a voice message.
* Turned on automatically if attachment is a valid voice message attachment.
*
* @return The same instance for chaining
*/
@Nonnull
R setVoiceMessage(boolean voiceMessage);

/**
* Applies the provided {@link MessageCreateData} to this request.
*
Expand Down Expand Up @@ -372,6 +386,7 @@ default R applyData(@Nonnull MessageCreateData data)
.setTTS(data.isTTS())
.setSuppressEmbeds(data.isSuppressEmbeds())
.setSuppressedNotifications(data.isSuppressedNotifications())
.setVoiceMessage(data.isVoiceMessage())
.setComponents(layoutComponents)
.setPoll(data.getPoll())
.setFiles(data.getFiles());
Expand All @@ -390,6 +405,7 @@ default R applyMessage(@Nonnull Message message)
.setEmbeds(embeds)
.setTTS(message.isTTS())
.setSuppressedNotifications(message.isSuppressedNotifications())
.setVoiceMessage(message.isVoiceMessage())
.setComponents(message.getActionRows())
.setPoll(message.getPoll() != null ? MessagePollData.from(message.getPoll()) : null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,12 @@ public boolean isSuppressedNotifications()
return (this.flags & MessageFlag.NOTIFICATIONS_SUPPRESSED.getValue()) != 0;
}

@Override
public boolean isVoiceMessage()
{
return (this.flags & MessageFlag.IS_VOICE_MESSAGE.getValue()) != 0;
}

@Nullable
@Override
public ThreadChannel getStartedThread()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,12 @@ default R setSuppressedNotifications(boolean suppressed)
getBuilder().setSuppressedNotifications(suppressed);
return (R) this;
}

@Nonnull
@Override
default R setVoiceMessage(boolean voiceMessage)
{
getBuilder().setVoiceMessage(voiceMessage);
return (R) this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import net.dv8tion.jda.api.utils.data.DataObject;
import net.dv8tion.jda.internal.requests.Requester;
import net.dv8tion.jda.internal.utils.EncodingUtil;
import okhttp3.MediaType;
import okhttp3.RequestBody;
import org.jetbrains.annotations.Contract;
import org.mockito.ThrowingConsumer;

Expand Down Expand Up @@ -74,6 +76,21 @@ public RestActionAssertions checkAssertions(@Nonnull ThrowingConsumer<Request<?>
return this;
}

@CheckReturnValue
@Contract("->this")
public RestActionAssertions hasMultipartBody()
{
return checkAssertions(request -> {
RequestBody body = request.getBody();
assertThat(body).isNotNull();
MediaType mediaType = body.contentType();
assertThat(mediaType).isNotNull();

assertThat(mediaType.toString())
.startsWith("multipart/form-data; boundary=");
});
}

@CheckReturnValue
@Contract("_->this")
public RestActionAssertions hasBodyEqualTo(@Nonnull DataObject expected)
Expand Down
Loading
Loading