diff --git a/build.gradle.kts b/build.gradle.kts index 2ac4c6d..71bd606 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,8 +12,8 @@ plugins { } val major = "0" -val minor = "8" -val patch = "2" +val minor = "9" +val patch = "0" group = "club.minnced" version = "$major.$minor.$patch" @@ -46,7 +46,7 @@ repositories { val versions = mapOf( "slf4j" to "1.7.32", "okhttp" to "4.10.0", - "json" to "20210307", + "json" to "20220320", "jda" to "5.0.0-alpha.13", "discord4j" to "3.2.2", "javacord" to "3.4.0", diff --git a/src/main/java/club/minnced/discord/webhook/LibraryInfo.java b/src/main/java/club/minnced/discord/webhook/LibraryInfo.java index 7a7a077..869991f 100644 --- a/src/main/java/club/minnced/discord/webhook/LibraryInfo.java +++ b/src/main/java/club/minnced/discord/webhook/LibraryInfo.java @@ -17,7 +17,7 @@ package club.minnced.discord.webhook; public class LibraryInfo { - public static final int DISCORD_API_VERSION = 9; + public static final int DISCORD_API_VERSION = 10; public static final String VERSION_MAJOR = "@MAJOR@"; public static final String VERSION_MINOR = "@MINOR@"; public static final String VERSION_PATCH = "@PATCH@"; diff --git a/src/main/java/club/minnced/discord/webhook/receive/ReadonlyAttachment.java b/src/main/java/club/minnced/discord/webhook/receive/ReadonlyAttachment.java index d076ca5..7994d28 100644 --- a/src/main/java/club/minnced/discord/webhook/receive/ReadonlyAttachment.java +++ b/src/main/java/club/minnced/discord/webhook/receive/ReadonlyAttachment.java @@ -16,6 +16,7 @@ package club.minnced.discord.webhook.receive; +import club.minnced.discord.webhook.send.MessageAttachment; import org.jetbrains.annotations.NotNull; import org.json.JSONObject; import org.json.JSONPropertyName; @@ -26,7 +27,7 @@ *
This does not actually contain the file but only meta-data * useful to retrieve the actual attachment. */ -public class ReadonlyAttachment implements JSONString { +public class ReadonlyAttachment extends MessageAttachment implements JSONString { private final String url; private final String proxyUrl; private final String fileName; @@ -37,6 +38,7 @@ public class ReadonlyAttachment implements JSONString { public ReadonlyAttachment( @NotNull String url, @NotNull String proxyUrl, @NotNull String fileName, int width, int height, int size, long id) { + super(id, fileName); this.url = url; this.proxyUrl = proxyUrl; this.fileName = fileName; diff --git a/src/main/java/club/minnced/discord/webhook/send/MessageAttachment.java b/src/main/java/club/minnced/discord/webhook/send/MessageAttachment.java index c3528ca..4e32724 100644 --- a/src/main/java/club/minnced/discord/webhook/send/MessageAttachment.java +++ b/src/main/java/club/minnced/discord/webhook/send/MessageAttachment.java @@ -18,6 +18,8 @@ import club.minnced.discord.webhook.IOUtil; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.json.JSONObject; import java.io.File; import java.io.FileInputStream; @@ -28,15 +30,25 @@ * Internal representation of attachments for outgoing messages */ public class MessageAttachment { + private static final byte[] empty = new byte[0]; + private final long id; private final String name; private final byte[] data; + protected MessageAttachment(long id, @NotNull String name) { + this.id = id; + this.name = name; + this.data = empty; + } + MessageAttachment(@NotNull String name, @NotNull byte[] data) { + this.id = 0; this.name = name; this.data = data; } MessageAttachment(@NotNull String name, @NotNull InputStream stream) throws IOException { + this.id = 0; this.name = name; try (InputStream data = stream) { this.data = IOUtil.readAllBytes(data); @@ -47,6 +59,50 @@ public class MessageAttachment { this(name, new FileInputStream(file)); } + /** + * Create an instance of this class with the provided ID and name. + * + *

This can be used in {@link club.minnced.discord.webhook.send.WebhookMessageBuilder#addFile(MessageAttachment)} + * to retain existing attachments on a message for an edit request. + * The name parameter can also be used to rename the attachment. + * + * @param id + * The snowflake ID for this attachment (must exist on the message you edit9 + * @param name + * The new name for the attachment, or null to keep existing name + * + * @throws java.lang.IllegalArgumentException + * If the provided ID is not a valid snowflake + * + * @return The attachment instance + */ + @NotNull + public static MessageAttachment fromId(long id, @Nullable String name) { + if (id > WebhookMessage.MAX_FILES) + throw new IllegalArgumentException("MessageAttachment ID must be higher than " + WebhookMessage.MAX_FILES + "."); + return new MessageAttachment(id, name == null ? "" : name); + } + + + /** + * Create an instance of this class with the provided ID and name. + * + *

This can be used in {@link club.minnced.discord.webhook.send.WebhookMessageBuilder#addFile(MessageAttachment)} + * to retain existing attachments on a message for an edit request. + * + * @param id + * The snowflake ID for this attachment (must exist on the message you edit9 + * + * @throws java.lang.IllegalArgumentException + * If the provided ID is not a valid snowflake + * + * @return The attachment instance + */ + @NotNull + public static MessageAttachment fromId(long id) { + return fromId(id, null); + } + @NotNull public String getName() { return name; @@ -56,4 +112,18 @@ public String getName() { public byte[] getData() { return data; } + + public long getId() { + return id; + } + + @NotNull + public JSONObject toJSON() { + JSONObject json = new JSONObject(); + if (!name.isEmpty()) + json.put("name", name); + if (id != 0) + json.put("id", id); + return json; + } } diff --git a/src/main/java/club/minnced/discord/webhook/send/WebhookMessage.java b/src/main/java/club/minnced/discord/webhook/send/WebhookMessage.java index b412643..4e03d16 100644 --- a/src/main/java/club/minnced/discord/webhook/send/WebhookMessage.java +++ b/src/main/java/club/minnced/discord/webhook/send/WebhookMessage.java @@ -233,6 +233,33 @@ public static WebhookMessage embeds(@NotNull Collection embeds) { return new WebhookMessage(null, null, null, new ArrayList<>(embeds), false, null, AllowedMentions.all(), 0); } + /** + * Creates a WebhookMessage from the provided attachments. + *
A message can hold up to {@value #MAX_FILES} attachments + * and a total of 8MiB of data. + * + * @param attachments + * The attachments to add, keys are the alternative names + * for each attachment + * + * @throws java.lang.NullPointerException + * If provided with null + * @throws java.lang.IllegalArgumentException + * If no attachments are provided or more than {@value #MAX_FILES} + * + * @return A WebhookMessage for the attachments + */ + @NotNull + public static WebhookMessage files(@NotNull Collection attachments) { + Objects.requireNonNull(attachments, "Attachments"); + + int fileAmount = attachments.size(); + if (fileAmount > WebhookMessage.MAX_FILES) + throw new IllegalArgumentException("Cannot add more than " + WebhookMessage.MAX_FILES + " files to a message"); + MessageAttachment[] files = attachments.toArray(new MessageAttachment[0]); + return new WebhookMessage(null, null, null, null, false, files, AllowedMentions.all(), 0); + } + /** * Creates a WebhookMessage from the provided attachments. *
A message can hold up to {@value #MAX_FILES} attachments @@ -254,8 +281,6 @@ public static WebhookMessage files(@NotNull Map attachments) { Objects.requireNonNull(attachments, "Attachments"); int fileAmount = attachments.size(); - if (fileAmount == 0) - throw new IllegalArgumentException("Cannot build an empty message"); if (fileAmount > WebhookMessage.MAX_FILES) throw new IllegalArgumentException("Cannot add more than " + WebhookMessage.MAX_FILES + " files to a message"); Set> entries = attachments.entrySet(); @@ -351,19 +376,28 @@ public RequestBody getBody() { payload.put("tts", isTTS); payload.put("allowed_mentions", allowedMentions); payload.put("flags", flags); - String json = payload.toString(); if (isFile()) { final MultipartBody.Builder builder = new MultipartBody.Builder().setType(MultipartBody.FORM); + JSONArray attachmentArray = new JSONArray(attachments.length); for (int i = 0; i < attachments.length; i++) { final MessageAttachment attachment = attachments[i]; if (attachment == null) break; - builder.addFormDataPart("file" + i, attachment.getName(), new IOUtil.OctetBody(attachment.getData())); + + JSONObject attachmentJson = attachment.toJSON(); + if (attachment.getId() == 0) { + builder.addFormDataPart("files[" + i + "]", attachment.getName(), new IOUtil.OctetBody(attachment.getData())); + attachmentJson.put("id", i); + } + + attachmentArray.put(attachmentJson); } - return builder.addFormDataPart("payload_json", json).build(); + + payload.put("attachments", attachmentArray); + return builder.addFormDataPart("payload_json", payload.toString()).build(); } - return RequestBody.create(IOUtil.JSON, json); + return RequestBody.create(IOUtil.JSON, payload.toString()); } @NotNull @@ -372,7 +406,9 @@ private static MessageAttachment convertAttachment(@NotNull String name, @NotNul Objects.requireNonNull(data, "Data"); try { MessageAttachment a; - if (data instanceof File) + if (data instanceof MessageAttachment) + a = (MessageAttachment) data; + else if (data instanceof File) a = new MessageAttachment(name, (File) data); else if (data instanceof InputStream) a = new MessageAttachment(name, (InputStream) data); diff --git a/src/main/java/club/minnced/discord/webhook/send/WebhookMessageBuilder.java b/src/main/java/club/minnced/discord/webhook/send/WebhookMessageBuilder.java index 7b857c5..4612d8c 100644 --- a/src/main/java/club/minnced/discord/webhook/send/WebhookMessageBuilder.java +++ b/src/main/java/club/minnced/discord/webhook/send/WebhookMessageBuilder.java @@ -54,6 +54,7 @@ public class WebhookMessageBuilder { protected boolean isTTS; protected int flags; private int fileIndex = 0; + private boolean hasFiles = false; /** * Whether this builder is currently empty @@ -100,6 +101,7 @@ public WebhookMessageBuilder resetFiles() { files[i] = null; } fileIndex = 0; + hasFiles = false; return this; } @@ -292,17 +294,80 @@ public WebhookMessageBuilder setEphemeral(boolean ephemeral) { return this; } + /** + * Remove all attachments from the message, unless included in the provided collection. + * + *

This can be used inplace of {@link #addFile(MessageAttachment)} overloads to entirely remove files. + * + * @param attachments + * The attachments to retain (or empty to remove all) + * + * @throws java.lang.NullPointerException + * If provided with null + * @throws java.lang.IllegalStateException + * If more than {@value WebhookMessage#MAX_FILES} are added + * + * @return This builder for chaining convenience + * + * @see MessageAttachment#fromId(long) + * @see MessageAttachment#fromId(long, String) + */ + @NotNull + public WebhookMessageBuilder retainAttachments(@NotNull Collection attachments) { + Objects.requireNonNull(attachments, "Attachments"); + if (fileIndex + attachments.size() >= WebhookMessage.MAX_FILES) + throw new IllegalStateException("Cannot add more than " + WebhookMessage.MAX_FILES + " attachments to a message"); + hasFiles = true; + for (MessageAttachment attachment : attachments) { + Objects.requireNonNull(attachment, "MessageAttachment"); + files[fileIndex++] = attachment; + } + return this; + } + /** * Adds the provided file as an attachment to this message. *
A single message can have up to {@value WebhookMessage#MAX_FILES} attachments. * - * @param file - * The file to attach + *

As of recent API updates, adding new files also requires specifying all existing files to keep attached. + * If the existing attachments are not provided in addition to new files, they will instead be removed from the message. + * + * @param attachment + * The file attachment to add + * + * @throws java.lang.NullPointerException + * If provided with null + * @throws java.lang.IllegalStateException + * If more than {@value WebhookMessage#MAX_FILES} are added * * @return This builder for chaining convenience + */ + @NotNull + public WebhookMessageBuilder addFile(@NotNull MessageAttachment attachment) { + Objects.requireNonNull(attachment, "MessageAttachment"); + if (fileIndex >= WebhookMessage.MAX_FILES) + throw new IllegalStateException("Cannot add more than " + WebhookMessage.MAX_FILES + " attachments to a message"); + files[fileIndex++] = attachment; + return this; + } + + /** + * Adds the provided file as an attachment to this message. + *
A single message can have up to {@value WebhookMessage#MAX_FILES} attachments. + * + *

As of recent API updates, adding new files also requires specifying all existing files to keep attached. + * If the existing attachments are not provided in addition to new files, they will instead be removed from the message. + * Use {@link #retainAttachments(java.util.Collection)} or {@link #addFile(MessageAttachment)} to add all existing attachments. + * + * @param file + * The file to attach * * @throws java.lang.NullPointerException * If provided with null + * @throws java.lang.IllegalStateException + * If more than {@value WebhookMessage#MAX_FILES} are added + * + * @return This builder for chaining convenience */ @NotNull public WebhookMessageBuilder addFile(@NotNull File file) { @@ -314,6 +379,10 @@ public WebhookMessageBuilder addFile(@NotNull File file) { * Adds the provided file as an attachment to this message. *
A single message can have up to {@value WebhookMessage#MAX_FILES} attachments. * + *

As of recent API updates, adding new files also requires specifying all existing files to keep attached. + * If the existing attachments are not provided in addition to new files, they will instead be removed from the message. + * Use {@link #retainAttachments(java.util.Collection)} or {@link #addFile(MessageAttachment)} to add all existing attachments. + * * @param name * The alternative name that should be used instead * @param file @@ -321,6 +390,8 @@ public WebhookMessageBuilder addFile(@NotNull File file) { * * @throws java.lang.NullPointerException * If provided with null + * @throws java.lang.IllegalStateException + * If more than {@value WebhookMessage#MAX_FILES} are added * * @return This builder for chaining convenience */ @@ -346,6 +417,10 @@ public WebhookMessageBuilder addFile(@NotNull String name, @NotNull File file) { * Adds the provided data as a file attachment to this message. *
A single message can have up to {@value WebhookMessage#MAX_FILES} attachments. * + *

As of recent API updates, adding new files also requires specifying all existing files to keep attached. + * If the existing attachments are not provided in addition to new files, they will instead be removed from the message. + * Use {@link #retainAttachments(java.util.Collection)} or {@link #addFile(MessageAttachment)} to add all existing attachments. + * * @param name * The alternative name that should be used * @param data @@ -372,6 +447,10 @@ public WebhookMessageBuilder addFile(@NotNull String name, @NotNull byte[] data) * Adds the provided data as a file attachment to this message. *
A single message can have up to {@value WebhookMessage#MAX_FILES} attachments. * + *

As of recent API updates, adding new files also requires specifying all existing files to keep attached. + * If the existing attachments are not provided in addition to new files, they will instead be removed from the message. + * Use {@link #retainAttachments(java.util.Collection)} or {@link #addFile(MessageAttachment)} to add all existing attachments. + * * @param name * The alternative name that should be used * @param data @@ -379,6 +458,8 @@ public WebhookMessageBuilder addFile(@NotNull String name, @NotNull byte[] data) * * @throws java.lang.NullPointerException * If provided with null + * @throws java.lang.IllegalStateException + * If more than {@value WebhookMessage#MAX_FILES} are added * * @return This builder for chaining convenience */ @@ -410,7 +491,7 @@ public WebhookMessage build() { if (isEmpty()) throw new IllegalStateException("Cannot build an empty message!"); return new WebhookMessage(username, avatarUrl, content.toString(), embeds, isTTS, - fileIndex == 0 ? null : Arrays.copyOf(files, fileIndex), allowedMentions, flags); + fileIndex == 0 && !hasFiles ? null : Arrays.copyOf(files, fileIndex), allowedMentions, flags); } diff --git a/src/test/java/root/send/MessageTest.java b/src/test/java/root/send/MessageTest.java index 11b96d2..aa2c792 100644 --- a/src/test/java/root/send/MessageTest.java +++ b/src/test/java/root/send/MessageTest.java @@ -301,17 +301,21 @@ public void checkMultipart() throws IOException { .put("allowed_mentions", allowedMentions) .put("content", "CONTENT!") .put("embeds", new JSONArray()) + .put("attachments", new JSONArray() + .put(new JSONObject() + .put("id", 0) + .put("name", "myFile.txt"))) .put("tts", false) .put("flags", 0) .toMap(), new JSONObject((String) multiPart.get("payload_json")).toMap() ); - Assert.assertTrue("Multipart doesn't contain file", multiPart.containsKey("file0")); - Assert.assertTrue("Multipart file is not of correct type", multiPart.get("file0") instanceof IOTestUtil.MultiPartFile); + Assert.assertTrue("Multipart doesn't contain file", multiPart.containsKey("files[0]")); + Assert.assertTrue("Multipart file is not of correct type", multiPart.get("files[0]") instanceof IOTestUtil.MultiPartFile); Assert.assertEquals("Multipart file mismatches", fileContent, - new String(((IOTestUtil.MultiPartFile) multiPart.get("file0")).content, StandardCharsets.UTF_8) + new String(((IOTestUtil.MultiPartFile) multiPart.get("files[0]")).content, StandardCharsets.UTF_8) ); }