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..3e0d81f 100644 --- a/src/main/java/club/minnced/discord/webhook/send/WebhookMessage.java +++ b/src/main/java/club/minnced/discord/webhook/send/WebhookMessage.java @@ -19,6 +19,7 @@ import club.minnced.discord.webhook.IOUtil; import club.minnced.discord.webhook.MessageFlags; import club.minnced.discord.webhook.receive.ReadonlyMessage; +import club.minnced.discord.webhook.send.component.LayoutComponent; import okhttp3.MultipartBody; import okhttp3.RequestBody; import org.jetbrains.annotations.NotNull; @@ -46,19 +47,21 @@ public class WebhookMessage { protected final String username, avatarUrl, content; protected final List embeds; + protected final List components; protected final boolean isTTS; protected final MessageAttachment[] attachments; protected final AllowedMentions allowedMentions; protected final int flags; protected WebhookMessage(final String username, final String avatarUrl, final String content, - final List embeds, final boolean isTTS, + final List embeds, List components, final boolean isTTS, final MessageAttachment[] files, final AllowedMentions allowedMentions, final int flags) { this.username = username; this.avatarUrl = avatarUrl; this.content = content; this.embeds = embeds; + this.components = components; this.isTTS = isTTS; this.attachments = files; this.allowedMentions = allowedMentions; @@ -149,7 +152,7 @@ public WebhookMessage asEphemeral(boolean ephemeral) { flags |= MessageFlags.EPHEMERAL; else flags &= ~MessageFlags.EPHEMERAL; - return new WebhookMessage(username, avatarUrl, content, embeds, isTTS, attachments, allowedMentions, flags); + return new WebhookMessage(username, avatarUrl, content, embeds, components, isTTS, attachments, allowedMentions, flags); } /** @@ -205,7 +208,7 @@ public static WebhookMessage embeds(@NotNull WebhookEmbed first, @NotNull Webhoo List list = new ArrayList<>(1 + embeds.length); list.add(first); Collections.addAll(list, embeds); - return new WebhookMessage(null, null, null, list, false, null, AllowedMentions.all(), 0); + return new WebhookMessage(null, null, null, list, null, false, null, AllowedMentions.all(), 0); } /** @@ -230,7 +233,7 @@ public static WebhookMessage embeds(@NotNull Collection embeds) { if (embeds.isEmpty()) throw new IllegalArgumentException("Cannot build an empty message"); embeds.forEach(Objects::requireNonNull); - return new WebhookMessage(null, null, null, new ArrayList<>(embeds), false, null, AllowedMentions.all(), 0); + return new WebhookMessage(null, null, null, new ArrayList<>(embeds), null, false, null, AllowedMentions.all(), 0); } /** @@ -267,7 +270,7 @@ public static WebhookMessage files(@NotNull Map attachments) { Object data = attachment.getValue(); files[i++] = convertAttachment(name, data); } - return new WebhookMessage(null, null, null, null, false, files, AllowedMentions.all(), 0); + return new WebhookMessage(null, null, null, null, null, false, files, AllowedMentions.all(), 0); } /** @@ -313,7 +316,7 @@ public static WebhookMessage files(@NotNull String name1, @NotNull Object data1, throw new IllegalArgumentException("Provided arguments must be pairs for (String, Data). Expected String and found " + (name == null ? null : name.getClass().getName())); files[j] = convertAttachment((String) name, data); } - return new WebhookMessage(null, null, null, null, false, files, AllowedMentions.all(), 0); + return new WebhookMessage(null, null, null, null, null, false, files, AllowedMentions.all(), 0); } /** @@ -344,6 +347,15 @@ public RequestBody getBody() { } else { payload.put("embeds", new JSONArray()); } + if (components != null && !components.isEmpty()) { + final JSONArray array = new JSONArray(); + for (LayoutComponent component : components) { + array.put(component); + } + payload.put("components", array); + } else { + payload.put("components", new JSONArray()); + } if (avatarUrl != null) payload.put("avatar_url", avatarUrl); if (username != null) @@ -386,4 +398,4 @@ else if (data instanceof byte[]) throw new IllegalArgumentException(ex); } } -} +} \ No newline at end of file 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..5d4cff3 100644 --- a/src/main/java/club/minnced/discord/webhook/send/WebhookMessageBuilder.java +++ b/src/main/java/club/minnced/discord/webhook/send/WebhookMessageBuilder.java @@ -17,6 +17,7 @@ package club.minnced.discord.webhook.send; import club.minnced.discord.webhook.MessageFlags; +import club.minnced.discord.webhook.send.component.LayoutComponent; import discord4j.core.spec.MessageCreateSpec; import discord4j.core.spec.MessageEditSpec; import discord4j.discordjson.json.AllowedMentionsData; @@ -48,6 +49,7 @@ public class WebhookMessageBuilder { protected final StringBuilder content = new StringBuilder(); protected final List embeds = new LinkedList<>(); + protected final List components = new ArrayList<>(); protected final MessageAttachment[] files = new MessageAttachment[WebhookMessage.MAX_FILES]; protected AllowedMentions allowedMentions = AllowedMentions.all(); protected String username, avatarUrl; @@ -183,6 +185,55 @@ public WebhookMessageBuilder addEmbeds(@NotNull Collection LayoutComponent.MAX_COMPONENTS) + throw new IllegalStateException("Cannot have more than " + LayoutComponent.MAX_COMPONENTS + " component layouts in a message"); + for (LayoutComponent component : components) { + Objects.requireNonNull(component, "Component"); + this.components.add(component); + } + return this; + } + + /** + * Adds the provided embeds to the builder + * + * @param components + * The layout components to add + * + * @return This builder for chaining convenience + * + * @throws java.lang.NullPointerException + * If provided with null + * @throws java.lang.IllegalStateException + * If more than {@value LayoutComponent#MAX_COMPONENTS} are added + */ + @NotNull + public WebhookMessageBuilder addComponents(@NotNull Collection components) { + Objects.requireNonNull(components, "Components"); + if (this.components.size() + components.size() > LayoutComponent.MAX_COMPONENTS) + throw new IllegalStateException("Cannot have more than " + LayoutComponent.MAX_COMPONENTS + " component layouts in a message"); + for (LayoutComponent component : components) { + Objects.requireNonNull(component, "Component"); + this.components.add(component); + } + return this; + } /** * Configures the content for this builder * @@ -409,7 +460,7 @@ public WebhookMessageBuilder addFile(@NotNull String name, @NotNull InputStream public WebhookMessage build() { if (isEmpty()) throw new IllegalStateException("Cannot build an empty message!"); - return new WebhookMessage(username, avatarUrl, content.toString(), embeds, isTTS, + return new WebhookMessage(username, avatarUrl, content.toString(), embeds, components, isTTS, fileIndex == 0 ? null : Arrays.copyOf(files, fileIndex), allowedMentions, flags); } @@ -452,13 +503,13 @@ public static WebhookMessageBuilder fromJDA(@NotNull net.dv8tion.jda.api.entitie AllowedMentions allowedMentions = AllowedMentions.none(); Mentions mentions = message.getMentions(); allowedMentions.withRoles( - mentions.getRoles().stream() - .map(Role::getId) - .collect(Collectors.toList())); + mentions.getRoles().stream() + .map(Role::getId) + .collect(Collectors.toList())); allowedMentions.withUsers( - mentions.getUsers().stream() - .map(User::getId) - .collect(Collectors.toList())); + mentions.getUsers().stream() + .map(User::getId) + .collect(Collectors.toList())); allowedMentions.withParseEveryone(mentions.mentionsEveryone()); builder.setAllowedMentions(allowedMentions); builder.setEphemeral(message.isEphemeral()); @@ -486,13 +537,13 @@ public static WebhookMessageBuilder fromJavacord(@NotNull org.javacord.api.entit AllowedMentions allowedMentions = AllowedMentions.none(); allowedMentions.withUsers( - message.getMentionedUsers().stream() - .map(DiscordEntity::getIdAsString) - .collect(Collectors.toList())); + message.getMentionedUsers().stream() + .map(DiscordEntity::getIdAsString) + .collect(Collectors.toList())); allowedMentions.withRoles( - message.getMentionedRoles().stream() - .map(DiscordEntity::getIdAsString) - .collect(Collectors.toList())); + message.getMentionedRoles().stream() + .map(DiscordEntity::getIdAsString) + .collect(Collectors.toList())); allowedMentions.withParseEveryone(message.mentionsEveryone()); builder.setAllowedMentions(allowedMentions); return builder; @@ -548,10 +599,10 @@ public static WebhookMessageBuilder fromD4J(@NotNull MessageCreateSpec spec) { builder.setTTS(tts.get()); if (!embeds.isAbsent()) { builder.addEmbeds( - embeds.get().stream() - .map(WebhookEmbedBuilder::fromD4J) - .map(WebhookEmbedBuilder::build) - .collect(Collectors.toList()) + embeds.get().stream() + .map(WebhookEmbedBuilder::fromD4J) + .map(WebhookEmbedBuilder::build) + .collect(Collectors.toList()) ); } @@ -627,4 +678,4 @@ public static WebhookMessageBuilder fromD4J(@NotNull MessageEditSpec spec) { return builder; } -} +} \ No newline at end of file diff --git a/src/main/java/club/minnced/discord/webhook/send/component/ActionComponent.java b/src/main/java/club/minnced/discord/webhook/send/component/ActionComponent.java new file mode 100644 index 0000000..dc54a95 --- /dev/null +++ b/src/main/java/club/minnced/discord/webhook/send/component/ActionComponent.java @@ -0,0 +1,49 @@ +/* + * Copyright 2018-2020 Florian Spie� + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package club.minnced.discord.webhook.send.component; + +import org.jetbrains.annotations.Nullable; + +/** + * Interactive components that can be inserted inside a {@link LayoutComponent} + * + * @see LayoutComponent#addComponent(ActionComponent) + */ +public interface ActionComponent extends Component { + + /** + * The custom id of the component. + *
This can be used for handling interactions with this component. + * + * @return Custom id of the component, or null for link style buttons + */ + @Nullable + String getCustomId(); + + /** + * Changes the disabled status of button + * @param disabled + * use true to disable button + */ + void withDisabled(boolean disabled); + + /** + * @return true if the button is disabled + */ + boolean isDisabled(); + +} \ No newline at end of file diff --git a/src/main/java/club/minnced/discord/webhook/send/component/ActionRow.java b/src/main/java/club/minnced/discord/webhook/send/component/ActionRow.java new file mode 100644 index 0000000..90bb7c9 --- /dev/null +++ b/src/main/java/club/minnced/discord/webhook/send/component/ActionRow.java @@ -0,0 +1,114 @@ +/* + * Copyright 2018-2020 Florian Spie� + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package club.minnced.discord.webhook.send.component; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +public class ActionRow implements LayoutComponent { + private final List components; + + private ActionRow(@NotNull List components) { + validate(components); + this.components = components; + } + + /** + * Creates an action row with the given list of components + * + * @param components + * The components to be added to the action row + * @throws IllegalStateException + * If a select menu is added with any other component + * @throws IllegalStateException + * If more than {@value LayoutComponent#MAX_COMPONENTS} buttons are added in the same action row + * @return An action row containing provided components, or null if the collection is empty or null + */ + @Nullable + public ActionRow of(@NotNull Collection components) { + if (components.size() == 0) return null; + return new ActionRow(new ArrayList<>(components)); + } + + /** + * Creates an action row with the given list of components + * + * @param components + * The components to be added to the action row + * @throws IllegalStateException + * If a select menu is added with any other component + * @throws IllegalStateException + * If more than {@value LayoutComponent#MAX_COMPONENTS} buttons are added in the same action row + * + * @return An action row containing provided components, or null if the provided array is empty or null + */ + @Nullable + public static ActionRow of(@NotNull ActionComponent... components) { + if (components == null || components.length == 0) return null; + return new ActionRow(Arrays.asList(components)); + } + + @Override + @NotNull + public Type getType() { + return Type.ACTION_ROW; + } + + @Override + @NotNull + public List getComponents() { + return this.components; + } + + @Override + @NotNull + public LayoutComponent addComponent(ActionComponent component) { + List newList = new ArrayList<>(this.components); + newList.add(component); + validate(newList); + this.components.add(component); + return this; + } + + @Override + public String toJSONString() { + JSONObject json = new JSONObject(); + json.put("type", this.getType().getId()); + json.put("components", this.components); + return json.toString(); + } + + private void validate(List components) { + int buttonCount = 0; + boolean hasSelectMenu = false; + for (ActionComponent component : components) { + if (component instanceof Button) buttonCount++; + else if (component instanceof SelectMenu) hasSelectMenu = true; + else throw new IllegalArgumentException("Provided component not an instance of Button or SelectMenu"); + } + if (hasSelectMenu && components.size() > 1) + throw new IllegalArgumentException("An action row containing a select menu cannot have have more than 1 component"); + if (buttonCount > Button.MAX_BUTTONS) + throw new IllegalArgumentException("An action row cannot contain more than " + Button.MAX_BUTTONS + " buttons"); + } +} \ No newline at end of file diff --git a/src/main/java/club/minnced/discord/webhook/send/component/Button.java b/src/main/java/club/minnced/discord/webhook/send/component/Button.java new file mode 100644 index 0000000..ca695da --- /dev/null +++ b/src/main/java/club/minnced/discord/webhook/send/component/Button.java @@ -0,0 +1,219 @@ +/* + * Copyright 2018-2020 Florian Spie� + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package club.minnced.discord.webhook.send.component; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.json.JSONObject; + +import java.net.URL; + +/** + * Button components that can be placed inside a {@link LayoutComponent} + * + * @see LayoutComponent#addComponent(ActionComponent) + */ +public class Button implements ActionComponent, SingleEmojiContainer