diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 14140121ad..bdf5ab06f8 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,33 +1,16 @@ - ## Making Changes -Depending on your changes there are certain rules you have to follow if you expect -your Pull Request to be merged. - -**Note**: It is recommended to create a new remote branch for each Pull Request. -Based on the current `master` changes! - -1. Adding a new Method or Class - - If your addition is not internal (e.g. an impl class or private method) you have to write documentation. - - For that please follow the [JavaDoc template](https://jda.wiki/contributing/structure-guide/#javadoc) - - Keep your code consistent! [example](https://jda.wiki/contributing/contributing/#making-changes) - - Follow the guides provided at [JDA Structure Guide](https://jda.wiki/contributing/structure-guide/) - - Compare your code style to the one used all over JDA and ensure you - do not break the consistency (if you find issues in the JDA style you can include and update it) - - Do not remove existing functionality, use deprecation instead (for reference [deprecation policy](https://github.com/discord-jda/JDA?tab=readme-ov-file#versioning-and-deprecation-policy)) +Depending on your changes there are certain rules you have to follow if you expect your Pull Request (PR) to be merged. -2. Making a Commit - - While having multiple commits can help the reader understand your changes, it might sometimes be - better to include more changes in a single commit. - - When you commit your changes write a proper commit caption which explains what you have done +1. Check for existing pull requests that might already implement your changes. + You can review those pull requests with your suggestions for improvements. +1. Reference documentation or issues that are relevant to your changes. + For instance, if you're implementing a new Discord feature, include a link to the [api docs](https://github.com/discord/discord-api-docs) pull request that documents the feature. +1. If you implement a new feature, include some examples on how you intend it to be used in your PR description. +1. Write documentation for all public API methods and types. +1. Follow the JDA coding style, even if unconventional, we want to keep it consistent to maintain readability. +1. Try to limit your PR to one logical change or closely related changes. + For instance, do not refactor other code that is irrelevant to your changes. If you want to refactor something, make a dedicated PR. -3. Updating your Fork - - Before you start committing make sure your fork is updated. - (See [Syncing a Fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) - or [Keeping a Fork Updated](https://thoughtbot.com/blog/keeping-a-github-fork-updated)) - -4. Only open Pull Requests to master - - Look at the [Repository Structure](https://jda.wiki/contributing/repository-structure/) for further details - -For more information please consult the [Contributing](https://jda.wiki/contributing/contributing/) -section of our wiki. +> [!TIP] +> Before starting to implement your changes, you can post in our [lib-dev](https://discord.gg/qcy8K58zWb) channel on our Discord Server. Getting early feedback will help you and the reviewers save a lot of time. diff --git a/README.md b/README.md index bcf76265f7..4d33420b84 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [jitpack]: https://img.shields.io/badge/Snapshots-JitPack-blue [download]: #download [discord-invite]: https://discord.gg/0hMr4ce0tIl3SLv5 -[migration]: https://jda.wiki/introduction/migration-v3-v4/ +[migration]: https://jda.wiki/introduction/migration-v4-v5/ [jenkins]: https://ci.dv8tion.net/job/JDA5 [license]: https://github.com/discord-jda/JDA/tree/master/LICENSE [faq]: https://jda.wiki/introduction/faq/ @@ -15,534 +15,294 @@ [troubleshooting-shield]: https://img.shields.io/badge/Wiki-Troubleshooting-darkgreen.svg [jenkins-shield]: https://img.shields.io/badge/Download-Jenkins-purple.svg [license-shield]: https://img.shields.io/badge/License-Apache%202.0-white.svg -[migration-shield]: https://img.shields.io/badge/Wiki-Migrating%20from%20V3-darkgreen.svg +[migration-shield]: https://img.shields.io/badge/Wiki-Migrating%20from%20V4-darkgreen.svg +[GatewayIntent]: https://docs.jda.wiki/net/dv8tion/jda/api/requests/GatewayIntent.html +[JDABuilder]: https://docs.jda.wiki/net/dv8tion/jda/api/JDABuilder.html +[DefaultShardManagerBuilder]: https://docs.jda.wiki/net/dv8tion/jda/api/sharding/DefaultShardManagerBuilder.html - + -[ ![maven-central][] ][download] -[ ![jitpack][] ](https://jitpack.io/#discord-jda/JDA) -[ ![jenkins-shield][] ][jenkins] -[ ![license-shield][] ][license] - -[ ![discord-shield][] ][discord-invite] -[ ![faq-shield] ][faq] -[ ![docs-shield] ][docs] -[ ![troubleshooting-shield] ][troubleshooting] -[ ![migration-shield][] ][migration] +[![maven-central][]][download] +[![jitpack][]](https://jitpack.io/#discord-jda/JDA) +[![jenkins-shield][]][jenkins] +[![license-shield][]][license] +[![discord-shield][]][discord-invite] +[![faq-shield]][faq] +[![docs-shield]][docs] +[![troubleshooting-shield]][troubleshooting] +[![migration-shield][]][migration] # JDA (Java Discord API) -JDA strives to provide a clean and full wrapping of the Discord REST API and its WebSocket-Events for Java. -This library is a helpful tool that provides the functionality to create a Discord bot in Java. - -## Summary - -1. [Introduction](#creating-the-jda-object) -2. [Sharding](#sharding-a-bot) -3. [Entity Lifetimes](#entity-lifetimes) -4. [Download](#download) -5. [Documentation](#documentation) -6. [Support](#getting-help) -7. [Extensions And Plugins](#third-party-recommendations) -8. [Contributing](#contributing-to-jda) -9. [Dependencies](#dependencies) -10. [Other Libraries](#related-projects) +This open source library is intended for implementing bots on Discord using the real-time gateway and REST API. It provides event based functionality to implement bots of any kind, allowing for effective and scalable applications. -## UserBots and SelfBots +## 📖 Overview -Discord is currently prohibiting creation and usage of automated client accounts (AccountType.CLIENT). -We have officially dropped support for client login as of version **4.2.0**! -If you need a bot, use a bot account from the [Application Dashboard](https://discord.com/developers/applications). +The core concepts of JDA have been developed to make building scalable apps easy: -[Read More](https://support.discord.com/hc/en-us/articles/115002192352-Automated-user-accounts-self-bots-) +1. Event System + Providing simplified events from the gateway API, to respond to any platform events in real-time without much hassle. +1. Rest Actions + Easy to use and scalable implementation of REST API functionality, letting you choose between callbacks with combinators, futures, and blocking. + The library also handles rate-limits imposed by Discord automatically, while still offering ways to replace the default implementation. +1. Customizable Cache + Trading memory usage for better performance where necessary, with sane default presets to choose from and customize. -## Creating the JDA Object +You can learn more by visiting our [wiki][wiki] or referencing our [Javadocs][docs]. -Creating the JDA Object is done via the JDABuilder class. After setting the token and other options via setters, -the JDA Object is then created by calling the `build()` method. When `build()` returns, -JDA might not have finished starting up. However, you can use `awaitReady()` -on the JDA object to ensure that the entire cache is loaded before proceeding. -Note that this method is blocking and will cause the thread to sleep until startup has completed. +## 🔬 Installation -**Example**: +[![maven-central][]](https://mvnrepository.com/artifact/net.dv8tion/JDA/latest) +[![jitpack][]](https://jitpack.io/#discord-jda/JDA) -```java -JDA jda = JDABuilder.createDefault("token").build(); -``` +This library is available on maven central. The latest version is always shown in the [GitHub Release](https://github.com/discord-jda/JDA/releases/latest). -### Configuration +The minimum java version supported by JDA is **Java SE 8**. JDA also uses JSR 305 to support solid interoperability with Kotlin out of the box. -Both the `JDABuilder` and the `DefaultShardManagerBuilder` allow a set of configurations to improve the experience. +### Gradle -**Example**: +```gradle +repositories { + mavenCentral() +} -```java -public static void main(String[] args) { - JDABuilder builder = JDABuilder.createDefault(args[0]); - - // Disable parts of the cache - builder.disableCache(CacheFlag.MEMBER_OVERRIDES, CacheFlag.VOICE_STATE); - // Enable the bulk delete event - builder.setBulkDeleteSplittingEnabled(false); - // Set activity (like "playing Something") - builder.setActivity(Activity.watching("TV")); - - builder.build(); +dependencies { + implementation("net.dv8tion:JDA:$version") { // replace $version with the latest version + // Optionally disable audio natives to reduce jar size by excluding `opus-java` + // Gradle DSL: + // exclude module: 'opus-java' + // Kotlin DSL: + // exclude(module="opus-java") + } } ``` -> See [JDABuilder](https://docs.jda.wiki/net/dv8tion/jda/api/JDABuilder.html) - and [DefaultShardManagerBuilder](https://docs.jda.wiki/net/dv8tion/jda/api/sharding/DefaultShardManagerBuilder.html) +### Maven -You can configure the memory usage by changing enabled `CacheFlags` on the `JDABuilder`. -Additionally, you can change the handling of the member/user cache by disabling **intents** or changing the **member cache policy**. -To learn more about intents and member loading/caching, read the [Gateway Intents Guide](https://jda.wiki/using-jda/gateway-intents-and-member-cache-policy/). +```xml + + net.dv8tion + JDA + $version + + +``` -```java -public void configureMemoryUsage(JDABuilder builder) { - // Disable cache for member activities (streaming/games/spotify) - builder.disableCache(CacheFlag.ACTIVITY); +## 🤖 Creating a Bot - // Only cache members who are either in a voice channel or owner of the guild - builder.setMemberCachePolicy(MemberCachePolicy.VOICE.or(MemberCachePolicy.OWNER)); +To use this library, you have to create an Application in the [Discord Application Dashboard](https://discord.com/developers/applications) and grab your bot token. You can find a step-by-step guide for this in our wiki page [Creating a Discord Bot](https://jda.wiki/using-jda/getting-started/#creating-a-discord-bot). - // Disable member chunking on startup - builder.setChunkingFilter(ChunkingFilter.NONE); +## 🏃‍♂️ Getting Started - // Disable presence updates and typing events - builder.disableIntents(GatewayIntent.GUILD_PRESENCE, GatewayIntent.GUILD_MESSAGE_TYPING); +We provide a number of [examples](https://github.com/discord-jda/JDA/tree/master/src/examples/java) to introduce you to JDA. You can also take a look at our official [Wiki][wiki], [Documentation][docs], and [FAQ][faq]. - // Consider guilds with more than 50 members as "large". - // Large guilds will only provide online members in their setup and thus reduce bandwidth if chunking is disabled. - builder.setLargeThreshold(50); -} -``` +Every bot implemented by JDA starts out using the [JDABuilder][JDABuilder] or [DefaultShardManagerBuilder][DefaultShardManagerBuilder]. Both builders provide a set of default presets for cache usage and events it wants to receive: -### Listening for Events +- `createDefault` - Enables cache for users who are active in voice channels and all cache flags +- `createLight` - Disables all user cache and cache flags +- `create` - Enables member chunking, caches all users, and enables all cache flags -The event system in JDA is configured through a hierarchy of classes/interfaces. -We offer two implementations for the `IEventManager`: +We recommend reading the guide on [caching and intents](https://jda.wiki/using-jda/gateway-intents-and-member-cache-policy/) to get a feel for configuring your bot properly. Here are some possible use-cases: -- **InterfacedEventManager** which uses an `EventListener` interface and the `ListenerAdapter` abstract class -- **AnnotatedEventManager** which uses the `@SubscribeEvent` annotation which can be applied to methods +### Example: Message Logging -By default, the **InterfacedEventManager** is used. -Since you can create your own implementation of `IEventManager`, this is a very versatile and configurable system. -If the aforementioned implementations don't suit your use-case you can simply create a custom implementation and -configure it on the `JDABuilder` with `setEventManager(...)`. +> [!NOTE] +> The following example makes use of the **privileged intent** `GatewayIntent.MESSAGE_CONTENT`, which must be explicitly enabled in your application dashboard. You can find out more about intents in our [wiki guide](https://jda.wiki/using-jda/gateway-intents-and-member-cache-policy/). -#### Examples: +Simply logging messages to the console. Making use of [JDABuilder][JDABuilder], the intended entry point for smaller bots that don't intend to grow to thousands of guilds. -**Using EventListener**: +Starting your bot and attaching an event listener, using the right [intents][GatewayIntent]: ```java -public class ReadyListener implements EventListener { - public static void main(String[] args) throws InterruptedException { - // Note: It is important to register your ReadyListener before building - JDA jda = JDABuilder.createDefault("token") - .addEventListeners(new ReadyListener()) - .build(); - - // optionally block until JDA is ready - jda.awaitReady(); - } - - @Override - public void onEvent(GenericEvent event) { - if (event instanceof ReadyEvent) { - System.out.println("API is ready!"); - } - } +public static void main(String[] args) { + JDABuilder.createLight(token, EnumSet.of(GatewayIntent.GUILD_MESSAGES, GatewayIntent.MESSAGE_CONTENT)) + .addEventListener(new MessageReceiveListener()) + .build(); } ``` -**Using ListenerAdapter**: +Your event listener could look like this: ```java -public class MessageListener extends ListenerAdapter { - public static void main(String[] args) { - JDA jda = JDABuilder.createDefault("token") - .enableIntents(GatewayIntent.MESSAGE_CONTENT) // enables explicit access to message.getContentDisplay() - .build(); - //You can also add event listeners to the already built JDA instance - // Note that some events may not be received if the listener is added after calling build() - // This includes events such as the ReadyEvent - jda.addEventListener(new MessageListener()); - } - - @Override - public void onMessageReceived(MessageReceivedEvent event) { - if (event.isFromType(ChannelType.PRIVATE)) { - System.out.printf("[PM] %s: %s\n", event.getAuthor().getName(), - event.getMessage().getContentDisplay()); - } else { - System.out.printf("[%s][%s] %s: %s\n", event.getGuild().getName(), - event.getTextChannel().getName(), event.getMember().getEffectiveName(), - event.getMessage().getContentDisplay()); - } - } +public class MessageReceiveListener extends ListenerAdapter { + @Override + public void onMessageReceived(MessageReceivedEvent event) { + System.out.printf("[%s] %#s: %s\n", + event.getChannel(), + event.getAuthor(), + event.getMessage().getContentDisplay()); + } } ``` -**Slash-Commands**: - -```java -public class Bot extends ListenerAdapter { - public static void main(String[] args) { - if (args.length < 1) { - System.out.println("You have to provide a token as first argument!"); - System.exit(1); - } - // args[0] would be the token (using an environment variable or config file is preferred for security) - // We don't need any intents for this bot. Slash commands work without any intents! - JDA jda = JDABuilder.createLight(args[0], Collections.emptyList()) - .addEventListeners(new Bot()) - .setActivity(Activity.playing("Type /ping")) - .build(); - - // Sets the global command list to the provided commands (removing all others) - jda.updateCommands().addCommands( - Commands.slash("ping", "Calculate ping of the bot"), - Commands.slash("ban", "Ban a user from the server") - .setDefaultPermissions(DefaultMemberPermissions.enabledFor(Permission.BAN_MEMBERS)) // only usable with ban permissions - .setGuildOnly(true) // Ban command only works inside a guild - .addOption(OptionType.USER, "user", "The user to ban", true) // required option of type user (target to ban) - .addOption(OptionType.STRING, "reason", "The ban reason") // optional reason - ).queue(); - } - - @Override - public void onSlashCommandInteraction(SlashCommandInteractionEvent event) { - // make sure we handle the right command - switch (event.getName()) { - case "ping": - long time = System.currentTimeMillis(); - event.reply("Pong!").setEphemeral(true) // reply or acknowledge - .flatMap(v -> - event.getHook().editOriginalFormat("Pong: %d ms", System.currentTimeMillis() - time) // then edit original - ).queue(); // Queue both reply and edit - break; - case "ban": - // double check permissions, don't trust Discord on this! - if (!event.getMember().hasPermission(Permission.BAN_MEMBERS)) { - event.reply("You cannot ban members! Nice try ;)").setEphemeral(true).queue(); - break; - } - User target = event.getOption("user", OptionMapping::getUser); - // optionally check for member information - Member member = event.getOption("user", OptionMapping::getMember); - if (!event.getMember().canInteract(member)) { - event.reply("You cannot ban this user.").setEphemeral(true).queue(); - break; - } - // Before starting our ban request, tell the user we received the command - // This sends a "Bot is thinking..." message which is later edited once we finished - event.deferReply().queue(); - String reason = event.getOption("reason", OptionMapping::getAsString); - AuditableRestAction action = event.getGuild().ban(target, 0); // Start building our ban request - if (reason != null) // reason is optional - action = action.reason(reason); // set the reason for the ban in the audit logs and ban log - action.queue(v -> { - // Edit the thinking message with our response on success - event.getHook().editOriginal("**" + target.getAsTag() + "** was banned by **" + event.getUser().getAsTag() + "**!").queue(); - }, error -> { - // Tell the user we encountered some error - event.getHook().editOriginal("Some error occurred, try again!").queue(); - error.printStackTrace(); - }); - break; - default: - System.out.printf("Unknown command %s used by %#s%n", event.getName(), event.getUser()); - } - } -} -``` +You can find a more thorough example with the [MessageLoggerExample](https://github.com/discord-jda/JDA/blob/master/src/examples/java/MessageLoggerExample.java) class. -### RestAction +### Example: Slash Command Bot -Through [RestAction](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html) we provide request handling with - - - [callbacks](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#queue%28java.util.function.Consumer%29) - - [promises](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#submit%28%29) - - and [sync](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#complete%28%29) +This is a bot that makes use of [interactions](https://jda.wiki/using-jda/interactions/) to respond to user commands. Unlike the message logging bot, this bot can work without any enabled intents, since interactions are always available. -and it is up to the user to decide which pattern to utilize. -It can be combined with reactive libraries such as [reactor-core](https://github.com/reactor/reactor-core) due to being lazy. +```java +public static void main(String[] args) { + JDA jda = JDABuilder.createLight(token, Collections.emptyList()) + .addEventListener(new SlashCommandListener()) + .build(); -The RestAction interface also supports a number of operators to avoid callback hell: + // Register your commands to make them visible globally on Discord: -- [`map`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#map%28java.util.function.Function%29) - Convert the result of the `RestAction` to a different value -- [`flatMap`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#flatMap%28java.util.function.Function%29) - Chain another `RestAction` on the result -- [`delay`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#delay%28java.time.Duration%29) - Delay the element of the previous step + CommandListUpdateAction commands = jda.updateCommands(); -**Example**: + // Add all your commands on this action instance + commands.addCommands( + Commands.slash("say", "Makes the bot say what you tell it to") + .addOption(STRING, "content", "What the bot should say", true), // Accepting a user input + Commands.slash("leave", "Makes the bot leave the server") + .setGuildOnly(true) // this doesn't make sense in DMs + .setDefaultPermissions(DefaultMemberPermissions.DISABLED) // only admins should be able to use this command. + ); -```java -public RestAction selfDestruct(MessageChannel channel, String content) { - return channel.sendMessage("The following message will destroy itself in 1 minute!") - .delay(10, SECONDS, scheduler) // edit 10 seconds later - .flatMap((it) -> it.editMessage(content)) - .delay(1, MINUTES, scheduler) // delete 1 minute later - .flatMap(Message::delete); + // Then finally send your commands to discord using the API + commands.queue(); } ``` -### More Examples - -We provide a small set of Examples in the [Example Directory](https://github.com/discord-jda/JDA/tree/master/src/examples/java). - -## Sharding a Bot - -When your bot joins over 2500 guilds, it is required to perform **Sharding**. -This means, your connection is split up into multiple **shards**, each only accessing a fraction of your total available guilds. -A shard can at most contain 2500 guilds when starting up the bot. - -Each shard is assigned a **shard id** and **shard total** (usually shown as `id / total`), which uniquely identifies which guilds are accessible on that shard. -For instance, the first of 2 shards would be `0 / 2` and the second would be `1 / 2`. - -If you want to use sharding with your bot, make use of the [DefaultShardManager](https://docs.jda.wiki/net/dv8tion/jda/api/sharding/DefaultShardManager.html) as seen in the example below. -This manager automatically assigns the right number of shards to your bot, so you do not need to do any math yourself. - -Internally, this shard manager also handles the proper scaling of threads for connections and handles the login rate-limit (Identify Rate-Limit) to properly startup without issues. - -If you do not want to use the shard manager, and instead manage sharding yourself, you can use [JDABuilder#useSharding](https://docs.jda.wiki/net/dv8tion/jda/api/JDABuilder.html#useSharding(int,int)) and [ConcurrentSessionController](https://docs.jda.wiki/net/dv8tion/jda/api/utils/ConcurrentSessionController.html). - - -### Example Sharding - Using DefaultShardManager +An event listener that responds to commands could look like this: ```java -public static void main(String[] args) { - DefaultShardManagerBuilder builder = DefaultShardManagerBuilder.createDefault(args[0]); - builder.addEventListeners(new MessageListener()); - builder.build(); +public class SlashCommandListener extends ListenerAdapter { + @Override + public void onSlashCommandInteraction(SlashCommandInteractionEvent event) { + switch (event.getName()) { + case "say" -> { + String content = event.getOption("content", OptionMapping::getAsString); + event.reply(content).queue(); + }; + case "leave" -> { + event.reply("I'm leaving the server now!") + .setEphemeral(true) // this message is only visible to the command user + .flatMap(m -> event.getGuild().leave()) // append a follow-up action using flatMap + .queue(); // enqueue both actions to run in sequence (send message -> leave guild) + }; + default -> return; + } + } } ``` -## Entity Lifetimes - -An **Entity** is the term used to describe types such as **GuildChannel**/**Message**/**User** and other entities that Discord provides. -Instances of these entities are created and deleted by JDA when Discord instructs it. This means the lifetime depends on signals provided by the Discord API which are used to create/update/delete entities. -This is done through Gateway Events known as "dispatches" that are handled by the JDA WebSocket handlers. -When Discord instructs JDA to delete entities, they are simply removed from the JDA cache and lose their references. -Once that happens, nothing in JDA interacts or updates the instances of those entities, and they become outdated. -Discord may instruct to delete these entities randomly for cache synchronization with the API. - -**It is not recommended to store _any_ of these entities for a longer period of time!** -Instead of keeping (e.g.) a `User` instance in some field, an ID should be used. With the ID of a user, -you can use `getUserById(id)` to get and keep the user reference in a local variable (see below). +You can find a more thorough example with the [SlashBotExample](https://github.com/discord-jda/JDA/blob/master/src/examples/java/SlashBotExample.java) class. -### Entity Updates +## 🚀 RestAction -When an entity is updated through its manager, they will send a request to the Discord API which will update the state -of the entity. The success of this request **does not** imply the entity has been updated yet. All entities are updated -by the aforementioned **Gateway Events** which means you cannot rely on the cache being updated yet once the -execution of a RestAction has completed. Some requests rely on the cache being updated to correctly update the entity. -An example of this is updating roles of a member which overrides all roles of the member by sending a list of the -new set of roles. This is done by first checking the current cache, the roles the member has right now, and appending -or removing the requested roles. If the cache has not yet been updated by an event, this will result in unexpected behavior. - -### Entity Deletion - -Discord may request that a client (the JDA session) invalidates its entire cache. When this happens, JDA will remove all of its current entities and reconnect the session. This is signaled through the `SessionRecreateEvent`. When entities are removed from the JDA cache, your instance will keep stale entities in memory. This results in memory duplication, potential memory leaks, and outdated state. It is **highly recommended** to only keep references to entities by storing their **id** and using the respective `get...ById(id)` method when needed. Alternatively, keep the entity stored and make sure to replace it as soon as possible when the cache is replaced. - -#### Example +In this library, the [RestAction](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html) interface is used as a request builder for all API endpoints. +This interface represents a lazy request builder, as shown in this simple example: ```java -public class UserLogger extends ListenerAdapter { - private final User user; - - public UserLogger(User user) { - this.user = user; - } - - private User getUser(JDA api) { - // Acquire a reference to the User instance through the id - User newUser = api.getUserById(this.user.getIdLong()); - if (newUser != null) - this.user = newUser; - return this.user; - } - - @Override - public void onMessageReceived(MessageReceivedEvent event) { - User author = event.getAuthor(); - Message message = event.getMessage(); - if (author.getIdLong() == this.user.getIdLong()) { - // Update user from message instance (likely more up-to-date) - this.user = author; - // Print the message of the user - System.out.println(author.getAsTag() + ": " + message.getContentDisplay()); - } - } - - @Override - public void onGuildJoin(GuildJoinEvent event) { - JDA api = event.getJDA(); - User user = getUser(); // use getter to refresh user automatically on access - user.openPrivateChannel().queue((channel) -> { - // Send a private message to the user - channel.sendMessageFormat("I have joined a new guild: **%s**", event.getGuild().getName()).queue(); - }); - } -} +channel.sendMessage("Hello Friend!") + .addFiles(FileUpload.fromData(greetImage)) // Chain builder methods to configure the request + .queue() // Send the request asynchronously ``` -## Download +> [!IMPORTANT] +> The final call to [`queue()`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#queue%28%29) sends the request. +> You can also send the request synchronously or using futures, check out our extended guide in the [RestAction Wiki](https://jda.wiki/using-jda/using-restaction/). -[ ![maven-central][] ](https://mvnrepository.com/artifact/net.dv8tion/JDA/latest) -[ ![jitpack][] ](https://jitpack.io/#discord-jda/JDA) +The RestAction interface also supports a number of operators to avoid callback hell: -Latest Release: [GitHub Release](https://github.com/discord-jda/JDA/releases/latest)
+- [`map`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#map%28java.util.function.Function%29) + Convert the result of the `RestAction` to a different value +- [`flatMap`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#flatMap%28java.util.function.Function%29) + Chain another `RestAction` on the result +- [`delay`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#delay%28java.time.Duration%29) + Delay the element of the previous step -Be sure to replace the **VERSION** key below with the one of the versions shown above! For snapshots, please use the instructions provided by [JitPack](https://jitpack.io/#discord-jda/JDA). +As well as combinators like: -**Maven** -```xml - - net.dv8tion - JDA - VERSION - -``` +- [`and`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#and(net.dv8tion.jda.api.requests.RestAction,java.util.function.BiFunction)) + Require another RestAction to complete successfully, running in parallel +- [`allOf`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#allOf(java.util.Collection)) + Accumulate a list of many actions into one (see also [`mapToResult`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#mapToResult())) +- [`zip`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#zip(net.dv8tion.jda.api.requests.RestAction,net.dv8tion.jda.api.requests.RestAction...)) + Similar to `and`, but combines the results into a list + -**Maven without Audio** -```xml - - net.dv8tion - JDA - VERSION - - - club.minnced - opus-java - - - -``` +And configurators like: -**Gradle** -```gradle -repositories { - mavenCentral() -} +- [`timeout`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#timeout(long,java.util.concurrent.TimeUnit)) and [`deadline`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#deadline(long)) + Configure how long the action is allowed to be in queue, cancelling if it takes too long +- [`setCheck`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#setCheck(java.util.function.BooleanSupplier)) + Running some checks right before the request is sent, this can be helpful when it is in queue for a while +- [`reason`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/restaction/AuditableRestAction.html#reason(java.lang.String)) + The [audit log reason](https://discord.com/developers/docs/resources/audit-log) for an action -dependencies { - //Change 'implementation' to 'compile' in old Gradle versions - implementation("net.dv8tion:JDA:VERSION") -} -``` +**Example**: -**Gradle without Audio** -```gradle -dependencies { - //Change 'implementation' to 'compile' in old Gradle versions - implementation("net.dv8tion:JDA:VERSION") { - exclude module: 'opus-java' - } +```java +public RestAction selfDestruct(MessageChannel channel, String content) { + return channel.sendMessage("The following message will destroy itself in 1 minute!") + .addActionRow(Button.danger("delete", "Delete now")) // further amend message before sending + .delay(10, SECONDS, scheduler) // after sending, wait 10 seconds + .flatMap((it) -> it.editMessage(content)) // then edit the message + .delay(1, MINUTES, scheduler) // wait another minute + .flatMap(Message::delete); // then delete } ``` -The snapshot builds are only available via JitPack and require adding the JitPack resolver, you need to specify specific commits to access those builds. -Stable releases are published to [maven-central](https://mvnrepository.com/artifact/net.dv8tion/JDA). - -If you do not need any opus de-/encoding done by JDA (voice receive/send with PCM) you can exclude `opus-java` entirely. -This can be done if you only send audio with an `AudioSendHandler` which only sends opus (`isOpus() = true`). (See [lavaplayer](https://github.com/sedmelluq/lavaplayer)) - -If you want to use a custom opus library you can provide the absolute path to `OpusLibrary.loadFrom(String)` before using -the audio API of JDA. This works without `opus-java-natives` as it only requires `opus-java-api`. -
_For this setup you should only exclude `opus-java-natives` as `opus-java-api` is a requirement for en-/decoding._ - -See [opus-java](https://github.com/discord-java/opus-java) - -### Logging Framework - SLF4J +This could then be used in code: -JDA is using [SLF4J](https://www.slf4j.org/) to log its messages. - -That means you should add some SLF4J implementation to your build path in addition to JDA. -If no implementation is found, following message will be printed to the console on startup: -``` -SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder". -SLF4J: Defaulting to no-operation (NOP) logger implementation -SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details. +```java +selfDestruct(channel, "Hello friend, this is my secret message").queue(); ``` -JDA currently provides a fallback Logger in case that no SLF4J implementation is present. -We strongly recommend to use one though, as that can improve speed and allows you to customize the Logger as well as log to files - -There is a guide for logback-classic available in our wiki: [Logging Setup](https://jda.wiki/setup/logging/) - -## Documentation - -Docs can be found on the [GitHub Pages][docs] -
We also have a wiki filled with information and troubleshooting guides at [jda.wiki][wiki] +## 🧩 Extensions -### Annotations - -We use a number of annotations to indicate future plans for implemented functionality such as new features of -the Discord API. - -- [Incubating](https://github.com/discord-jda/JDA/blob/master/src/main/java/net/dv8tion/jda/annotations/Incubating.java) -
This annotation is used to indicate that functionality may change in the future. Often used when a new feature is added. -- [ReplaceWith](https://github.com/discord-jda/JDA/blob/master/src/main/java/net/dv8tion/jda/annotations/ReplaceWith.java) -
Paired with `@Deprecated` this is used to inform you how the new code-fragment is supposed to look once the hereby annotated functionality is removed. -- [ForRemoval](https://github.com/discord-jda/JDA/blob/master/src/main/java/net/dv8tion/jda/annotations/ForRemoval.java) -
Paired with `@Deprecated` this indicates that we plan to entirely remove the hereby annotated functionality in the future. -- [DeprecatedSince](https://github.com/discord-jda/JDA/blob/master/src/main/java/net/dv8tion/jda/annotations/DeprecatedSince.java) -
Paired with `@Deprecated` this specifies when a feature was marked as deprecated. - -[Sources](https://github.com/discord-jda/JDA/tree/master/src/main/java/net/dv8tion/jda/annotations) - -## Getting Help +### [jda-ktx](https://github.com/MinnDevelopment/jda-ktx) -For general troubleshooting you can visit our wiki [Troubleshooting][troubleshooting] and [FAQ][faq]. -
If you need help, or just want to talk with the JDA or other Devs, you can join the [Official JDA Discord Guild][discord-invite]. +Created and maintained by [MinnDevelopment](https://github.com/MinnDevelopment). +Provides [Kotlin](https://kotlinlang.org/) extensions for **RestAction** and events that provide a more idiomatic Kotlin experience. -Alternatively you can also join the [Unofficial Discord API Guild](https://discord.gg/discord-api). -Once you joined, you can find JDA-specific help in the `#java_jda` channel. +```kotlin +fun main() { + val jda = light(BOT_TOKEN) + + jda.onCommand("ping") { event -> + val time = measureTime { + event.reply("Pong!").await() // suspending + }.inWholeMilliseconds -For guides and setup help you can also take a look at the [wiki](https://jda.wiki/) -
Especially interesting are the [Getting Started](https://jda.wiki/introduction/jda/) -and [Setup](https://jda.wiki/setup/intellij/) Pages. + event.hook.editOriginal("Pong: $time ms").queue() + } +} +``` -## Third Party Recommendations +There are a number of examples available in the [README](https://github.com/MinnDevelopment/jda-ktx/#jda-ktx). ### [Lavaplayer](https://github.com/lavalink-devs/lavaplayer) -Created by [sedmelluq](https://github.com/sedmelluq) and now maintained by the [lavalink community](https://github.com/lavalink-devs) -
Lavaplayer is the most popular library used by Music Bots created in Java. -It is highly compatible with JDA and Discord4J and allows to play audio from -Youtube, Soundcloud, Twitch, Bandcamp and [more providers](https://github.com/lavalink-devs/lavaplayer#supported-formats). -
The library can easily be expanded to more services by implementing your own AudioSourceManager and registering it. +Created by [sedmelluq](https://github.com/sedmelluq) and now maintained by the [lavalink community](https://github.com/lavalink-devs) +Lavaplayer is the most popular library used by Music Bots created in Java. +It is highly compatible with JDA and Discord4J and allows playing audio from +YouTube, Soundcloud, Twitch, Bandcamp and [more providers](https://github.com/lavalink-devs/lavaplayer#supported-formats). +The library can easily be expanded to more services by implementing your own AudioSourceManager and registering it. +We recommend to also use [udpqueue](#udpqueue-an-extension-of-jda-nas) in addition to lavaplayer, to avoid stuttering issues caused by GC pauses. It is recommended to read the [Usage](https://github.com/lavalink-devs/lavaplayer#usage) section of Lavaplayer -to understand a proper implementation. -
Sedmelluq provided a demo in his repository which presents an example implementation for JDA: +to understand a proper implementation. +Sedmelluq provided a demo in his repository which presents an example implementation for JDA: https://github.com/lavalink-devs/lavaplayer/tree/master/demo-jda -### [Lavalink](https://github.com/lavalink-devs/Lavalink) - -Created by [Freya Arbjerg](https://github.com/freyacodes) and now maintained by the [lavalink community](https://github.com/lavalink-devs). +### [udpqueue](https://github.com/MinnDevelopment/udpqueue.rs) (an extension of [jda-nas](https://github.com/sedmelluq/jda-nas)) -Lavalink is a popular standalone audio sending node based on Lavaplayer. Lavalink was built with scalability in mind, -and allows streaming music via many servers. It supports most of Lavaplayer's features. +Created and maintained by [sedmelluq](https://github.com/sedmelluq) and extended by [MinnDevelopment](https://github.com/MinnDevelopment) +Provides a native implementation for the JDA Audio Send-System **to avoid GC pauses potentially causing problems** with continuous audio playback. -Lavalink is used by many large bots, as well as bot developers who can not use a Java library like Lavaplayer. -If you plan on serving music on a smaller scale with JDA it is often preferable to just use Lavaplayer directly -as it is easier. - -[Lavalink-Client](https://github.com/FredBoat/Lavalink-Client) is the official Lavalink client for JDA. - - -### [jda-nas](https://github.com/sedmelluq/jda-nas) and [udpqueue](https://github.com/MinnDevelopment/udpqueue.rs) - -Created and maintained by [sedmelluq](https://github.com/sedmelluq) and extended by [MinnDevelopment](https://github.com/MinnDevelopment) -
Provides a native implementation for the JDA Audio Send-System to avoid GC pauses. - -Note that this send system creates an extra UDP-Client which causes audio receive to no longer function properly, +Note that this send-system creates an extra UDP-Client which causes audio receive to no longer function properly, since Discord identifies the sending UDP-Client as the receiver. ```java @@ -550,95 +310,31 @@ JDABuilder builder = JDABuilder.createDefault(BOT_TOKEN) .setAudioSendFactory(new NativeAudioSendFactory()); ``` -### [jda-ktx](https://github.com/MinnDevelopment/jda-ktx) - -Created and maintained by [MinnDevelopment](https://github.com/MinnDevelopment). -
Provides [Kotlin](https://kotlinlang.org/) extensions for **RestAction** and events that provide a more idiomatic Kotlin experience. - -```kotlin -fun main() { - val jda = light(BOT_TOKEN) - - jda.onCommand("ping") { event -> - val time = measureTime { - event.reply("Pong!").await() // suspending - }.inWholeMilliseconds +### [Lavalink](https://github.com/lavalink-devs/Lavalink) - event.hook.editOriginal("Pong: $time ms").queue() - } -} -``` +Created by [Freya Arbjerg](https://github.com/freyacodes) and now maintained by the [lavalink community](https://github.com/lavalink-devs). -There is a number of examples available in the [README](https://github.com/MinnDevelopment/jda-ktx/#jda-ktx). +Lavalink is a popular standalone audio sending node based on Lavaplayer. Lavalink was built with scalability in mind, +and allows streaming music via many servers. It supports most of Lavaplayer's features. ------- +Lavalink is used by many large bots, as well as bot developers who can not use a Java library like Lavaplayer. +If you plan on serving music on a smaller scale with JDA, it is often preferable to just use Lavaplayer directly +as it is easier. -More can be found in our github organization: [JDA-Applications](https://github.com/JDA-Applications) +[Lavalink-Client](https://github.com/FredBoat/Lavalink-Client) is the official Lavalink client for JDA. -## Contributing to JDA +## 🛠️ Contributing to JDA If you want to contribute to JDA, make sure to base your branch off of our **master** branch (or a feature-branch) and create your PR into that **same** branch. -More information can be found at the wiki page [Contributing](https://jda.wiki/contributing/contributing/). +Please follow our [Contributing Guidelines](https://github.com/discord-jda/JDA/blob/master/.github/CONTRIBUTING.md). -## Versioning and Deprecation Policy +Do not expect your pull request to get immediate attention, sometimes it will take a long time to get a response. +You can join our [discord server][discord-invite] and ask in [#lib-dev](https://discord.com/channels/125227483518861312/869965829024915466) before starting to work on a new PR, to get more immediate feedback from our community members. -Since the Discord API is in itself a moving standard, the stability is never guaranteed. For this reason, JDA does not follow the common semver versioning strategy. +## 🚨 Breaking Changes -The JDA version is structured with a looser definition, where the version change indicates the significance of changes. -For instance, using `5.1.2` as a baseline: - -- A change to the major like `6.0.0` indicates that a lot of code has to be adjusted due to major changes to the interfaces. A change like this always comes with a full migration guide like [Migrating from 4.X to 5.X](https://jda.wiki/introduction/migration-v4-v5/). -- A change to the minor like `5.2.0` indicates some code may need to be adjusted due to the removal or change of interfaces. You can usually find the necessary changes in the release documentation. -- A change to the patch like `5.1.3` indicates bug fixes and new feature additions that are backwards compatible. - -If a feature is marked as deprecated, it usually also indicates an alternative. For instance: - -```java -@Deprecated -@DeprecatedSince("5.1.2") -@ForRemoval(deadline="5.2.0") -@ReplaceWith("setFoo(foo)") -public void changeFoo(Foo foo) { ... } -``` +Due to the nature of the Discord API, the library will regularly introduce breaking changes to allow for a quick adoption of newer features. We try to keep these breaking changes minimal, but cannot avoid them entirely. -The method `changeFoo` was deprecated in release `5.1.2` and is going to be removed in `5.2.0`. Your change should replace all usage of `changeFoo(foo)` with `setFoo(foo)`. - -Sometimes, a feature might be removed without a replacement. This will be clearly explained in the documentation. - - -## Dependencies: - -This project requires **Java 8+**.
-All dependencies are managed automatically by Gradle. - * NV WebSocket Client - * Version: **2.14** - * [Github](https://github.com/TakahikoKawasaki/nv-websocket-client) - * OkHttp - * Version: **4.10.0** - * [Github](https://github.com/square/okhttp) - * Apache Commons Collections4 - * Version: **4.4** - * [Website](https://commons.apache.org/proper/commons-collections) - * jackson - * Version: **2.14.1** - * [Github](https://github.com/FasterXML/jackson) - * Trove4j - * Version: **3.0.3** - * [BitBucket](https://bitbucket.org/trove4j/trove) - * slf4j-api - * Version: **1.7.36** - * [Website](https://www.slf4j.org/) - * opus-java (optional) - * Version: **1.1.1** - * [GitHub](https://github.com/discord-java/opus-java) - -## Related Projects - -- [Discord4J](https://github.com/Discord4J/Discord4J) -- [Discord.NET](https://github.com/discord-net/Discord.Net) -- [discord.py](https://github.com/Rapptz/discord.py) -- [serenity](https://github.com/serenity-rs/serenity) - -**See also:** [Discord API Community Libraries](https://github.com/apacheli/discord-api-libs) +Most breaking changes will result in a **minor** version bump (`5.1.2` → `5.2.0`). diff --git a/build.gradle.kts b/build.gradle.kts index f547d7fc99..e0f7318ee2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,7 +28,7 @@ plugins { `maven-publish` id("io.github.gradle-nexus.publish-plugin") version "1.1.0" - id("com.github.johnrengelman.shadow") version "7.1.2" + id("com.github.johnrengelman.shadow") version "8.1.1" } @@ -107,33 +107,32 @@ dependencies { /* ABI dependencies */ //Code safety - compileOnly("com.google.code.findbugs:jsr305:3.0.2") - compileOnly("org.jetbrains:annotations:23.0.0") + compileOnly(libs.findbugs) + compileOnly(libs.jetbrains.annotations) //Logger - api("org.slf4j:slf4j-api:1.7.36") + api(libs.slf4j) //Web Connection Support - api("com.neovisionaries:nv-websocket-client:2.14") - api("com.squareup.okhttp3:okhttp:4.12.0") + api(libs.websocket.client) + api(libs.okhttp) //Opus library support - api("club.minnced:opus-java:1.1.1") + api(libs.opus) //Collections Utility - api("org.apache.commons:commons-collections4:4.4") + api(libs.commons.collections) //we use this only together with opus-java // if that dependency is excluded it also doesn't need jna anymore // since jna is a transitive runtime dependency of opus-java we don't include it explicitly as dependency - compileOnly("net.java.dev.jna:jna:4.4.0") + compileOnly(libs.jna) /* Internal dependencies */ //General Utility - implementation("net.sf.trove4j:trove4j:3.0.3") - implementation("com.fasterxml.jackson.core:jackson-core:2.16.0") - implementation("com.fasterxml.jackson.core:jackson-databind:2.16.0") + implementation(libs.trove4j) + implementation(libs.bundles.jackson) //Sets the dependencies for the examples configurations["examplesImplementation"].withDependencies { @@ -142,10 +141,11 @@ dependencies { addAll(configurations["compileOnly"].allDependencies) } - testImplementation("org.junit.jupiter:junit-jupiter:5.10.1") - testImplementation("org.reflections:reflections:0.10.2") - testImplementation("org.mockito:mockito-core:5.8.0") - testImplementation("org.assertj:assertj-core:3.25.3") + testImplementation(libs.junit) + testImplementation(libs.reflections) + testImplementation(libs.mockito) + testImplementation(libs.assertj) + testImplementation(libs.commons.lang3) } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index aa991fceae..48c0a02ca4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index 4ce7e83eb4..8f29929de9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,27 @@ rootProject.name = "JDA" + +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + version("jackson", "2.16.0") + library("jackson-core", "com.fasterxml.jackson.core", "jackson-core").versionRef("jackson") + library("jackson-databind", "com.fasterxml.jackson.core", "jackson-databind").versionRef("jackson") + bundle("jackson", listOf("jackson-core", "jackson-databind")) + + library("opus", "club.minnced", "opus-java" ).version("1.1.1") + library("findbugs", "com.google.code.findbugs", "jsr305" ).version("3.0.2") + library("websocket-client", "com.neovisionaries", "nv-websocket-client" ).version("2.14") + library("okhttp", "com.squareup.okhttp3", "okhttp" ).version("4.12.0") + library("jna", "net.java.dev.jna", "jna" ).version("5.14.0") + library("trove4j", "net.sf.trove4j", "core" ).version("3.1.0") + library("commons-collections", "org.apache.commons", "commons-collections4").version("4.4") + library("commons-lang3", "org.apache.commons", "commons-lang3" ).version("3.14.0") + library("assertj", "org.assertj", "assertj-core" ).version("3.25.3") + library("jetbrains-annotations", "org.jetbrains", "annotations" ).version("24.1.0") + library("junit", "org.junit.jupiter", "junit-jupiter" ).version("5.10.2") + library("mockito", "org.mockito", "mockito-core" ).version("5.11.0") + library("reflections", "org.reflections", "reflections" ).version("0.10.2") + library("slf4j", "org.slf4j", "slf4j-api" ).version("1.7.36") + } + } +} diff --git a/src/main/java/net/dv8tion/jda/api/Permission.java b/src/main/java/net/dv8tion/jda/api/Permission.java index 22e99f903d..1b15040d78 100644 --- a/src/main/java/net/dv8tion/jda/api/Permission.java +++ b/src/main/java/net/dv8tion/jda/api/Permission.java @@ -15,8 +15,6 @@ */ package net.dv8tion.jda.api; -import net.dv8tion.jda.annotations.ForRemoval; -import net.dv8tion.jda.annotations.ReplaceWith; import net.dv8tion.jda.internal.utils.Checks; import javax.annotation.Nonnull; @@ -33,19 +31,17 @@ public enum Permission // General Server / Channel Permissions MANAGE_CHANNEL( 4, true, true, "Manage Channels"), MANAGE_SERVER( 5, true, false, "Manage Server"), - VIEW_AUDIT_LOGS( 7, true, false, "View Audit Logs"), - VIEW_CHANNEL( 10, true, true, "View Channel(s)"), + VIEW_AUDIT_LOGS( 7, true, false, "View Audit Log"), + VIEW_CHANNEL( 10, true, true, "View Channels"), VIEW_GUILD_INSIGHTS( 19, true, false, "View Server Insights"), MANAGE_ROLES( 28, true, false, "Manage Roles"), MANAGE_PERMISSIONS( 28, false, true, "Manage Permissions"), MANAGE_WEBHOOKS( 29, true, true, "Manage Webhooks"), - @Deprecated - @ForRemoval(deadline = "5.0.0") - @ReplaceWith("MANAGE_GUILD_EXPRESSIONS") - MANAGE_EMOJIS_AND_STICKERS( 30, true, false, "Manage Emojis and Stickers"), - MANAGE_GUILD_EXPRESSIONS( 30, true, false, "Manage Emojis, Stickers, and Soundboards"), + MANAGE_GUILD_EXPRESSIONS( 30, true, false, "Manage Expressions"), MANAGE_EVENTS( 33, true, true, "Manage Events"), VIEW_CREATOR_MONETIZATION_ANALYTICS(41, true, false, "View Creator Analytics"), + CREATE_GUILD_EXPRESSIONS( 43, true, false, "Create Expressions"), + CREATE_SCHEDULED_EVENTS( 44, true, false, "Create Events"), // Membership Permissions CREATE_INSTANT_INVITE(0, true, true, "Create Instant Invite"), @@ -68,6 +64,7 @@ public enum Permission USE_APPLICATION_COMMANDS( 31, true, true, "Use Application Commands"), MESSAGE_EXT_STICKER( 37, true, true, "Use External Stickers"), MESSAGE_ATTACH_VOICE_MESSAGE(46, true, true, "Send Voice Messages"), + MESSAGE_SEND_POLLS( 49, true, true, "Create Polls"), // Thread Permissions MANAGE_THREADS( 34, true, true, "Manage Threads"), diff --git a/src/main/java/net/dv8tion/jda/api/entities/Message.java b/src/main/java/net/dv8tion/jda/api/entities/Message.java index d10b6cdd8d..15c4d4c07a 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/Message.java +++ b/src/main/java/net/dv8tion/jda/api/entities/Message.java @@ -31,6 +31,7 @@ import net.dv8tion.jda.api.entities.emoji.CustomEmoji; import net.dv8tion.jda.api.entities.emoji.Emoji; import net.dv8tion.jda.api.entities.emoji.RichCustomEmoji; +import net.dv8tion.jda.api.entities.messages.MessagePoll; import net.dv8tion.jda.api.entities.sticker.GuildSticker; import net.dv8tion.jda.api.entities.sticker.Sticker; import net.dv8tion.jda.api.entities.sticker.StickerItem; @@ -48,6 +49,7 @@ import net.dv8tion.jda.api.requests.restaction.MessageCreateAction; import net.dv8tion.jda.api.requests.restaction.MessageEditAction; import net.dv8tion.jda.api.requests.restaction.ThreadChannelAction; +import net.dv8tion.jda.api.requests.restaction.pagination.PollVotersPaginationAction; import net.dv8tion.jda.api.requests.restaction.pagination.ReactionPaginationAction; import net.dv8tion.jda.api.utils.AttachedFile; import net.dv8tion.jda.api.utils.AttachmentProxy; @@ -55,10 +57,12 @@ import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.api.utils.messages.MessageCreateData; import net.dv8tion.jda.api.utils.messages.MessageEditData; +import net.dv8tion.jda.api.utils.messages.MessagePollData; import net.dv8tion.jda.api.utils.messages.MessageRequest; import net.dv8tion.jda.internal.JDAImpl; import net.dv8tion.jda.internal.entities.ReceivedMessage; import net.dv8tion.jda.internal.requests.FunctionalCallback; +import net.dv8tion.jda.internal.requests.restaction.pagination.PollVotersPaginationActionImpl; import net.dv8tion.jda.internal.utils.Checks; import net.dv8tion.jda.internal.utils.Helpers; import net.dv8tion.jda.internal.utils.IOUtil; @@ -681,6 +685,43 @@ default String getGuildId() @Nonnull List getComponents(); + /** + * The {@link MessagePoll} attached to this message. + * + * @return Possibly-null poll instance for this message + * + * @see #endPoll() + */ + @Nullable + MessagePoll getPoll(); + + /** + * End the poll attached to this message. + * + * @throws IllegalStateException + * If this poll was not sent by the currently logged in account or no poll was attached to this message + * + * @return {@link AuditableRestAction} - Type: {@link Message} + */ + @Nonnull + @CheckReturnValue + AuditableRestAction endPoll(); + + /** + * Paginate the users who voted for a poll answer. + * + * @param answerId + * The id of the poll answer, usually the ordinal position of the answer (first is 1) + * + * @return {@link PollVotersPaginationAction} + */ + @Nonnull + @CheckReturnValue + default PollVotersPaginationAction retrievePollVoters(long answerId) + { + return new PollVotersPaginationActionImpl(getJDA(), getChannelId(), getId(), answerId); + } + /** * Rows of interactive components such as {@link Button Buttons}. *
You can use {@link MessageRequest#setComponents(LayoutComponent...)} to update these. @@ -1359,6 +1400,48 @@ default MessageCreateAction reply(@Nonnull MessageCreateData msg) return getChannel().sendMessage(msg).setMessageReference(this); } + /** + * Shortcut for {@code getChannel().sendMessagePoll(data).setMessageReference(this)}. + * + *

Possible {@link net.dv8tion.jda.api.requests.ErrorResponse ErrorResponses} include: + *

    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_CHANNEL UNKNOWN_CHANNEL} + *
    if this channel was deleted
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#CANNOT_SEND_TO_USER CANNOT_SEND_TO_USER} + *
    If this is a {@link PrivateChannel} and the currently logged in account + * does not share any Guilds with the recipient User
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MESSAGE_BLOCKED_BY_AUTOMOD MESSAGE_BLOCKED_BY_AUTOMOD} + *
    If this message was blocked by an {@link net.dv8tion.jda.api.entities.automod.AutoModRule AutoModRule}
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MESSAGE_BLOCKED_BY_HARMFUL_LINK_FILTER MESSAGE_BLOCKED_BY_HARMFUL_LINK_FILTER} + *
    If this message was blocked by the harmful link filter
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#POLL_INVALID_CHANNEL_TYPE POLL_INVALID_CHANNEL_TYPE} + *
    This channel does not allow polls
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#POLL_WITH_UNUSABLE_EMOJI POLL_WITH_UNUSABLE_EMOJI} + *
    This poll uses an external emoji that the bot is not allowed to use
  • + *
+ * + * @param poll + * The poll to send + * + * @throws InsufficientPermissionException + * If {@link MessageChannel#sendMessage(MessageCreateData)} throws + * @throws IllegalArgumentException + * If {@link MessageChannel#sendMessage(MessageCreateData)} throws + * + * @return {@link MessageCreateAction} + */ + @Nonnull + @CheckReturnValue + default MessageCreateAction replyPoll(@Nonnull MessagePollData poll) + { + return getChannel().sendMessagePoll(poll).setMessageReference(this); + } + /** * Shortcut for {@code getChannel().sendMessageEmbeds(embed, other).setMessageReference(this)}. * diff --git a/src/main/java/net/dv8tion/jda/api/entities/WebhookClient.java b/src/main/java/net/dv8tion/jda/api/entities/WebhookClient.java index 2956d91d39..5a81a73cd9 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/WebhookClient.java +++ b/src/main/java/net/dv8tion/jda/api/entities/WebhookClient.java @@ -32,6 +32,8 @@ import net.dv8tion.jda.api.utils.MiscUtil; import net.dv8tion.jda.api.utils.messages.MessageCreateData; import net.dv8tion.jda.api.utils.messages.MessageEditData; +import net.dv8tion.jda.api.utils.messages.MessagePollBuilder; +import net.dv8tion.jda.api.utils.messages.MessagePollData; import net.dv8tion.jda.internal.requests.IncomingWebhookClientImpl; import net.dv8tion.jda.internal.utils.Checks; @@ -128,6 +130,43 @@ public interface WebhookClient extends ISnowflake @CheckReturnValue WebhookMessageCreateAction sendMessage(@Nonnull MessageCreateData message); + /** + * Send a message poll to this webhook. + * + *

If this is an {@link InteractionHook InteractionHook} this method will be delayed until the interaction is acknowledged. + * + *

Possible {@link net.dv8tion.jda.api.requests.ErrorResponse ErrorResponses} include: + *

    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_WEBHOOK UNKNOWN_WEBHOOK} + *
    The webhook is no longer available, either it was deleted or in case of interactions it expired.
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MESSAGE_BLOCKED_BY_AUTOMOD MESSAGE_BLOCKED_BY_AUTOMOD} + *
    If this message was blocked by an {@link net.dv8tion.jda.api.entities.automod.AutoModRule AutoModRule}
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MESSAGE_BLOCKED_BY_HARMFUL_LINK_FILTER MESSAGE_BLOCKED_BY_HARMFUL_LINK_FILTER} + *
    If this message was blocked by the harmful link filter
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#POLL_INVALID_CHANNEL_TYPE POLL_INVALID_CHANNEL_TYPE} + *
    This channel does not allow polls
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#POLL_WITH_UNUSABLE_EMOJI POLL_WITH_UNUSABLE_EMOJI} + *
    This poll uses an external emoji that the bot is not allowed to use
  • + *
+ * + * @param poll + * The {@link MessagePollData} to send + * + * @throws IllegalArgumentException + * If null is provided + * + * @return {@link net.dv8tion.jda.api.requests.restaction.WebhookMessageCreateAction} + * + * @see MessagePollBuilder + */ + @Nonnull + @CheckReturnValue + WebhookMessageCreateAction sendMessagePoll(@Nonnull MessagePollData poll); + /** * Send a message to this webhook. * diff --git a/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java b/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java index c325f09390..4d4fc7314f 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java +++ b/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java @@ -35,6 +35,7 @@ import net.dv8tion.jda.api.requests.restaction.MessageEditAction; import net.dv8tion.jda.api.requests.restaction.pagination.MessagePaginationAction; import net.dv8tion.jda.api.requests.restaction.pagination.PaginationAction; +import net.dv8tion.jda.api.requests.restaction.pagination.PollVotersPaginationAction; import net.dv8tion.jda.api.requests.restaction.pagination.ReactionPaginationAction; import net.dv8tion.jda.api.utils.AttachedFile; import net.dv8tion.jda.api.utils.FileUpload; @@ -42,6 +43,7 @@ import net.dv8tion.jda.api.utils.data.DataArray; import net.dv8tion.jda.api.utils.messages.MessageCreateData; import net.dv8tion.jda.api.utils.messages.MessageEditData; +import net.dv8tion.jda.api.utils.messages.MessagePollData; import net.dv8tion.jda.internal.JDAImpl; import net.dv8tion.jda.internal.entities.EntityBuilder; import net.dv8tion.jda.internal.requests.RestActionImpl; @@ -49,6 +51,7 @@ import net.dv8tion.jda.internal.requests.restaction.MessageCreateActionImpl; import net.dv8tion.jda.internal.requests.restaction.MessageEditActionImpl; import net.dv8tion.jda.internal.requests.restaction.pagination.MessagePaginationActionImpl; +import net.dv8tion.jda.internal.requests.restaction.pagination.PollVotersPaginationActionImpl; import net.dv8tion.jda.internal.requests.restaction.pagination.ReactionPaginationActionImpl; import net.dv8tion.jda.internal.utils.Checks; @@ -644,6 +647,52 @@ default MessageCreateAction sendMessageComponents(@Nonnull CollectionPossible {@link net.dv8tion.jda.api.requests.ErrorResponse ErrorResponses} include: + *
    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_CHANNEL UNKNOWN_CHANNEL} + *
    if this channel was deleted
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#CANNOT_SEND_TO_USER CANNOT_SEND_TO_USER} + *
    If this is a {@link PrivateChannel} and the currently logged in account + * does not share any Guilds with the recipient User
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MESSAGE_BLOCKED_BY_AUTOMOD MESSAGE_BLOCKED_BY_AUTOMOD} + *
    If this message was blocked by an {@link net.dv8tion.jda.api.entities.automod.AutoModRule AutoModRule}
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MESSAGE_BLOCKED_BY_HARMFUL_LINK_FILTER MESSAGE_BLOCKED_BY_HARMFUL_LINK_FILTER} + *
    If this message was blocked by the harmful link filter
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#POLL_INVALID_CHANNEL_TYPE POLL_INVALID_CHANNEL_TYPE} + *
    This channel does not allow polls
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#POLL_WITH_UNUSABLE_EMOJI POLL_WITH_UNUSABLE_EMOJI} + *
    This poll uses an external emoji that the bot is not allowed to use
  • + *
+ * + * @param poll + * The poll to send + * + * @throws UnsupportedOperationException + * If this is a {@link PrivateChannel} and the recipient is a bot + * @throws IllegalArgumentException + * If the poll is null + * @throws net.dv8tion.jda.api.exceptions.InsufficientPermissionException + * If this is a {@link GuildMessageChannel} and this account does not have + * {@link net.dv8tion.jda.api.Permission#VIEW_CHANNEL Permission.VIEW_CHANNEL} or {@link net.dv8tion.jda.api.Permission#MESSAGE_SEND Permission.MESSAGE_SEND} + * + * @return {@link MessageCreateAction} + */ + @Nonnull + @CheckReturnValue + default MessageCreateAction sendMessagePoll(@Nonnull MessagePollData poll) + { + Checks.notNull(poll, "Poll"); + return new MessageCreateActionImpl(this).setPoll(poll); + } + /** * Send a message to this channel. * @@ -972,6 +1021,108 @@ default AuditableRestAction deleteMessageById(long messageId) return deleteMessageById(Long.toUnsignedString(messageId)); } + /** + * End the poll attached to this message. + * + *

A bot cannot expire the polls of other users. + * + *

The following {@link net.dv8tion.jda.api.requests.ErrorResponse ErrorResponses} are possible: + *

    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#INVALID_AUTHOR_EDIT INVALID_AUTHOR_EDIT} + *
    If the poll was sent by another user
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#CANNOT_EXPIRE_MISSING_POLL CANNOT_EXPIRE_MISSING_POLL} + *
    The message did not have a poll attached
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_MESSAGE UNKNOWN_MESSAGE} + *
    The message no longer exists
  • + *
+ * + * @param messageId + * The ID for the poll message + * + * @throws IllegalArgumentException + * If the provided messageId is not a valid snowflake + * + * @return {@link AuditableRestAction} - Type: {@link Message} + */ + @Nonnull + @CheckReturnValue + default AuditableRestAction endPollById(@Nonnull String messageId) + { + Checks.isSnowflake(messageId, "Message ID"); + return new AuditableRestActionImpl<>(getJDA(), Route.Messages.END_POLL.compile(getId(), messageId), (response, request) -> { + JDAImpl jda = (JDAImpl) getJDA(); + return jda.getEntityBuilder().createMessageWithChannel(response.getObject(), MessageChannel.this, false); + }); + } + + /** + * End the poll attached to this message. + * + *

A bot cannot expire the polls of other users. + * + *

The following {@link net.dv8tion.jda.api.requests.ErrorResponse ErrorResponses} are possible: + *

    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#INVALID_AUTHOR_EDIT INVALID_AUTHOR_EDIT} + *
    If the poll was sent by another user
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#CANNOT_EXPIRE_MISSING_POLL CANNOT_EXPIRE_MISSING_POLL} + *
    The message did not have a poll attached
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_MESSAGE UNKNOWN_MESSAGE} + *
    The message no longer exists
  • + *
+ * + * @param messageId + * The ID for the poll message + * + * @return {@link AuditableRestAction} - Type: {@link Message} + */ + @Nonnull + @CheckReturnValue + default AuditableRestAction endPollById(long messageId) + { + return endPollById(Long.toUnsignedString(messageId)); + } + + /** + * Paginate the users who voted for a poll answer. + * + * @param messageId + * The message id for the poll + * @param answerId + * The id of the poll answer, usually the ordinal position of the answer (first is 1) + * + * @throws IllegalArgumentException + * If the message id is not a valid snowflake + * + * @return {@link PollVotersPaginationAction} + */ + @Nonnull + @CheckReturnValue + default PollVotersPaginationAction retrievePollVotersById(@Nonnull String messageId, long answerId) + { + return new PollVotersPaginationActionImpl(getJDA(), getId(), messageId, answerId); + } + + /** + * Paginate the users who voted for a poll answer. + * + * @param messageId + * The message id for the poll + * @param answerId + * The id of the poll answer, usually the ordinal position of the answer (first is 1) + * + * @return {@link PollVotersPaginationAction} + */ + @Nonnull + @CheckReturnValue + default PollVotersPaginationAction retrievePollVotersById(long messageId, long answerId) + { + return new PollVotersPaginationActionImpl(getJDA(), getId(), Long.toUnsignedString(messageId), answerId); + } + /** * Creates a new {@link net.dv8tion.jda.api.entities.MessageHistory MessageHistory} object for each call of this method. *
MessageHistory is NOT an internal message cache, but rather it queries the Discord servers for previously sent messages. diff --git a/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePoll.java b/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePoll.java new file mode 100644 index 0000000000..f7bcef204d --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePoll.java @@ -0,0 +1,273 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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 net.dv8tion.jda.api.entities.messages; + +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.emoji.Emoji; +import net.dv8tion.jda.api.entities.emoji.EmojiUnion; +import net.dv8tion.jda.api.utils.messages.MessagePollBuilder; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.List; + +/** + * Poll sent with messages. + * + * @see Message#getPoll() + * @see Message#endPoll() + */ +public interface MessagePoll +{ + /** Maximum length of a {@link MessagePollBuilder#setTitle(String) poll question title} ({@value}) */ + int MAX_QUESTION_TEXT_LENGTH = 300; + /** Maximum length of a {@link MessagePollBuilder#addAnswer(String)} poll answer title} ({@value}) */ + int MAX_ANSWER_TEXT_LENGTH = 55; + /** Maximum amount of {@link MessagePollBuilder#addAnswer(String) poll answers} ({@value}) */ + int MAX_ANSWERS = 10; + /** Maximum {@link MessagePollBuilder#setDuration(Duration) duration} of poll ({@value}) */ + long MAX_DURATION_HOURS = 7 * 24; + + /** + * The layout of the poll. + * + * @return The poll layout, or {@link LayoutType#UNKNOWN} if unknown + */ + @Nonnull + LayoutType getLayout(); + + /** + * The poll question, representing the title. + * + * @return {@link Question} + */ + @Nonnull + Question getQuestion(); + + /** + * The poll answers. + * + *

Each answer also has the current {@link Answer#getVotes() votes}. + * The votes might not be finalized and might be incorrect before the poll has expired, + * see {@link #isFinalizedVotes()}. + * + * @return Immutable {@link List} of {@link Answer} + */ + @Nonnull + List getAnswers(); + + /** + * The time when this poll will automatically expire. + * + *

The author of the poll can always expire the poll manually, using {@link Message#endPoll()}. + * + * @return {@link OffsetDateTime} representing the time when the poll expires automatically, or null if it never expires + */ + @Nullable + OffsetDateTime getTimeExpiresAt(); + + /** + * Whether this poll allows multiple answers to be selected. + * + * @return True, if this poll allows multi selection + */ + boolean isMultiAnswer(); + + /** + * Whether this poll is finalized and recounted. + * + *

The votes for answers might be inaccurate due to eventual consistency, until this is true. + * Finalization does not mean the votes cannot change anymore, use {@link #isExpired()} to check if a poll has ended. + * + * @return True, if the votes have been precisely counted + */ + boolean isFinalizedVotes(); + + /** + * Whether this poll has passed its {@link #getTimeExpiresAt() expiration time}. + * + * @return True, if this poll is expired. + */ + default boolean isExpired() + { + return getTimeExpiresAt().isBefore(OffsetDateTime.now()); + } + + /** + * The question for a poll. + */ + class Question + { + private final String text; + private final EmojiUnion emoji; + + public Question(String text, Emoji emoji) + { + this.text = text; + this.emoji = (EmojiUnion) emoji; + } + + /** + * The poll question title. + * + *

Shown above all answers. + * + * @return The question title + */ + @Nonnull + public String getText() + { + return text; + } + + /** + * Possible emoji related to the poll question. + * + * @return Possibly-null emoji + */ + @Nullable + public EmojiUnion getEmoji() + { + return emoji; + } + } + + /** + * One of the answers for a poll. + * + *

Provides the current {@link #getVotes()} and whether you have voted for it. + */ + class Answer + { + private final long id; + private final String text; + private final EmojiUnion emoji; + private final int votes; + private final boolean selfVoted; + + public Answer(long id, String text, EmojiUnion emoji, int votes, boolean selfVoted) + { + this.id = id; + this.text = text; + this.emoji = emoji; + this.votes = votes; + this.selfVoted = selfVoted; + } + + /** + * The id of this answer. + * + * @return The answer id. + */ + public long getId() + { + return id; + } + + /** + * The text content of the answer. + * + * @return The answer label. + */ + @Nonnull + public String getText() + { + return text; + } + + /** + * The emoji assigned to this answer. + * + * @return {@link EmojiUnion} + */ + @Nullable + public EmojiUnion getEmoji() + { + return emoji; + } + + /** + * The number of votes this answer has received so far. + * + *

This might not be {@link #isFinalizedVotes() finalized}. + * + * @return The current number of votes + */ + public int getVotes() + { + return votes; + } + + /** + * Whether the answer was voted for by the currently logged in account. + * + * @return True, if the bot has voted for this. + */ + public boolean isSelfVoted() + { + return selfVoted; + } + } + + /** + * The poll layout. + * + *

Currently always {@link #DEFAULT}. + */ + enum LayoutType + { + DEFAULT(1), + UNKNOWN(-1); + + private final int key; + + LayoutType(int key) + { + this.key = key; + } + + /** + * The raw API key used to identify this layout. + * + * @return The API key + */ + public int getKey() + { + return key; + } + + /** + * Resolves the provided raw API key to the layout enum constant. + * + * @param key + * The API key + * + * @return The layout type or {@link #UNKNOWN} + */ + public static LayoutType fromKey(int key) + { + for (LayoutType type : values()) + { + if (type.key == key) + return type; + } + return UNKNOWN; + } + } +} diff --git a/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePollImpl.java b/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePollImpl.java new file mode 100644 index 0000000000..f5e6dbbb1d --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePollImpl.java @@ -0,0 +1,81 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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 net.dv8tion.jda.api.entities.messages; + +import javax.annotation.Nonnull; +import java.time.OffsetDateTime; +import java.util.List; + +public class MessagePollImpl implements MessagePoll +{ + private final LayoutType layout; + private final Question question; + private final List answers; + private final OffsetDateTime expiresAt; + private final boolean isMultiAnswer; + private final boolean isFinalizedVotes; + + public MessagePollImpl(LayoutType layout, Question question, List answers, OffsetDateTime expiresAt, boolean isMultiAnswer, boolean isFinalizedVotes) + { + this.layout = layout; + this.question = question; + this.answers = answers; + this.expiresAt = expiresAt; + this.isMultiAnswer = isMultiAnswer; + this.isFinalizedVotes = isFinalizedVotes; + } + + @Nonnull + @Override + public LayoutType getLayout() + { + return layout; + } + + @Nonnull + @Override + public Question getQuestion() + { + return question; + } + + @Nonnull + @Override + public List getAnswers() + { + return answers; + } + + @Nonnull + @Override + public OffsetDateTime getTimeExpiresAt() + { + return expiresAt; + } + + @Override + public boolean isMultiAnswer() + { + return isMultiAnswer; + } + + @Override + public boolean isFinalizedVotes() + { + return isFinalizedVotes; + } +} diff --git a/src/main/java/net/dv8tion/jda/api/events/message/poll/GenericMessagePollVoteEvent.java b/src/main/java/net/dv8tion/jda/api/events/message/poll/GenericMessagePollVoteEvent.java new file mode 100644 index 0000000000..f8ada658eb --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/events/message/poll/GenericMessagePollVoteEvent.java @@ -0,0 +1,137 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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 net.dv8tion.jda.api.events.message.poll; + +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.events.message.GenericMessageEvent; +import net.dv8tion.jda.api.requests.RestAction; + +import javax.annotation.CheckReturnValue; +import javax.annotation.Nonnull; + +/** + * Indicates that a poll vote was added/removed. + *
Every MessagePollVoteEvent is derived from this event and can be casted. + * + *

Can be used to detect both remove and add events. + * + *

Requirements
+ * + *

These events require at least one of the following intents (Will not fire at all if neither is enabled): + *

    + *
  • {@link net.dv8tion.jda.api.requests.GatewayIntent#GUILD_MESSAGE_POLLS GUILD_MESSAGE_POLLS} to work in guild text channels
  • + *
  • {@link net.dv8tion.jda.api.requests.GatewayIntent#DIRECT_MESSAGE_POLLS DIRECT_MESSAGE_POLLS} to work in private channels
  • + *
+ */ +public class GenericMessagePollVoteEvent extends GenericMessageEvent +{ + protected final long userId; + protected final long messageId; + protected final long answerId; + + public GenericMessagePollVoteEvent(@Nonnull MessageChannel channel, long responseNumber, long messageId, long userId, long answerId) + { + super(channel.getJDA(), responseNumber, messageId, channel); + this.userId = userId; + this.messageId = messageId; + this.answerId = answerId; + } + + /** + * The id of the voting user. + * + * @return The user id + */ + @Nonnull + public String getUserId() + { + return Long.toUnsignedString(userId); + } + + /** + * The id for the voting user. + * + * @return The user id + */ + public long getUserIdLong() + { + return userId; + } + + /** + * The id of the answer, usually the ordinal position. + *
The first answer options is usually 1. + * + * @return The answer id + */ + public long getAnswerId() + { + return answerId; + } + + /** + * Retrieves the voting {@link User}. + * + * @return {@link RestAction} - Type: {@link User} + */ + @Nonnull + @CheckReturnValue + public RestAction retrieveUser() + { + return getJDA().retrieveUserById(getUserIdLong()); + } + + /** + * Retrieves the voting {@link Member}. + * + *

Note that banning a member will also fire {@link MessagePollVoteRemoveEvent} and no member will be available + * in those cases. An {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_MEMBER UNKNOWN_MEMBER} error response + * should be the failure result. + * + * @throws IllegalStateException + * If this event is not from a guild + * + * @return {@link RestAction} - Type: {@link Member} + */ + @Nonnull + @CheckReturnValue + public RestAction retrieveMember() + { + if (!getChannel().getType().isGuild()) + throw new IllegalStateException("Cannot retrieve member for a vote that happened outside of a guild"); + return getGuild().retrieveMemberById(getUserIdLong()); + } + + /** + * Retrieves the message for this event. + *
Simple shortcut for {@code getChannel().retrieveMessageById(getMessageId())}. + * + *

The {@link Message#getMember() Message.getMember()} method will always return null for the resulting message. + * To retrieve the member you can use {@code getGuild().retrieveMember(message.getAuthor())}. + * + * @return {@link RestAction} - Type: {@link Message} + */ + @Nonnull + @CheckReturnValue + public RestAction retrieveMessage() + { + return getChannel().retrieveMessageById(getMessageId()); + } +} diff --git a/src/main/java/net/dv8tion/jda/api/events/message/poll/MessagePollVoteAddEvent.java b/src/main/java/net/dv8tion/jda/api/events/message/poll/MessagePollVoteAddEvent.java new file mode 100644 index 0000000000..24fa90eefb --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/events/message/poll/MessagePollVoteAddEvent.java @@ -0,0 +1,43 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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 net.dv8tion.jda.api.events.message.poll; + +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; + +import javax.annotation.Nonnull; + +/** + * Indicates that a user voted for a poll answer. + *
If the poll allows selecting multiple answers, one event per vote is sent. + * + *

Can be used to track when a user votes for a poll answer + * + *

Requirements
+ * + *

These events require at least one of the following intents (Will not fire at all if neither is enabled): + *

    + *
  • {@link net.dv8tion.jda.api.requests.GatewayIntent#GUILD_MESSAGE_POLLS GUILD_MESSAGE_POLLS} to work in guild text channels
  • + *
  • {@link net.dv8tion.jda.api.requests.GatewayIntent#DIRECT_MESSAGE_POLLS DIRECT_MESSAGE_POLLS} to work in private channels
  • + *
+ */ +public class MessagePollVoteAddEvent extends GenericMessagePollVoteEvent +{ + public MessagePollVoteAddEvent(@Nonnull MessageChannel channel, long responseNumber, long messageId, long userId, long answerId) + { + super(channel, responseNumber, messageId, userId, answerId); + } +} diff --git a/src/main/java/net/dv8tion/jda/api/events/message/poll/MessagePollVoteRemoveEvent.java b/src/main/java/net/dv8tion/jda/api/events/message/poll/MessagePollVoteRemoveEvent.java new file mode 100644 index 0000000000..5c261f88fe --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/events/message/poll/MessagePollVoteRemoveEvent.java @@ -0,0 +1,43 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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 net.dv8tion.jda.api.events.message.poll; + +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; + +import javax.annotation.Nonnull; + +/** + * Indicates that a user removed a vote for a poll answer. + *
If the poll allows selecting multiple answers, one event per vote is sent. + * + *

Can be used to track when a user removes a vote for a poll answer + * + *

Requirements
+ * + *

These events require at least one of the following intents (Will not fire at all if neither is enabled): + *

    + *
  • {@link net.dv8tion.jda.api.requests.GatewayIntent#GUILD_MESSAGE_POLLS GUILD_MESSAGE_POLLS} to work in guild text channels
  • + *
  • {@link net.dv8tion.jda.api.requests.GatewayIntent#DIRECT_MESSAGE_POLLS DIRECT_MESSAGE_POLLS} to work in private channels
  • + *
+ */ +public class MessagePollVoteRemoveEvent extends GenericMessagePollVoteEvent +{ + public MessagePollVoteRemoveEvent(@Nonnull MessageChannel channel, long responseNumber, long messageId, long userId, long answerId) + { + super(channel, responseNumber, messageId, userId, answerId); + } +} diff --git a/src/main/java/net/dv8tion/jda/api/hooks/ListenerAdapter.java b/src/main/java/net/dv8tion/jda/api/hooks/ListenerAdapter.java index fa764c405e..0ee9eb6160 100644 --- a/src/main/java/net/dv8tion/jda/api/hooks/ListenerAdapter.java +++ b/src/main/java/net/dv8tion/jda/api/hooks/ListenerAdapter.java @@ -61,6 +61,9 @@ import net.dv8tion.jda.api.events.interaction.command.*; import net.dv8tion.jda.api.events.interaction.component.*; import net.dv8tion.jda.api.events.message.*; +import net.dv8tion.jda.api.events.message.poll.GenericMessagePollVoteEvent; +import net.dv8tion.jda.api.events.message.poll.MessagePollVoteAddEvent; +import net.dv8tion.jda.api.events.message.poll.MessagePollVoteRemoveEvent; import net.dv8tion.jda.api.events.message.react.*; import net.dv8tion.jda.api.events.role.GenericRoleEvent; import net.dv8tion.jda.api.events.role.RoleCreateEvent; @@ -186,6 +189,8 @@ public void onMessageReactionAdd(@Nonnull MessageReactionAddEvent event) {} public void onMessageReactionRemove(@Nonnull MessageReactionRemoveEvent event) {} public void onMessageReactionRemoveAll(@Nonnull MessageReactionRemoveAllEvent event) {} public void onMessageReactionRemoveEmoji(@Nonnull MessageReactionRemoveEmojiEvent event) {} + public void onMessagePollVoteAdd(@Nonnull MessagePollVoteAddEvent event) {} + public void onMessagePollVoteRemove(@Nonnull MessagePollVoteRemoveEvent event) {} //PermissionOverride Events public void onPermissionOverrideDelete(@Nonnull PermissionOverrideDeleteEvent event) {} @@ -389,6 +394,7 @@ public void onGenericContextInteraction(@Nonnull GenericContextInteractionEvent< public void onGenericSelectMenuInteraction(@Nonnull GenericSelectMenuInteractionEvent event) {} public void onGenericMessage(@Nonnull GenericMessageEvent event) {} public void onGenericMessageReaction(@Nonnull GenericMessageReactionEvent event) {} + public void onGenericMessagePollVote(@Nonnull GenericMessagePollVoteEvent event) {} public void onGenericUser(@Nonnull GenericUserEvent event) {} public void onGenericUserPresence(@Nonnull GenericUserPresenceEvent event) {} public void onGenericUserUpdate(@Nonnull GenericUserUpdateEvent event) {} diff --git a/src/main/java/net/dv8tion/jda/api/interactions/callbacks/IReplyCallback.java b/src/main/java/net/dv8tion/jda/api/interactions/callbacks/IReplyCallback.java index 2988de9137..dbaca131ea 100644 --- a/src/main/java/net/dv8tion/jda/api/interactions/callbacks/IReplyCallback.java +++ b/src/main/java/net/dv8tion/jda/api/interactions/callbacks/IReplyCallback.java @@ -26,6 +26,8 @@ import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction; import net.dv8tion.jda.api.utils.FileUpload; import net.dv8tion.jda.api.utils.messages.MessageCreateData; +import net.dv8tion.jda.api.utils.messages.MessagePollBuilder; +import net.dv8tion.jda.api.utils.messages.MessagePollData; import net.dv8tion.jda.internal.requests.restaction.interactions.ReplyCallbackActionImpl; import net.dv8tion.jda.internal.utils.Checks; @@ -146,6 +148,53 @@ default ReplyCallbackAction reply(@Nonnull MessageCreateData message) return action.applyData(message); } + /** + * Reply to this interaction and acknowledge it. + *
This will send a reply message for this interaction. + * You can use {@link ReplyCallbackAction#setEphemeral(boolean) setEphemeral(true)} to only let the target user see the message. + * Replies are non-ephemeral by default. + * + *

You only have 3 seconds to acknowledge an interaction! + *
When the acknowledgement is sent after the interaction expired, you will receive {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_INTERACTION ErrorResponse.UNKNOWN_INTERACTION}. + *

If your handling can take longer than 3 seconds, due to various rate limits or other conditions, you should use {@link #deferReply()} instead. + * + *

Possible {@link net.dv8tion.jda.api.requests.ErrorResponse ErrorResponses} include: + *

    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_INTERACTION UNKNOWN_INTERACTION} + *
    If the interaction has already been acknowledged or timed out
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MESSAGE_BLOCKED_BY_AUTOMOD MESSAGE_BLOCKED_BY_AUTOMOD} + *
    If this message was blocked by an {@link net.dv8tion.jda.api.entities.automod.AutoModRule AutoModRule}
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MESSAGE_BLOCKED_BY_HARMFUL_LINK_FILTER MESSAGE_BLOCKED_BY_HARMFUL_LINK_FILTER} + *
    If this message was blocked by the harmful link filter
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#POLL_INVALID_CHANNEL_TYPE POLL_INVALID_CHANNEL_TYPE} + *
    This channel does not allow polls
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#POLL_WITH_UNUSABLE_EMOJI POLL_WITH_UNUSABLE_EMOJI} + *
    This poll uses an external emoji that the bot is not allowed to use
  • + *
+ * + * @param poll + * The {@link MessagePollData} to send + * + * @throws IllegalArgumentException + * If null is provided + * + * @return {@link ReplyCallbackAction} + * + * @see net.dv8tion.jda.api.utils.messages.MessageCreateBuilder MessageCreateBuilder + * @see MessagePollBuilder + */ + @Nonnull + @CheckReturnValue + default ReplyCallbackAction replyPoll(@Nonnull MessagePollData poll) + { + Checks.notNull(poll, "Message Poll"); + return deferReply().setPoll(poll); + } + /** * Reply to this interaction and acknowledge it. *
This will send a reply message for this interaction. diff --git a/src/main/java/net/dv8tion/jda/api/interactions/commands/CommandInteractionPayload.java b/src/main/java/net/dv8tion/jda/api/interactions/commands/CommandInteractionPayload.java index 36103c8c72..35b31f8454 100644 --- a/src/main/java/net/dv8tion/jda/api/interactions/commands/CommandInteractionPayload.java +++ b/src/main/java/net/dv8tion/jda/api/interactions/commands/CommandInteractionPayload.java @@ -149,6 +149,20 @@ default String getCommandString() for (OptionMapping o : getOptions()) { builder.append(" ").append(o.getName()).append(": "); + // Discord doesn't send the resolved entities on autocomplete interactions + if (this instanceof CommandAutoCompleteInteraction) + { + switch (o.getType()) + { + case CHANNEL: + case USER: + case ROLE: + case MENTIONABLE: + builder.append(o.getAsLong()); + continue; + } + } + switch (o.getType()) { case CHANNEL: diff --git a/src/main/java/net/dv8tion/jda/api/interactions/commands/build/SubcommandData.java b/src/main/java/net/dv8tion/jda/api/interactions/commands/build/SubcommandData.java index 60cfc3a3d2..4fee17fb19 100644 --- a/src/main/java/net/dv8tion/jda/api/interactions/commands/build/SubcommandData.java +++ b/src/main/java/net/dv8tion/jda/api/interactions/commands/build/SubcommandData.java @@ -67,14 +67,13 @@ public SubcommandData(@Nonnull String name, @Nonnull String description) protected void checkName(@Nonnull String name) { Checks.inRange(name, 1, 32, "Name"); - Checks.matches(name, Checks.ALPHANUMERIC_WITH_DASH, "Name"); Checks.isLowercase(name, "Name"); + Checks.matches(name, Checks.ALPHANUMERIC_WITH_DASH, "Name"); } protected void checkDescription(@Nonnull String description) { - Checks.notEmpty(description, "Description"); - Checks.notLonger(description, 100, "Description"); + Checks.inRange(description, 1, 100, "Description"); } /** diff --git a/src/main/java/net/dv8tion/jda/api/interactions/commands/build/SubcommandGroupData.java b/src/main/java/net/dv8tion/jda/api/interactions/commands/build/SubcommandGroupData.java index 36d519f964..7635b92906 100644 --- a/src/main/java/net/dv8tion/jda/api/interactions/commands/build/SubcommandGroupData.java +++ b/src/main/java/net/dv8tion/jda/api/interactions/commands/build/SubcommandGroupData.java @@ -58,12 +58,10 @@ public class SubcommandGroupData implements SerializableData */ public SubcommandGroupData(@Nonnull String name, @Nonnull String description) { - Checks.notEmpty(name, "Name"); - Checks.notEmpty(description, "Description"); - Checks.notLonger(name, 32, "Name"); - Checks.notLonger(description, 100, "Description"); - Checks.matches(name, Checks.ALPHANUMERIC_WITH_DASH, "Name"); + Checks.inRange(name, 1, 32, "Name"); Checks.isLowercase(name, "Name"); + Checks.matches(name, Checks.ALPHANUMERIC_WITH_DASH, "Name"); + Checks.inRange(description, 1, 100, "Description"); this.name = name; this.description = description; } diff --git a/src/main/java/net/dv8tion/jda/api/requests/ErrorResponse.java b/src/main/java/net/dv8tion/jda/api/requests/ErrorResponse.java index 450f4f9844..49de2c3448 100644 --- a/src/main/java/net/dv8tion/jda/api/requests/ErrorResponse.java +++ b/src/main/java/net/dv8tion/jda/api/requests/ErrorResponse.java @@ -164,6 +164,7 @@ public enum ErrorResponse SERVER_NOT_ENOUGH_BOOSTS( 50101, "This server needs more boosts to perform this action"), MIXED_PREMIUM_ROLES_FOR_EMOJI( 50144, "Cannot mix subscription and non subscription roles for an emoji"), ILLEGAL_EMOJI_CONVERSION( 50145, "Cannot convert between premium emoji and normal emoji"), + USER_MUST_BE_VERIFIED( 50178, "The user account must first be verified"), MFA_NOT_ENABLED( 60003, "MFA auth required but not enabled"), NO_USER_WITH_TAG_EXISTS( 80004, "No users with DiscordTag exist"), REACTION_BLOCKED( 90001, "Reaction Blocked"), @@ -185,6 +186,12 @@ public enum ErrorResponse TITLE_BLOCKED_BY_AUTOMOD( 200001, "Title was blocked by automatic moderation"), MESSAGE_BLOCKED_BY_HARMFUL_LINK_FILTER( 240000, "Message blocked by harmful links filter"), FAILED_TO_BAN_USERS( 500000, "Failed to ban users"), + POLL_VOTING_BLOCKED( 520000, "Poll voting blocked"), + POLL_EXPIRED( 520001, "Poll expired"), + POLL_INVALID_CHANNEL_TYPE( 520002, "Invalid channel type for poll creation"), + CANNOT_UPDATE_POLL_MESSAGE( 520003, "Cannot edit a poll message"), + POLL_WITH_UNUSABLE_EMOJI( 520004, "Cannot use an emoji included with the poll"), + CANNOT_EXPIRE_MISSING_POLL( 520006, "Cannot expire a non-poll message"), SERVER_ERROR( 0, "Discord encountered an internal server error! Not good!"); diff --git a/src/main/java/net/dv8tion/jda/api/requests/GatewayIntent.java b/src/main/java/net/dv8tion/jda/api/requests/GatewayIntent.java index c7eb6f6a9f..89814da397 100644 --- a/src/main/java/net/dv8tion/jda/api/requests/GatewayIntent.java +++ b/src/main/java/net/dv8tion/jda/api/requests/GatewayIntent.java @@ -194,6 +194,16 @@ public enum GatewayIntent */ AUTO_MODERATION_EXECUTION(21), + /** + * Events for poll votes in {@link net.dv8tion.jda.api.entities.Guild Guilds}. + */ + GUILD_MESSAGE_POLLS(24), + + /** + * Events for poll votes in {@link net.dv8tion.jda.api.entities.channel.concrete.PrivateChannel PrivateChannels}. + */ + DIRECT_MESSAGE_POLLS(25), + ; /** diff --git a/src/main/java/net/dv8tion/jda/api/requests/Route.java b/src/main/java/net/dv8tion/jda/api/requests/Route.java index a522a1943e..6c1adb4904 100644 --- a/src/main/java/net/dv8tion/jda/api/requests/Route.java +++ b/src/main/java/net/dv8tion/jda/api/requests/Route.java @@ -262,6 +262,9 @@ public static class Messages public static final Route GET_MESSAGE = new Route(GET, "channels/{channel_id}/messages/{message_id}"); public static final Route DELETE_MESSAGES = new Route(POST, "channels/{channel_id}/messages/bulk-delete"); + + public static final Route END_POLL = new Route(POST, "channels/{channel_id}/polls/{message_id}/expire"); + public static final Route GET_POLL_ANSWER_VOTERS = new Route(GET, "channels/{channel_id}/polls/{message_id}/answers/{answer_id}"); } public static class Invites diff --git a/src/main/java/net/dv8tion/jda/api/requests/restaction/pagination/PollVotersPaginationAction.java b/src/main/java/net/dv8tion/jda/api/requests/restaction/pagination/PollVotersPaginationAction.java new file mode 100644 index 0000000000..79d0aa28ab --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/requests/restaction/pagination/PollVotersPaginationAction.java @@ -0,0 +1,31 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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 net.dv8tion.jda.api.requests.restaction.pagination; + +import net.dv8tion.jda.api.entities.User; + +/** + * {@link PaginationAction PaginationAction} that paginates the votes for a poll answer. + * + *

Limits
+ * Minimum - 1
+ * Maximum - 1000 + *
Default - 1000 + */ +public interface PollVotersPaginationAction extends PaginationAction +{ +} diff --git a/src/main/java/net/dv8tion/jda/api/utils/messages/MessageCreateBuilder.java b/src/main/java/net/dv8tion/jda/api/utils/messages/MessageCreateBuilder.java index 44690c659b..afa82c5858 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/messages/MessageCreateBuilder.java +++ b/src/main/java/net/dv8tion/jda/api/utils/messages/MessageCreateBuilder.java @@ -59,6 +59,7 @@ public class MessageCreateBuilder extends AbstractMessageBuilder implements MessageCreateRequest { private final List files = new ArrayList<>(10); + private MessagePollData poll; private boolean tts; public MessageCreateBuilder() {} @@ -191,6 +192,21 @@ public List getAttachments() return Collections.unmodifiableList(files); } + @Nullable + @Override + public MessagePollData getPoll() + { + return poll; + } + + @Nonnull + @Override + public MessageCreateBuilder setPoll(@Nullable MessagePollData poll) + { + this.poll = poll; + return this; + } + @Nonnull @Override public MessageCreateBuilder addFiles(@Nonnull Collection files) @@ -222,7 +238,7 @@ public MessageCreateBuilder setSuppressedNotifications(boolean suppressed) @Override public boolean isEmpty() { - return Helpers.isBlank(content) && embeds.isEmpty() && files.isEmpty() && components.isEmpty(); + return Helpers.isBlank(content) && embeds.isEmpty() && files.isEmpty() && components.isEmpty() && poll == null; } @Override @@ -243,8 +259,8 @@ public MessageCreateData build() List components = new ArrayList<>(this.components); AllowedMentionsData mentions = this.mentions.copy(); - if (content.isEmpty() && embeds.isEmpty() && files.isEmpty() && components.isEmpty()) - throw new IllegalStateException("Cannot build an empty message. You need at least one of content, embeds, components, or files"); + if (content.isEmpty() && embeds.isEmpty() && files.isEmpty() && components.isEmpty() && poll == null) + throw new IllegalStateException("Cannot build an empty message. You need at least one of content, embeds, components, poll, or files"); int length = Helpers.codePointLength(content); if (length > Message.MAX_CONTENT_LENGTH) @@ -255,7 +271,7 @@ public MessageCreateData build() if (components.size() > Message.MAX_COMPONENT_COUNT) throw new IllegalStateException("Cannot build message with over " + Message.MAX_COMPONENT_COUNT + " component layouts, provided " + components.size()); - return new MessageCreateData(content, embeds, files, components, mentions, tts, messageFlags); + return new MessageCreateData(content, embeds, files, components, mentions, poll, tts, messageFlags); } @Nonnull diff --git a/src/main/java/net/dv8tion/jda/api/utils/messages/MessageCreateData.java b/src/main/java/net/dv8tion/jda/api/utils/messages/MessageCreateData.java index 77e5690ef0..43110c8c27 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/messages/MessageCreateData.java +++ b/src/main/java/net/dv8tion/jda/api/utils/messages/MessageCreateData.java @@ -27,6 +27,7 @@ import net.dv8tion.jda.internal.utils.IOUtil; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.*; /** @@ -44,19 +45,21 @@ public class MessageCreateData implements MessageData, AutoCloseable, Serializab private final List files; private final List components; private final AllowedMentionsData mentions; + private final MessagePollData poll; private final boolean tts; private final int flags; protected MessageCreateData( String content, List embeds, List files, List components, - AllowedMentionsData mentions, boolean tts, int flags) + AllowedMentionsData mentions, MessagePollData poll, boolean tts, int flags) { this.content = content; this.embeds = Collections.unmodifiableList(embeds); this.files = Collections.unmodifiableList(files); this.components = Collections.unmodifiableList(components); this.mentions = mentions; + this.poll = poll; this.tts = tts; this.flags = flags; } @@ -237,6 +240,17 @@ public List getAttachments() return getFiles(); } + /** + * The poll to send with the message + * + * @return The poll, or null if no poll is sent + */ + @Nullable + public MessagePollData getPoll() + { + return poll; + } + @Override public boolean isSuppressEmbeds() { @@ -316,6 +330,7 @@ public DataObject toData() { DataObject json = DataObject.empty(); json.put("content", content); + json.put("poll", poll); json.put("embeds", DataArray.fromCollection(embeds)); json.put("components", DataArray.fromCollection(components)); json.put("tts", tts); diff --git a/src/main/java/net/dv8tion/jda/api/utils/messages/MessageCreateRequest.java b/src/main/java/net/dv8tion/jda/api/utils/messages/MessageCreateRequest.java index 5f5d54f7d7..19c8ee9044 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/messages/MessageCreateRequest.java +++ b/src/main/java/net/dv8tion/jda/api/utils/messages/MessageCreateRequest.java @@ -27,6 +27,7 @@ import net.dv8tion.jda.internal.utils.Checks; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.io.File; import java.util.Arrays; import java.util.Collection; @@ -297,6 +298,27 @@ default R addFiles(@Nonnull FileUpload... files) @Override List getAttachments(); + /** + * The poll attached to this message + * + * @return The attached poll, or null if no poll is present + */ + @Nullable + MessagePollData getPoll(); + + /** + * Add a poll to this message. + * + * @param poll + * The poll to send + * + * @return The same instance for chaining + * + * @see MessagePollBuilder + */ + @Nonnull + R setPoll(@Nullable MessagePollData poll); + /** * Whether the message should use Text-to-Speech (TTS). * diff --git a/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollBuilder.java b/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollBuilder.java new file mode 100644 index 0000000000..14b4d3641c --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollBuilder.java @@ -0,0 +1,228 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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 net.dv8tion.jda.api.utils.messages; + +import net.dv8tion.jda.api.entities.emoji.Emoji; +import net.dv8tion.jda.api.entities.emoji.EmojiUnion; +import net.dv8tion.jda.api.entities.messages.MessagePoll; +import net.dv8tion.jda.internal.utils.Checks; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Builder for {@link MessagePollData} + * + * @see MessageCreateBuilder#setPoll(MessagePollData) + */ +public class MessagePollBuilder +{ + private final List answers = new ArrayList<>(MessagePoll.MAX_ANSWERS); + private MessagePoll.LayoutType layout = MessagePoll.LayoutType.DEFAULT; + private String title; + private Duration duration = Duration.ofHours(24); + private boolean isMultiAnswer; + + /** + * Create a new builder instance + * + * @param title + * The poll title (up to {@link MessagePoll#MAX_QUESTION_TEXT_LENGTH} characters) + * + * @throws IllegalArgumentException + * If the title is blank or longer than {@link MessagePoll#MAX_QUESTION_TEXT_LENGTH} characters + */ + public MessagePollBuilder(@Nonnull String title) + { + this.setTitle(title); + } + + /** + * They poll layout. + * + * @param layout + * The layout + * + * @throws IllegalArgumentException + * If null or {@link net.dv8tion.jda.api.entities.messages.MessagePoll.LayoutType#UNKNOWN UNKNOWN} is provided + * + * @return The updated builder + */ + @Nonnull + public MessagePollBuilder setLayout(@Nonnull MessagePoll.LayoutType layout) + { + Checks.notNull(layout, "Layout"); + Checks.check(layout != MessagePoll.LayoutType.UNKNOWN, "Layout cannot be UNKNOWN"); + + this.layout = layout; + return this; + } + + /** + * Change the title for this poll. + * + * @param title + * The poll title (up to {@link MessagePoll#MAX_QUESTION_TEXT_LENGTH} characters) + * + * @throws IllegalArgumentException + * If the title is blank or longer than {@link MessagePoll#MAX_QUESTION_TEXT_LENGTH} characters + * + * @return The updated builder + */ + @Nonnull + public MessagePollBuilder setTitle(@Nonnull String title) + { + Checks.notBlank(title, "Title"); + title = title.trim(); + Checks.notLonger(title, MessagePoll.MAX_QUESTION_TEXT_LENGTH, "Title"); + + this.title = title; + return this; + } + + /** + * Change the duration for this poll. + *
Default: {@code 1} day + * + *

The poll will automatically expire after this duration. + * + * @param duration + * The duration of this poll (in hours resolution) + * + * @throws IllegalArgumentException + * If the duration is null, less than 1 hour, or longer than {@value MessagePoll#MAX_DURATION_HOURS} hours (7 days) + * + * @return The updated builder + */ + @Nonnull + public MessagePollBuilder setDuration(@Nonnull Duration duration) + { + Checks.notNull(duration, "Duration"); + Checks.positive(duration.toHours(), "Duration"); + Checks.notLonger(duration, Duration.ofHours(MessagePoll.MAX_DURATION_HOURS), TimeUnit.HOURS, "Duration"); + + this.duration = duration; + return this; + } + + /** + * Change the duration for this poll. + *
Default: {@code 1} day + * + *

The poll will automatically expire after this duration. + * + * @param duration + * The duration of this poll (in hours resolution) + * @param unit + * The time unit for the duration + * + * @throws IllegalArgumentException + * If the time unit is null or the duration is not between 1 and {@value MessagePoll#MAX_DURATION_HOURS} hours (7 days) long + * + * @return The updated builder + */ + @Nonnull + public MessagePollBuilder setDuration(long duration, @Nonnull TimeUnit unit) + { + Checks.notNull(unit, "TimeUnit"); + return setDuration(Duration.ofHours(unit.toHours(duration))); + } + + /** + * Whether this poll allows selecting multiple answers. + *
Default: {@code false} + * + * @param multiAnswer + * True, if this poll should allow multiple answers + * + * @return The updated builder + */ + @Nonnull + public MessagePollBuilder setMultiAnswer(boolean multiAnswer) + { + isMultiAnswer = multiAnswer; + return this; + } + + /** + * Add an answer to this poll. + * + * @param title + * The answer title + * + * @throws IllegalArgumentException + * If the title is null, blank, or longer than {@value MessagePoll#MAX_ANSWER_TEXT_LENGTH} characters + * + * @return The updated builder + */ + @Nonnull + public MessagePollBuilder addAnswer(@Nonnull String title) + { + return addAnswer(title, null); + } + + /** + * Add an answer to this poll. + * + * @param title + * The answer title + * @param emoji + * Optional emoji to show next to the answer text + * + * @throws IllegalArgumentException + * If the title is null, blank, or longer than {@value MessagePoll#MAX_ANSWER_TEXT_LENGTH} characters + * + * @return The updated builder + */ + @Nonnull + public MessagePollBuilder addAnswer(@Nonnull String title, @Nullable Emoji emoji) + { + Checks.notBlank(title, "Answer title"); + title = title.trim(); + Checks.notLonger(title, MessagePoll.MAX_ANSWER_TEXT_LENGTH, "Answer title"); + Checks.check(this.answers.size() < MessagePoll.MAX_ANSWERS, "Poll cannot have more than %d answers", MessagePoll.MAX_ANSWERS); + + this.answers.add(new MessagePoll.Answer(this.answers.size() + 1, title, (EmojiUnion) emoji, 0, false)); + return this; + } + + /** + * Build the poll data. + * + * @throws IllegalStateException + * If no answers have been added to the builder + * + * @return {@link MessagePollData} + */ + @Nonnull + public MessagePollData build() + { + if (answers.isEmpty()) + throw new IllegalStateException("Cannot build a poll without answers"); + return new MessagePollData( + layout, + new MessagePoll.Question(title, null), + new ArrayList<>(answers), + duration, + isMultiAnswer + ); + } +} diff --git a/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollData.java b/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollData.java new file mode 100644 index 0000000000..4d71a28c2f --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollData.java @@ -0,0 +1,106 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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 net.dv8tion.jda.api.utils.messages; + +import net.dv8tion.jda.api.entities.messages.MessagePoll; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.api.utils.data.SerializableData; +import net.dv8tion.jda.internal.utils.Helpers; +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nonnull; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * A poll that can be attached to a {@link MessageCreateRequest}. + * + *

Example
+ *

{@code
+ * channel.sendMessage("Hello guys! Check my poll:")
+ *   .setPoll(
+ *     MessagePollData.builder("Which programming language is better?")
+ *       .addAnswer("Java", Emoji.fromFormatted("<:java:1006323566314274856>"))
+ *       .addAnswer("Kotlin", Emoji.fromFormatted("<:kotlin:295940257797636096>"))
+ *       .build())
+ *   .queue()
+ * }
+ * + * @see #builder(String) + * @see MessageCreateBuilder#setPoll(MessagePollData) + */ +public class MessagePollData implements SerializableData +{ + private final MessagePoll.LayoutType layout; + private final MessagePoll.Question question; + private final List answers; + private final Duration duration; + private final boolean isMultiAnswer; + + public MessagePollData(MessagePoll.LayoutType layout, MessagePoll.Question question, List answers, Duration duration, boolean isMultiAnswer) + { + this.layout = layout; + this.question = question; + this.answers = answers; + this.duration = duration; + this.isMultiAnswer = isMultiAnswer; + } + + /** + * Creates a new {@link MessagePollBuilder}. + * + *

A poll must have at least one answer. + * + * @param title + * The poll title (up to {@value MessagePoll#MAX_QUESTION_TEXT_LENGTH} characters) + * + * @throws IllegalArgumentException + * If the title is blank or longer than {@value MessagePoll#MAX_QUESTION_TEXT_LENGTH} characters + * + * @return {@link MessagePollBuilder} + */ + @Nonnull + public static MessagePollBuilder builder(@Nonnull String title) + { + return new MessagePollBuilder(title); + } + + @NotNull + @Override + public DataObject toData() + { + DataObject data = DataObject.empty(); + + data.put("duration", TimeUnit.SECONDS.toHours(duration.getSeconds())); + data.put("allow_multiselect", isMultiAnswer); + data.put("layout_type", layout.getKey()); + + data.put("question", DataObject.empty() + .put("text", question.getText())); + + data.put("answers", answers.stream() + .map(answer -> DataObject.empty() + .put("answer_id", answer.getId()) + .put("poll_media", DataObject.empty() + .put("text", answer.getText()) + .put("emoji", answer.getEmoji()))) + .collect(Helpers.toDataArray())); + + return data; + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/entities/AbstractWebhookClient.java b/src/main/java/net/dv8tion/jda/internal/entities/AbstractWebhookClient.java index 1e636eb5e0..386e43c337 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/AbstractWebhookClient.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/AbstractWebhookClient.java @@ -29,6 +29,7 @@ import net.dv8tion.jda.api.utils.FileUpload; import net.dv8tion.jda.api.utils.messages.MessageCreateData; import net.dv8tion.jda.api.utils.messages.MessageEditData; +import net.dv8tion.jda.api.utils.messages.MessagePollData; import net.dv8tion.jda.internal.requests.restaction.WebhookMessageCreateActionImpl; import net.dv8tion.jda.internal.requests.restaction.WebhookMessageDeleteActionImpl; import net.dv8tion.jda.internal.requests.restaction.WebhookMessageEditActionImpl; @@ -102,6 +103,14 @@ public WebhookMessageCreateAction sendMessage(@Nonnull MessageCreateData mess return sendRequest().applyData(message); } + @Nonnull + @Override + public WebhookMessageCreateAction sendMessagePoll(@Nonnull MessagePollData poll) + { + Checks.notNull(poll, "Message Poll"); + return sendRequest().setPoll(poll); + } + @Nonnull @Override public WebhookMessageCreateAction sendFiles(@Nonnull Collection files) diff --git a/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java b/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java index 97d7258a77..3c69ee7ecb 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java @@ -39,8 +39,11 @@ import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.entities.emoji.Emoji; import net.dv8tion.jda.api.entities.emoji.EmojiUnion; import net.dv8tion.jda.api.entities.emoji.RichCustomEmoji; +import net.dv8tion.jda.api.entities.messages.MessagePoll; +import net.dv8tion.jda.api.entities.messages.MessagePollImpl; import net.dv8tion.jda.api.entities.sticker.*; import net.dv8tion.jda.api.entities.templates.Template; import net.dv8tion.jda.api.entities.templates.TemplateChannel; @@ -1836,6 +1839,8 @@ else if (MISSING_CHANNEL.equals(ex.getMessage())) ); } + MessagePoll poll = jsonObject.optObject("poll").map(EntityBuilder::createMessagePoll).orElse(null); + // Message Components List components = Collections.emptyList(); Optional componentsArrayOpt = jsonObject.optArray("components"); @@ -1866,7 +1871,7 @@ else if (MISSING_CHANNEL.equals(ex.getMessage())) int position = jsonObject.getInt("position", -1); return new ReceivedMessage(id, channelId, guildId, api, guild, channel, type, messageReference, fromWebhook, applicationId, tts, pinned, - content, nonce, user, member, activity, editTime, mentions, reactions, attachments, embeds, stickers, components, flags, + content, nonce, user, member, activity, poll, editTime, mentions, reactions, attachments, embeds, stickers, components, flags, messageInteraction, startedThread, position); } @@ -1897,6 +1902,47 @@ private static MessageActivity createMessageActivity(DataObject jsonObject) return new MessageActivity(activityType, partyId, application); } + public static MessagePollImpl createMessagePoll(DataObject data) + { + MessagePoll.LayoutType layout = MessagePoll.LayoutType.fromKey(data.getInt("layout_type")); + OffsetDateTime expiresAt = data.isNull("expiry") ? null : data.getOffsetDateTime("expiry"); + boolean isMultiAnswer = data.getBoolean("allow_multiselect"); + + DataArray answersData = data.getArray("answers"); + DataObject questionData = data.getObject("question"); + + DataObject resultsData = data.optObject("results").orElseGet( + () -> DataObject.empty().put("answer_counts", DataArray.empty()) // FIXME: Discord bug + ); + boolean isFinalized = resultsData.getBoolean("is_finalized"); + + DataArray resultVotes = resultsData.getArray("answer_counts"); + TLongObjectMap voteMapping = new TLongObjectHashMap<>(); + resultVotes.stream(DataArray::getObject) + .forEach(votes -> voteMapping.put(votes.getLong("id"), votes)); + + MessagePoll.Question question = new MessagePoll.Question( + questionData.getString("text"), + questionData.optObject("emoji").map(Emoji::fromData).orElse(null)); + + List answers = answersData.stream(DataArray::getObject) + .map(answer -> { + long answerId = answer.getLong("answer_id"); + DataObject media = answer.getObject("poll_media"); + DataObject votes = voteMapping.get(answerId); + return new MessagePoll.Answer( + answerId, + media.getString("text"), + media.optObject("emoji").map(Emoji::fromData).orElse(null), + votes != null ? votes.getInt("count") : 0, + votes != null && votes.getBoolean("me_voted") + ); + }) + .collect(Helpers.toUnmodifiableList()); + + return new MessagePollImpl(layout, question, answers, expiresAt, isMultiAnswer, isFinalized); + } + public MessageReaction createMessageReaction(MessageChannel chan, long channelId, long messageId, DataObject obj) { DataObject emoji = obj.getObject("emoji"); diff --git a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java index e0dedbf78b..9965195cd2 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java @@ -82,7 +82,6 @@ import net.dv8tion.jda.internal.utils.concurrent.task.GatewayTask; import okhttp3.MediaType; import okhttp3.MultipartBody; -import org.jetbrains.annotations.NotNull; import javax.annotation.CheckReturnValue; import javax.annotation.Nonnull; @@ -1553,7 +1552,7 @@ public AuditableRestAction ban(@Nonnull Collection reactions, List attachments, List embeds, List stickers, List components, int flags, Message.Interaction interaction, ThreadChannel startedThread, int position) @@ -151,6 +153,7 @@ public ReceivedMessage( this.interaction = interaction; this.startedThread = startedThread; this.position = position; + this.poll = poll; } private void checkSystem(String comment) @@ -613,6 +616,29 @@ public List getComponents() return components; } + @Override + public MessagePoll getPoll() + { + checkIntent(); + return poll; + } + + @Nonnull + @Override + public AuditableRestAction endPoll() + { + checkUser(); + if (poll == null) + throw new IllegalStateException("This message does not contain a poll"); + return new AuditableRestActionImpl<>(getJDA(), Route.Messages.END_POLL.compile(getChannelId(), getId()), (response, request) -> { + JDAImpl jda = (JDAImpl) getJDA(); + EntityBuilder entityBuilder = jda.getEntityBuilder(); + if (hasChannel()) + return entityBuilder.createMessageWithChannel(response.getObject(), channel, false); + return entityBuilder.createMessageFromWebhook(response.getObject(), hasGuild() ? getGuild() : null); + }); + } + @Nonnull @Override public Mentions getMentions() diff --git a/src/main/java/net/dv8tion/jda/internal/entities/channel/mixin/middleman/MessageChannelMixin.java b/src/main/java/net/dv8tion/jda/internal/entities/channel/mixin/middleman/MessageChannelMixin.java index 8f33c5990d..e2992ae842 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/channel/mixin/middleman/MessageChannelMixin.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/channel/mixin/middleman/MessageChannelMixin.java @@ -40,7 +40,9 @@ import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.api.utils.messages.MessageCreateData; import net.dv8tion.jda.api.utils.messages.MessageEditData; +import net.dv8tion.jda.api.utils.messages.MessagePollData; import net.dv8tion.jda.internal.requests.RestActionImpl; +import org.jetbrains.annotations.NotNull; import javax.annotation.CheckReturnValue; import javax.annotation.Nonnull; @@ -155,6 +157,33 @@ default MessageCreateAction sendMessageEmbeds(@Nonnull Collection components) + { + checkCanAccessChannel(); + checkCanSendMessage(); + return MessageChannelUnion.super.sendMessageComponents(components); + } + + @Nonnull + @Override + default MessageCreateAction sendMessagePoll(@Nonnull MessagePollData poll) + { + checkCanAccessChannel(); + checkCanSendMessage(); + return MessageChannelUnion.super.sendMessagePoll(poll); + } + @Nonnull @CheckReturnValue default MessageCreateAction sendMessage(@Nonnull MessageCreateData msg) diff --git a/src/main/java/net/dv8tion/jda/internal/handle/MessagePollVoteHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/MessagePollVoteHandler.java new file mode 100644 index 0000000000..1e38887ce9 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/handle/MessagePollVoteHandler.java @@ -0,0 +1,84 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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 net.dv8tion.jda.internal.handle; + +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.events.message.poll.MessagePollVoteAddEvent; +import net.dv8tion.jda.api.events.message.poll.MessagePollVoteRemoveEvent; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.JDAImpl; +import net.dv8tion.jda.internal.requests.WebSocketClient; + +public class MessagePollVoteHandler extends SocketHandler +{ + private final boolean add; + + public MessagePollVoteHandler(JDAImpl api, boolean add) + { + super(api); + this.add = add; + } + + @Override + protected Long handleInternally(DataObject content) + { + long answerId = content.getLong("answer_id"); + long userId = content.getUnsignedLong("user_id"); + long messageId = content.getUnsignedLong("message_id"); + long channelId = content.getUnsignedLong("channel_id"); + long guildId = content.getUnsignedLong("guild_id", 0); + + if (api.getGuildSetupController().isLocked(guildId)) + return guildId; + + Guild guild = api.getGuildById(guildId); + MessageChannel channel = api.getChannelById(MessageChannel.class, channelId); + if (channel == null) + { + if (guild != null) + { + GuildChannel actual = guild.getGuildChannelById(channelId); + if (actual != null) + { + WebSocketClient.LOG.debug("Dropping message poll vote event for unexpected channel of type {}", actual.getType()); + return null; + } + } + + if (guildId != 0) + { + api.getEventCache().cache(EventCache.Type.CHANNEL, channelId, responseNumber, allContent, this::handle); + EventCache.LOG.debug("Received a vote for a channel that JDA does not currently have cached"); + return null; + } + + channel = getJDA().getEntityBuilder().createPrivateChannel( + DataObject.empty() + .put("id", channelId) + ); + } + + if (add) + api.handleEvent(new MessagePollVoteAddEvent(channel, responseNumber, messageId, userId, answerId)); + else + api.handleEvent(new MessagePollVoteRemoveEvent(channel, responseNumber, messageId, userId, answerId)); + + return null; + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/interactions/CommandDataImpl.java b/src/main/java/net/dv8tion/jda/internal/interactions/CommandDataImpl.java index c3bf85f68b..88bc202595 100644 --- a/src/main/java/net/dv8tion/jda/internal/interactions/CommandDataImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/interactions/CommandDataImpl.java @@ -87,8 +87,7 @@ public void checkName(@Nonnull String name) public void checkDescription(@Nonnull String description) { checkType(Command.Type.SLASH, "set description"); - Checks.notEmpty(description, "Description"); - Checks.notLonger(description, MAX_DESCRIPTION_LENGTH, "Description"); + Checks.inRange(description, 1, MAX_DESCRIPTION_LENGTH, "Description"); } @Nonnull diff --git a/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java b/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java index 08f24d1ad5..924c45724a 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java @@ -1395,6 +1395,8 @@ protected void setupHandlers() handlers.put("MESSAGE_REACTION_REMOVE", new MessageReactionHandler(api, false)); handlers.put("MESSAGE_REACTION_REMOVE_ALL", new MessageReactionBulkRemoveHandler(api)); handlers.put("MESSAGE_REACTION_REMOVE_EMOJI", new MessageReactionClearEmojiHandler(api)); + handlers.put("MESSAGE_POLL_VOTE_ADD", new MessagePollVoteHandler(api, true)); + handlers.put("MESSAGE_POLL_VOTE_REMOVE", new MessagePollVoteHandler(api, false)); handlers.put("MESSAGE_UPDATE", new MessageUpdateHandler(api)); handlers.put("PRESENCE_UPDATE", new PresenceUpdateHandler(api)); handlers.put("READY", new ReadyHandler(api)); diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/MessageCreateActionImpl.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/MessageCreateActionImpl.java index 6f80cd8167..6e76e56a5b 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/restaction/MessageCreateActionImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/MessageCreateActionImpl.java @@ -79,7 +79,7 @@ protected RequestBody finalizeData() { if (!stickers.isEmpty()) return getRequestBody(DataObject.empty().put("sticker_ids", stickers)); - throw new IllegalStateException("Cannot build empty messages! Must provide at least one of: content, embed, file, or stickers"); + throw new IllegalStateException("Cannot build empty messages! Must provide at least one of: content, embed, file, poll, or stickers"); } try (MessageCreateData data = builder.build()) diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/pagination/PollVotersPaginationActionImpl.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/pagination/PollVotersPaginationActionImpl.java new file mode 100644 index 0000000000..5ac181ee7b --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/pagination/PollVotersPaginationActionImpl.java @@ -0,0 +1,85 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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 net.dv8tion.jda.internal.requests.restaction.pagination; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.exceptions.ParsingException; +import net.dv8tion.jda.api.requests.Request; +import net.dv8tion.jda.api.requests.Response; +import net.dv8tion.jda.api.requests.Route; +import net.dv8tion.jda.api.requests.restaction.pagination.PollVotersPaginationAction; +import net.dv8tion.jda.api.utils.data.DataArray; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.entities.EntityBuilder; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +public class PollVotersPaginationActionImpl extends PaginationActionImpl implements PollVotersPaginationAction +{ + public PollVotersPaginationActionImpl(JDA jda, String channelId, String messageId, long answerId) + { + super(jda, Route.Messages.GET_POLL_ANSWER_VOTERS.compile(channelId, messageId, Long.toString(answerId)), 1, 1000, 1000); + this.order = PaginationOrder.FORWARD; + } + + @NotNull + @Override + public EnumSet getSupportedOrders() + { + return EnumSet.of(PaginationOrder.FORWARD); + } + + @Override + protected long getKey(User it) + { + return it.getIdLong(); + } + + @Override + protected void handleSuccess(Response response, Request> request) + { + DataArray array = response.getObject().getArray("users"); + List users = new ArrayList<>(array.length()); + EntityBuilder builder = api.getEntityBuilder(); + for (int i = 0; i < array.length(); i++) + { + try + { + DataObject object = array.getObject(i); + users.add(builder.createUser(object)); + } + catch(ParsingException | NullPointerException e) + { + LOG.warn("Encountered an exception in PollVotersPaginationAction", e); + } + } + + if (!users.isEmpty()) + { + if (useCache) + cached.addAll(users); + last = users.get(users.size() - 1); + lastKey = last.getIdLong(); + } + + request.onSuccess(users); + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/utils/Checks.java b/src/main/java/net/dv8tion/jda/internal/utils/Checks.java index 5e913595b8..edd1fe10fd 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/Checks.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/Checks.java @@ -28,7 +28,9 @@ import org.intellij.lang.annotations.PrintFormat; import org.jetbrains.annotations.Contract; +import java.time.Duration; import java.util.*; +import java.util.concurrent.TimeUnit; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; @@ -212,6 +214,18 @@ public static void notNegative(final long n, final String name) throw new IllegalArgumentException(name + " may not be negative"); } + public static void notLonger(final Duration duration, final Duration maxDuration, final TimeUnit resolutionUnit, final String name) + { + notNull(duration, name); + check( + duration.compareTo(maxDuration) <= 0, + "%s may not be longer than %s. Provided: %s", + name, + JDALogger.getLazyString(() -> Helpers.durationToString(maxDuration, resolutionUnit)), + JDALogger.getLazyString(() -> Helpers.durationToString(duration, resolutionUnit)) + ); + } + // Unique streams checks public static void checkUnique(Stream stream, String format, BiFunction getArgs) diff --git a/src/main/java/net/dv8tion/jda/internal/utils/Helpers.java b/src/main/java/net/dv8tion/jda/internal/utils/Helpers.java index b8a7051213..703f7d1e84 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/Helpers.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/Helpers.java @@ -26,6 +26,7 @@ import java.time.format.DateTimeFormatter; import java.time.temporal.TemporalAccessor; import java.util.*; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.ToLongFunction; import java.util.stream.Collector; @@ -316,4 +317,27 @@ public static boolean hasCause(Throwable throwable, Class c { return Collector.of(DataArray::empty, DataArray::add, DataArray::addAll); } + + public static String durationToString(Duration duration, TimeUnit resolutionUnit) + { + long actual = resolutionUnit.convert(duration.getSeconds(), TimeUnit.SECONDS); + String raw = actual + " " + resolutionUnit.toString().toLowerCase(Locale.ROOT); + + long days = duration.toDays(); + long hours = duration.toHours() % 24; + long minutes = duration.toMinutes() % 60; + long seconds = duration.getSeconds() - TimeUnit.DAYS.toSeconds(days) - TimeUnit.HOURS.toSeconds(hours) - TimeUnit.MINUTES.toSeconds(minutes); + + StringJoiner joiner = new StringJoiner(" "); + if (days > 0) + joiner.add(days + " days"); + if (hours > 0) + joiner.add(hours + " hours"); + if (minutes > 0) + joiner.add(minutes + " minutes"); + if (seconds > 0) + joiner.add(seconds + " seconds"); + + return raw + " (" + joiner + ")"; + } } diff --git a/src/main/java/net/dv8tion/jda/internal/utils/message/MessageCreateBuilderMixin.java b/src/main/java/net/dv8tion/jda/internal/utils/message/MessageCreateBuilderMixin.java index af8f154de3..7cf498d989 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/message/MessageCreateBuilderMixin.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/message/MessageCreateBuilderMixin.java @@ -21,6 +21,7 @@ import net.dv8tion.jda.api.utils.FileUpload; import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder; import net.dv8tion.jda.api.utils.messages.MessageCreateRequest; +import net.dv8tion.jda.api.utils.messages.MessagePollData; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -62,6 +63,21 @@ default R addFiles(@Nonnull Collection files) return (R) this; } + @Nullable + @Override + default MessagePollData getPoll() + { + return getBuilder().getPoll(); + } + + @Nonnull + @Override + default R setPoll(@Nullable MessagePollData poll) + { + getBuilder().setPoll(poll); + return (R) this; + } + @Nonnull @Override default R setTTS(boolean tts) diff --git a/src/test/java/net/dv8tion/jda/test/ChecksHelper.java b/src/test/java/net/dv8tion/jda/test/ChecksHelper.java new file mode 100644 index 0000000000..142f2792d9 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/ChecksHelper.java @@ -0,0 +1,96 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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 net.dv8tion.jda.test; + +import net.dv8tion.jda.test.assertions.checks.*; +import org.junit.jupiter.api.function.ThrowingConsumer; + +import java.time.Duration; +import java.util.regex.Pattern; + +public class ChecksHelper +{ + public static String tooLongError(String name, int maxLength, String value) + { + return name + " may not be longer than " + maxLength + " characters! Provided: \"" + value + "\""; + } + + public static String notInRangeError(String name, int minLength, int maxLength, String value) + { + return name + " must be between " + minLength + " and " + maxLength + " characters long! Provided: \"" + value + "\""; + } + + public static String isNullError(String name) + { + return name + " may not be null"; + } + + public static String isEmptyError(String name) + { + return name + " may not be empty"; + } + + public static String isBlankError(String name) + { + return name + " may not be blank"; + } + + public static String isNotLowercase(String name, String value) + { + return name + " must be lowercase only! Provided: \"" + value + "\""; + } + + public static String notRegexMatch(String name, Pattern pattern, String value) + { + return name + " must match regex ^" + pattern + "$. Provided: \"" + value + "\""; + } + + public static String isNegativeError(String name) + { + return name + " may not be negative"; + } + + public static String notPositiveError(String name) + { + return name + " may not be negative or zero"; + } + + public static StringChecksAssertions assertStringChecks(String name, ThrowingConsumer callable) + { + return new StringChecksAssertions(name, callable); + } + + public static > EnumChecksAssertions assertEnumChecks(String name, ThrowingConsumer callable) + { + return new EnumChecksAssertions<>(name, callable); + } + + public static DurationChecksAssertions assertDurationChecks(String name, ThrowingConsumer callable) + { + return new DurationChecksAssertions(name, callable); + } + + public static LongChecksAssertions assertLongChecks(String name, ThrowingConsumer callable) + { + return new LongChecksAssertions(name, callable); + } + + public static SimpleChecksAssertions assertChecks(String name, ThrowingConsumer callable) + { + return new SimpleChecksAssertions<>(name, callable); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/Constants.java b/src/test/java/net/dv8tion/jda/test/Constants.java index 0e1c4c9ef4..4dc3e449bf 100644 --- a/src/test/java/net/dv8tion/jda/test/Constants.java +++ b/src/test/java/net/dv8tion/jda/test/Constants.java @@ -19,6 +19,7 @@ public interface Constants { long GUILD_ID = 125227483518861312L; + long CHANNEL_ID = 125227483518861312L; long MINN_USER_ID = 86699011792191488L; long BUTLER_USER_ID = 150203841827045376L; } diff --git a/src/test/java/net/dv8tion/jda/test/IntegrationTest.java b/src/test/java/net/dv8tion/jda/test/IntegrationTest.java index a884a14a06..ebb0952457 100644 --- a/src/test/java/net/dv8tion/jda/test/IntegrationTest.java +++ b/src/test/java/net/dv8tion/jda/test/IntegrationTest.java @@ -25,6 +25,7 @@ import net.dv8tion.jda.internal.entities.EntityBuilder; import net.dv8tion.jda.internal.requests.Requester; import net.dv8tion.jda.internal.requests.RestActionImpl; +import net.dv8tion.jda.test.assertions.restaction.RestActionAssertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestInfo; diff --git a/src/test/java/net/dv8tion/jda/test/assertions/checks/AbstractChecksAssertions.java b/src/test/java/net/dv8tion/jda/test/assertions/checks/AbstractChecksAssertions.java new file mode 100644 index 0000000000..f8f964a7c4 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/assertions/checks/AbstractChecksAssertions.java @@ -0,0 +1,48 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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 net.dv8tion.jda.test.assertions.checks; + +import org.junit.jupiter.api.function.ThrowingConsumer; + +import static net.dv8tion.jda.test.ChecksHelper.isNullError; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +public class AbstractChecksAssertions> +{ + protected final String name; + protected final ThrowingConsumer callable; + + public AbstractChecksAssertions(String name, ThrowingConsumer callable) + { + this.name = name; + this.callable = callable; + } + + public S checksNotNull() + { + return throwsFor(null, isNullError(name)); + } + + @SuppressWarnings("unchecked") + public S throwsFor(T input, String expectedError) + { + assertThatIllegalArgumentException() + .isThrownBy(() -> callable.accept(input)) + .withMessage(expectedError); + return (S) this; + } +} diff --git a/src/test/java/net/dv8tion/jda/test/assertions/checks/DurationChecksAssertions.java b/src/test/java/net/dv8tion/jda/test/assertions/checks/DurationChecksAssertions.java new file mode 100644 index 0000000000..ba50121459 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/assertions/checks/DurationChecksAssertions.java @@ -0,0 +1,57 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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 net.dv8tion.jda.test.assertions.checks; + +import org.junit.jupiter.api.function.ThrowingConsumer; + +import java.time.Duration; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +import static net.dv8tion.jda.internal.utils.Helpers.durationToString; +import static net.dv8tion.jda.test.ChecksHelper.isNegativeError; +import static net.dv8tion.jda.test.ChecksHelper.notPositiveError; + +public class DurationChecksAssertions extends AbstractChecksAssertions +{ + public DurationChecksAssertions(String name, ThrowingConsumer callable) + { + super(name, callable); + } + + public DurationChecksAssertions checksNotNegative() + { + throwsFor(Duration.ofSeconds(-1), isNegativeError(name)); + return this; + } + + public DurationChecksAssertions checksPositive() + { + throwsFor(Duration.ofSeconds(-1), notPositiveError(name)); + throwsFor(Duration.ZERO, notPositiveError(name)); + return this; + } + + public DurationChecksAssertions checksNotLonger(Duration maxDuration, TimeUnit resolution) + { + Duration input = maxDuration.plusSeconds(resolution.toSeconds(1)); + throwsFor(input, + String.format(Locale.ROOT, "%s may not be longer than %s. Provided: %s", + name, durationToString(maxDuration, resolution), durationToString(input, resolution))); + return this; + } +} diff --git a/src/test/java/net/dv8tion/jda/test/assertions/checks/EnumChecksAssertions.java b/src/test/java/net/dv8tion/jda/test/assertions/checks/EnumChecksAssertions.java new file mode 100644 index 0000000000..1ca7712fdf --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/assertions/checks/EnumChecksAssertions.java @@ -0,0 +1,33 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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 net.dv8tion.jda.test.assertions.checks; + +import org.junit.jupiter.api.function.ThrowingConsumer; + +public class EnumChecksAssertions> extends AbstractChecksAssertions> +{ + public EnumChecksAssertions(String name, ThrowingConsumer callable) + { + super(name, callable); + } + + public EnumChecksAssertions checkIsNot(E variant) + { + throwsFor(variant, name + " cannot be " + variant); + return this; + } +} diff --git a/src/test/java/net/dv8tion/jda/test/assertions/checks/LongChecksAssertions.java b/src/test/java/net/dv8tion/jda/test/assertions/checks/LongChecksAssertions.java new file mode 100644 index 0000000000..6ba7b8bafc --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/assertions/checks/LongChecksAssertions.java @@ -0,0 +1,36 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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 net.dv8tion.jda.test.assertions.checks; + +import org.junit.jupiter.api.function.ThrowingConsumer; + +import static net.dv8tion.jda.test.ChecksHelper.notPositiveError; + +public class LongChecksAssertions extends AbstractChecksAssertions +{ + public LongChecksAssertions(String name, ThrowingConsumer callable) + { + super(name, callable); + } + + public LongChecksAssertions checksPositive() + { + throwsFor( 0L, notPositiveError(name)); + throwsFor( -1L, notPositiveError(name)); + return this; + } +} diff --git a/src/test/java/net/dv8tion/jda/test/assertions/checks/SimpleChecksAssertions.java b/src/test/java/net/dv8tion/jda/test/assertions/checks/SimpleChecksAssertions.java new file mode 100644 index 0000000000..91441c2660 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/assertions/checks/SimpleChecksAssertions.java @@ -0,0 +1,27 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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 net.dv8tion.jda.test.assertions.checks; + +import org.junit.jupiter.api.function.ThrowingConsumer; + +public class SimpleChecksAssertions extends AbstractChecksAssertions> +{ + public SimpleChecksAssertions(String name, ThrowingConsumer callable) + { + super(name, callable); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/assertions/checks/StringChecksAssertions.java b/src/test/java/net/dv8tion/jda/test/assertions/checks/StringChecksAssertions.java new file mode 100644 index 0000000000..91e89207fd --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/assertions/checks/StringChecksAssertions.java @@ -0,0 +1,75 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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 net.dv8tion.jda.test.assertions.checks; + +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.function.ThrowingConsumer; + +import java.util.regex.Pattern; + +import static net.dv8tion.jda.test.ChecksHelper.*; + +public class StringChecksAssertions extends AbstractChecksAssertions +{ + public StringChecksAssertions(String name, ThrowingConsumer callable) + { + super(name, callable); + } + + public StringChecksAssertions checksNotEmpty() + { + throwsFor(null, isNullError(name)); + throwsFor("", isEmptyError(name)); + return this; + } + + public StringChecksAssertions checksNotBlank() + { + throwsFor(null, isNullError(name)); + throwsFor("", isBlankError(name)); + throwsFor(" ", isBlankError(name)); + return this; + } + + public StringChecksAssertions checksNotLonger(int maxLength) + { + String invalidInput = StringUtils.repeat("s", maxLength + 1); + throwsFor(invalidInput, tooLongError(name, maxLength, invalidInput)); + return this; + } + + public StringChecksAssertions checksLowercaseOnly() + { + throwsFor("InvalidCasing", isNotLowercase(name, "InvalidCasing")); + return this; + } + + public StringChecksAssertions checksRange(int minLength, int maxLength) + { + String tooLong = StringUtils.repeat("s", maxLength + 1); + String tooShort = StringUtils.repeat("s", minLength - 1); + throwsFor(tooShort, notInRangeError(name, minLength, maxLength, tooShort)); + throwsFor(tooLong, notInRangeError(name, minLength, maxLength, tooLong)); + return this; + } + + public StringChecksAssertions checksRegex(String input, Pattern regex) + { + throwsFor(input, notRegexMatch(name, regex, input)); + return this; + } +} diff --git a/src/test/java/net/dv8tion/jda/test/assertions/events/EventFiredAssertions.java b/src/test/java/net/dv8tion/jda/test/assertions/events/EventFiredAssertions.java new file mode 100644 index 0000000000..3cb518678c --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/assertions/events/EventFiredAssertions.java @@ -0,0 +1,61 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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 net.dv8tion.jda.test.assertions.events; + +import net.dv8tion.jda.internal.JDAImpl; +import org.junit.jupiter.api.function.ThrowingConsumer; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.assertArg; +import static org.mockito.Mockito.*; + +public class EventFiredAssertions +{ + private final Class eventType; + private final JDAImpl jda; + private final List> assertions = new ArrayList<>(); + + public EventFiredAssertions(Class eventType, JDAImpl jda) + { + this.eventType = eventType; + this.jda = jda; + } + + public EventFiredAssertions hasGetterWithValueEqualTo(Function getter, V value) + { + assertions.add(event -> assertThat(getter.apply(event)).isEqualTo(value)); + return this; + } + + public void isFiredBy(Runnable runnable) + { + doNothing().when(jda).handleEvent(assertArg(arg -> { + assertThat(arg).isInstanceOf(eventType); + T casted = eventType.cast(arg); + for (ThrowingConsumer assertion : assertions) + assertion.accept(casted); + })); + + runnable.run(); + + verify(jda, times(1)).handleEvent(any()); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/RestActionAssertions.java b/src/test/java/net/dv8tion/jda/test/assertions/restaction/RestActionAssertions.java similarity index 99% rename from src/test/java/net/dv8tion/jda/test/RestActionAssertions.java rename to src/test/java/net/dv8tion/jda/test/assertions/restaction/RestActionAssertions.java index 37f9f24518..3e12458291 100644 --- a/src/test/java/net/dv8tion/jda/test/RestActionAssertions.java +++ b/src/test/java/net/dv8tion/jda/test/assertions/restaction/RestActionAssertions.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.dv8tion.jda.test; +package net.dv8tion.jda.test.assertions.restaction; import net.dv8tion.jda.api.requests.Method; import net.dv8tion.jda.api.requests.Request; diff --git a/src/test/java/net/dv8tion/jda/test/entities/MessageSerializationTest.java b/src/test/java/net/dv8tion/jda/test/entities/MessageSerializationTest.java index 8d4bb8ec33..87e28a2072 100644 --- a/src/test/java/net/dv8tion/jda/test/entities/MessageSerializationTest.java +++ b/src/test/java/net/dv8tion/jda/test/entities/MessageSerializationTest.java @@ -28,7 +28,6 @@ public class MessageSerializationTest { - private static final String DESCRIPTION_TEXT = "Description Text"; private static final String TITLE_TEXT = "Title Text"; private static final String TITLE_URL = "https://example.com/title"; diff --git a/src/test/java/net/dv8tion/jda/test/entities/guild/BulkBanTest.java b/src/test/java/net/dv8tion/jda/test/entities/guild/BulkBanTest.java index 8192df657b..d5d126d338 100644 --- a/src/test/java/net/dv8tion/jda/test/entities/guild/BulkBanTest.java +++ b/src/test/java/net/dv8tion/jda/test/entities/guild/BulkBanTest.java @@ -36,6 +36,7 @@ import java.util.stream.Collectors; import java.util.stream.LongStream; +import static net.dv8tion.jda.test.ChecksHelper.assertDurationChecks; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -70,6 +71,10 @@ void testInvalidInputs() { hasPermission(true); + assertDurationChecks("Deletion timeframe", duration -> guild.ban(Collections.emptyList(), duration)) + .checksNotNegative() + .throwsFor(Duration.ofDays(100), "Deletion timeframe must not be larger than 7 days. Provided: 8640000 seconds"); + Set users = Collections.singleton(null); assertThatIllegalArgumentException() @@ -78,12 +83,7 @@ void testInvalidInputs() assertThatIllegalArgumentException() .isThrownBy(() -> guild.ban(null, null).queue()) .withMessage("Users may not be null"); - assertThatIllegalArgumentException() - .isThrownBy(() -> guild.ban(Collections.emptyList(), Duration.ofSeconds(-1)).queue()) - .withMessage("Deletion time cannot be negative"); - assertThatIllegalArgumentException() - .isThrownBy(() -> guild.ban(Collections.emptyList(), Duration.ofDays(100)).queue()) - .withMessage("Deletion timeframe must not be larger than 7 days. Provided: 8640000 seconds"); + assertThatIllegalArgumentException() .isThrownBy(() -> guild.ban( diff --git a/src/test/java/net/dv8tion/jda/test/entities/message/MessagePollDataTest.java b/src/test/java/net/dv8tion/jda/test/entities/message/MessagePollDataTest.java new file mode 100644 index 0000000000..6cd1f67b5b --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/entities/message/MessagePollDataTest.java @@ -0,0 +1,75 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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 net.dv8tion.jda.test.entities.message; + +import net.dv8tion.jda.api.entities.messages.MessagePoll; +import net.dv8tion.jda.api.utils.messages.MessagePollBuilder; +import net.dv8tion.jda.test.ChecksHelper; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import static net.dv8tion.jda.test.ChecksHelper.*; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +public class MessagePollDataTest +{ + @Test + void testInvalidInputs() + { + assertStringChecks("Title", MessagePollBuilder::new) + .checksNotNull() + .checksNotBlank() + .checksNotLonger(300); + + MessagePollBuilder builder = new MessagePollBuilder("test title"); + + assertEnumChecks("Layout", builder::setLayout) + .checksNotNull() + .checkIsNot(MessagePoll.LayoutType.UNKNOWN); + + assertDurationChecks("Duration", builder::setDuration) + .checksNotNull() + .checksPositive() + .checksNotLonger(Duration.ofHours(7 * 24), TimeUnit.HOURS); + + ChecksHelper.assertChecks("TimeUnit", (unit) -> builder.setDuration(1, unit)) + .checksNotNull(); + + assertLongChecks("Duration", (duration) -> builder.setDuration(duration, TimeUnit.SECONDS)) + .checksPositive() + .throwsFor(TimeUnit.DAYS.toSeconds(8), "Duration may not be longer than 168 hours (7 days). Provided: 192 hours (8 days)"); + + assertStringChecks("Answer title", builder::addAnswer) + .checksNotNull() + .checksNotBlank() + .checksNotLonger(55); + + assertThatIllegalStateException() + .isThrownBy(builder::build) + .withMessage("Cannot build a poll without answers"); + + for (int i = 0; i < 10; i++) + builder.addAnswer("Answer " + i); + + assertThatIllegalArgumentException() + .isThrownBy(() -> builder.addAnswer("Answer " + 10)) + .withMessage("Poll cannot have more than 10 answers"); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/entities/message/PollVotersPaginationTest.java b/src/test/java/net/dv8tion/jda/test/entities/message/PollVotersPaginationTest.java new file mode 100644 index 0000000000..57010d81c6 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/entities/message/PollVotersPaginationTest.java @@ -0,0 +1,59 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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 net.dv8tion.jda.test.entities.message; + +import net.dv8tion.jda.api.requests.Method; +import net.dv8tion.jda.internal.requests.restaction.pagination.PollVotersPaginationActionImpl; +import net.dv8tion.jda.test.IntegrationTest; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +public class PollVotersPaginationTest extends IntegrationTest +{ + private PollVotersPaginationActionImpl newAction() + { + return new PollVotersPaginationActionImpl(jda, "381886978205155338", "1228092239079804968", 5); + } + + @Test + void testDefaults() + { + assertThatRequestFrom(newAction()) + .hasMethod(Method.GET) + .hasCompiledRoute("channels/381886978205155338/polls/1228092239079804968/answers/5?limit=1000&after=0") + .whenQueueCalled(); + } + + @Test + void testSkipTo() + { + long randomId = random.nextLong(); + assertThatRequestFrom(newAction().skipTo(randomId)) + .hasMethod(Method.GET) + .hasQueryParams("limit", "1000", "after", Long.toUnsignedString(randomId)) + .whenQueueCalled(); + } + + @Test + void testOrder() + { + assertThatIllegalArgumentException() + .isThrownBy(() -> newAction().reverse()) + .withMessage("Cannot use PaginationOrder.BACKWARD for this pagination endpoint."); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/events/AbstractSocketHandlerTest.java b/src/test/java/net/dv8tion/jda/test/events/AbstractSocketHandlerTest.java new file mode 100644 index 0000000000..686a00df44 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/events/AbstractSocketHandlerTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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 net.dv8tion.jda.test.events; + +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.handle.GuildSetupController; +import net.dv8tion.jda.test.Constants; +import net.dv8tion.jda.test.IntegrationTest; +import net.dv8tion.jda.test.assertions.events.EventFiredAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.mockito.Mock; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +public class AbstractSocketHandlerTest extends IntegrationTest +{ + @Mock + protected GuildSetupController setupController; + @Mock + protected Guild guild; + + @BeforeEach + final void setupHandlerContext() + { + when(jda.getGuildSetupController()).thenReturn(setupController); + when(setupController.isLocked(anyLong())).thenReturn(false); + when(jda.getGuildById(eq(Constants.GUILD_ID))).thenReturn(guild); + } + + protected DataObject event(String type, DataObject data) + { + return DataObject.empty() + .put("s", 1) + .put("op", 0) + .put("t", type) + .put("d", data); + } + + protected EventFiredAssertions assertThatEvent(Class eventType) + { + return new EventFiredAssertions<>(eventType, jda); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/events/MessagePollHandlerTests.java b/src/test/java/net/dv8tion/jda/test/events/MessagePollHandlerTests.java new file mode 100644 index 0000000000..e3acc34a37 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/events/MessagePollHandlerTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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 net.dv8tion.jda.test.events; + +import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.events.message.poll.MessagePollVoteAddEvent; +import net.dv8tion.jda.api.events.message.poll.MessagePollVoteRemoveEvent; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.handle.MessagePollVoteHandler; +import net.dv8tion.jda.test.Constants; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +public class MessagePollHandlerTests extends AbstractSocketHandlerTest +{ + @Mock + protected GuildMessageChannel channel; + + @BeforeEach + final void setupMessageContext() + { + when(jda.getChannelById(eq(MessageChannel.class), eq(Constants.CHANNEL_ID))).thenReturn(channel); + } + + @Test + void testMinimalVoteAdd() + { + MessagePollVoteHandler handler = new MessagePollVoteHandler(jda, true); + + String messageId = randomSnowflake(); + + assertThatEvent(MessagePollVoteAddEvent.class) + .hasGetterWithValueEqualTo(MessagePollVoteAddEvent::getMessageId, messageId) + .hasGetterWithValueEqualTo(MessagePollVoteAddEvent::getAnswerId, 1L) + .hasGetterWithValueEqualTo(MessagePollVoteAddEvent::getUserIdLong, Constants.MINN_USER_ID) + .isFiredBy(() -> { + handler.handle(random.nextLong(), event("MESSAGE_POLL_VOTE_ADD", DataObject.empty() + .put("answer_id", 1) + .put("message_id", messageId) + .put("channel_id", Constants.CHANNEL_ID) + .put("user_id", Constants.MINN_USER_ID))); + }); + } + + @Test + void testMinimalVoteRemove() + { + MessagePollVoteHandler handler = new MessagePollVoteHandler(jda, false); + + String messageId = randomSnowflake(); + + assertThatEvent(MessagePollVoteRemoveEvent.class) + .hasGetterWithValueEqualTo(MessagePollVoteRemoveEvent::getMessageId, messageId) + .hasGetterWithValueEqualTo(MessagePollVoteRemoveEvent::getAnswerId, 1L) + .hasGetterWithValueEqualTo(MessagePollVoteRemoveEvent::getUserIdLong, Constants.MINN_USER_ID) + .isFiredBy(() -> { + handler.handle(random.nextLong(), event("MESSAGE_POLL_VOTE_REMOVE", DataObject.empty() + .put("answer_id", 1) + .put("message_id", messageId) + .put("channel_id", Constants.CHANNEL_ID) + .put("user_id", Constants.MINN_USER_ID))); + }); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/interactions/CommandDataTest.java b/src/test/java/net/dv8tion/jda/test/interactions/CommandDataTest.java index 1dae4f186c..e568089b5d 100644 --- a/src/test/java/net/dv8tion/jda/test/interactions/CommandDataTest.java +++ b/src/test/java/net/dv8tion/jda/test/interactions/CommandDataTest.java @@ -27,12 +27,14 @@ import net.dv8tion.jda.api.utils.data.DataArray; import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.interactions.CommandDataImpl; +import net.dv8tion.jda.internal.utils.Checks; import net.dv8tion.jda.test.PrettyRepresentation; import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.List; +import static net.dv8tion.jda.test.ChecksHelper.assertStringChecks; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -175,50 +177,51 @@ void testRequiredThrows() @Test void testNameChecks() { - assertThatIllegalArgumentException() - .isThrownBy(() -> new CommandDataImpl("invalid name", "Valid description")) - .withMessage("Name must match regex ^[\\w-]+$. Provided: \"invalid name\""); - assertThatIllegalArgumentException() - .isThrownBy(() -> new CommandDataImpl("invalidName", "Valid description")) - .withMessage("Name must be lowercase only! Provided: \"invalidName\""); - assertThatIllegalArgumentException() - .isThrownBy(() -> new CommandDataImpl("valid_name", "")) - .withMessage("Description may not be empty"); - - assertThatIllegalArgumentException() - .isThrownBy(() -> new SubcommandData("invalid name", "Valid description")) - .withMessage("Name must match regex ^[\\w-]+$. Provided: \"invalid name\""); - assertThatIllegalArgumentException() - .isThrownBy(() -> new SubcommandData("invalidName", "Valid description")) - .withMessage("Name must be lowercase only! Provided: \"invalidName\""); - assertThatIllegalArgumentException() - .isThrownBy(() -> new SubcommandData("valid_name", "")) - .withMessage("Description may not be empty"); - - assertThatIllegalArgumentException() - .isThrownBy(() -> new SubcommandGroupData("invalid name", "Valid description")) - .withMessage("Name must match regex ^[\\w-]+$. Provided: \"invalid name\""); - assertThatIllegalArgumentException() - .isThrownBy(() -> new SubcommandGroupData("invalidName", "Valid description")) - .withMessage("Name must be lowercase only! Provided: \"invalidName\""); - assertThatIllegalArgumentException() - .isThrownBy(() -> new SubcommandGroupData("valid_name", "")) - .withMessage("Description may not be empty"); + assertStringChecks("Name", input -> new CommandDataImpl(input, "Valid description")) + .checksNotNull() + .checksRange(1, 32) + .checksLowercaseOnly() + .checksRegex("invalid name", Checks.ALPHANUMERIC_WITH_DASH); + + assertStringChecks("Name", input -> new SubcommandData(input, "Valid description")) + .checksNotNull() + .checksRange(1, 32) + .checksLowercaseOnly() + .checksRegex("invalid name", Checks.ALPHANUMERIC_WITH_DASH); + + assertStringChecks("Name", input -> new SubcommandGroupData(input, "Valid description")) + .checksNotNull() + .checksRange(1, 32) + .checksLowercaseOnly() + .checksRegex("invalid name", Checks.ALPHANUMERIC_WITH_DASH); + + assertStringChecks("Description", input -> new CommandDataImpl("valid_name", input)) + .checksNotNull() + .checksRange(1, 100); + + assertStringChecks("Description", input -> new SubcommandData("valid_name", input)) + .checksNotNull() + .checksRange(1, 100); + + assertStringChecks("Description", input -> new SubcommandGroupData("valid_name", input)) + .checksNotNull() + .checksRange(1, 100); } @Test void testChoices() { OptionData stringOption = new OptionData(OptionType.STRING, "choice", "Option with choices!"); + + assertStringChecks("Value", value -> stringOption.addChoice("valid_name", value)) + .checksNotEmpty(); + assertThatIllegalArgumentException() .isThrownBy(() -> stringOption.addChoice("invalid name", 0)) .withMessage("Cannot add long choice for OptionType.STRING"); assertThatIllegalArgumentException() .isThrownBy(() -> stringOption.addChoice("invalidName", 0.0)) .withMessage("Cannot add double choice for OptionType.STRING"); - assertThatIllegalArgumentException() - .isThrownBy(() -> stringOption.addChoice("valid_name", "")) - .withMessage("Value may not be empty"); OptionData intOption = new OptionData(OptionType.INTEGER, "choice", "Option with choices!"); List choices = new ArrayList<>(); diff --git a/src/test/java/net/dv8tion/jda/test/restaction/MessageCreateActionTest.java b/src/test/java/net/dv8tion/jda/test/restaction/MessageCreateActionTest.java index 57456b018d..8b9ee1c397 100644 --- a/src/test/java/net/dv8tion/jda/test/restaction/MessageCreateActionTest.java +++ b/src/test/java/net/dv8tion/jda/test/restaction/MessageCreateActionTest.java @@ -18,9 +18,11 @@ import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.entities.emoji.Emoji; import net.dv8tion.jda.api.requests.restaction.MessageCreateAction; import net.dv8tion.jda.api.utils.data.DataArray; import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.api.utils.messages.MessagePollBuilder; import net.dv8tion.jda.internal.requests.restaction.MessageCreateActionImpl; import net.dv8tion.jda.test.IntegrationTest; import org.junit.jupiter.api.BeforeEach; @@ -28,8 +30,11 @@ import org.mockito.Mock; import javax.annotation.Nonnull; +import java.util.concurrent.TimeUnit; import static net.dv8tion.jda.api.requests.Method.POST; +import static net.dv8tion.jda.test.restaction.MessageCreateActionTest.Data.emoji; +import static net.dv8tion.jda.test.restaction.MessageCreateActionTest.Data.pollAnswer; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.Mockito.when; @@ -54,6 +59,7 @@ private static DataObject defaultMessageRequest() .put("components", DataArray.empty()) .put("content", "") .put("embeds", DataArray.empty()) + .put("poll", null) .put("enforce_nonce", true) .put("flags", 0) .put("nonce", FIXED_NONCE) @@ -73,7 +79,7 @@ void testEmpty() assertThatIllegalStateException().isThrownBy(() -> new MessageCreateActionImpl(channel) .queue() - ).withMessage("Cannot build empty messages! Must provide at least one of: content, embed, file, or stickers"); + ).withMessage("Cannot build empty messages! Must provide at least one of: content, embed, file, poll, or stickers"); } @Test @@ -106,9 +112,63 @@ void testEmbedOnly() .whenQueueCalled(); } + @Test + void testPollOnly() + { + MessageCreateAction action = new MessageCreateActionImpl(channel) + .setPoll(new MessagePollBuilder("Test poll") + .setDuration(3, TimeUnit.DAYS) + .setMultiAnswer(true) + .addAnswer("Test answer 1") + .addAnswer("Test answer 2", Emoji.fromUnicode("🤔")) + .addAnswer("Test answer 3", Emoji.fromCustom("minn", 821355005788684298L, true)) + .build()); + + assertThatRequestFrom(action) + .hasMethod(POST) + .hasCompiledRoute(ENDPOINT_URL) + .hasBodyEqualTo(defaultMessageRequest() + .put("poll", DataObject.empty() + .put("duration", 72) + .put("allow_multiselect", true) + .put("layout_type", 1) + .put("question", DataObject.empty() + .put("text", "Test poll")) + .put("answers", DataArray.empty() + .add(pollAnswer(1, "Test answer 1", null)) + .add(pollAnswer(2, "Test answer 2", emoji("🤔"))) + .add(pollAnswer(3, "Test answer 3", emoji("minn", 821355005788684298L, true)))))) + .whenQueueCalled(); + } + @Nonnull protected DataObject normalizeRequestBody(@Nonnull DataObject body) { return body.put("nonce", FIXED_NONCE); } + + static class Data + { + static DataObject pollAnswer(long id, String title, DataObject emoji) + { + return DataObject.empty() + .put("answer_id", id) + .put("poll_media", DataObject.empty() + .put("text", title) + .put("emoji", emoji)); + } + + static DataObject emoji(String name) + { + return DataObject.empty().put("name", name); + } + + static DataObject emoji(String name, long id, boolean animated) + { + return DataObject.empty() + .put("name", name) + .put("id", id) + .put("animated", animated); + } + } }