├── .gitattributes ├── .github ├── pom.xml └── workflows │ ├── docs.yml │ ├── gradle.yml │ └── pom.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main └── java │ └── club │ └── minnced │ └── discord │ └── webhook │ ├── IOUtil.java │ ├── LibraryInfo.java │ ├── MessageFlags.java │ ├── WebhookClient.java │ ├── WebhookClientBuilder.java │ ├── WebhookCluster.java │ ├── exception │ └── HttpException.java │ ├── external │ ├── D4JWebhookClient.java │ ├── JDAWebhookClient.java │ └── JavacordWebhookClient.java │ ├── receive │ ├── EntityFactory.java │ ├── ReadonlyAttachment.java │ ├── ReadonlyEmbed.java │ ├── ReadonlyMessage.java │ └── ReadonlyUser.java │ ├── send │ ├── AllowedMentions.java │ ├── MessageAttachment.java │ ├── WebhookEmbed.java │ ├── WebhookEmbedBuilder.java │ ├── WebhookMessage.java │ └── WebhookMessageBuilder.java │ └── util │ ├── ThreadPools.java │ └── WebhookErrorHandler.java └── test ├── java └── root │ ├── IOTest.java │ ├── IOTestUtil.java │ ├── receive │ ├── ReceiveEmbedTest.java │ ├── ReceiveMessageTest.java │ └── ReceiveMock.java │ └── send │ ├── IOMock.java │ ├── MessageTest.java │ └── SendEmbedTest.java └── resources └── logback-test.xml /.gitattributes: -------------------------------------------------------------------------------- 1 | # Preserve gradlew's line ending 2 | gradlew binary 3 | 4 | .git* text eol=lf 5 | *.java text eol=lf 6 | *.gradle text eol=lf 7 | *.bat text eol=lf 8 | 9 | * text=auto 10 | -------------------------------------------------------------------------------- /.github/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 4.0.0 10 | club.minnced 11 | discord-webhooks 12 | 0.8.4 13 | discord-webhooks 14 | Provides easy to use bindings for the Discord Webhook API 15 | https://github.com/MinnDevelopment/discord-webhooks 16 | 17 | 18 | The Apache Software License, Version 2.0 19 | http://www.apache.org/licenses/LICENSE-2.0.txt 20 | repo 21 | 22 | 23 | 24 | 25 | Minn 26 | Florian Spieß 27 | business@minnced.club 28 | 29 | 30 | 31 | scm:git:git://github.com/MinnDevelopment/discord-webhooks 32 | scm:git:ssh:git@github.com:MinnDevelopment/discord-webhooks 33 | https://github.com/MinnDevelopment/discord-webhooks 34 | 35 | 36 | 37 | org.slf4j 38 | slf4j-api 39 | 1.7.32 40 | compile 41 | 42 | 43 | com.squareup.okhttp3 44 | okhttp 45 | 4.10.0 46 | compile 47 | 48 | 49 | org.json 50 | json 51 | 20230618 52 | compile 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Generate javadoc github pages 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | concurrency: 14 | group: "pages" 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | build: 19 | environment: 20 | name: github-pages 21 | url: ${{ steps.deployment.outputs.page_url }} 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Set up JDK 11 26 | uses: actions/setup-java@v2 27 | with: 28 | java-version: 11 29 | distribution: temurin 30 | cache: 'gradle' 31 | - name: Grant execute permission for gradlew 32 | run: chmod +x gradlew 33 | - name: Generate documentation directory 34 | uses: gradle/gradle-build-action@v2.4.2 35 | with: 36 | arguments: javadoc 37 | - name: Upload artifact 38 | uses: actions/upload-pages-artifact@v3 39 | with: 40 | path: './build/docs/javadoc' 41 | - name: Deploy to GitHub Pages 42 | id: deployment 43 | uses: actions/deploy-pages@main 44 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Java CI with Gradle 5 | 6 | on: 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout Repo 17 | uses: actions/checkout@v2 18 | - name: Set up JDK 11 19 | uses: actions/setup-java@v2 20 | with: 21 | java-version: 11 22 | distribution: temurin 23 | cache: 'gradle' 24 | - name: Grant execute permission for gradlew 25 | run: chmod +x gradlew 26 | - name: Build with Gradle 27 | uses: gradle/gradle-build-action@v2.4.2 28 | with: 29 | arguments: build test 30 | gradle-version: wrapper 31 | -------------------------------------------------------------------------------- /.github/workflows/pom.yml: -------------------------------------------------------------------------------- 1 | name: Generate .github/pom.xml 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up JDK 11 13 | uses: actions/setup-java@v2 14 | with: 15 | java-version: 11 16 | distribution: temurin 17 | - name: Grant execute permission for gradlew 18 | run: chmod +x gradlew 19 | - name: Generate pom.xml 20 | uses: gradle/gradle-build-action@v2.4.2 21 | with: 22 | arguments: generatePomFileForReleasePublication 23 | - name: Move pom file to github folder 24 | run: | 25 | mv build/publications/Release/pom-default.xml .github/pom.xml 26 | - name: Commit pom.xml 27 | id: commit 28 | continue-on-error: true 29 | run: | 30 | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" 31 | git config --local user.name "github-actions[bot]" 32 | git add .github/pom.xml 33 | git commit -m "Update pom.xml" 34 | - name: Push changes 35 | if: steps.commit.outcome == 'success' && steps.commit.conclusion == 'success' 36 | uses: ad-m/github-push-action@8407731efefc0d8f72af254c74276b7a90be36e1 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .gradle/ 3 | out/ 4 | build/ 5 | gradle.properties 6 | webhook_url.txt 7 | 8 | # Intellij project files 9 | *.iml 10 | *.ipr -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018-2020 Florian Spieß 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [version]: https://img.shields.io/maven-central/v/club.minnced/discord-webhooks 2 | [download]: https://mvnrepository.com/artifact/club.minnced/discord-webhooks/latest 3 | [license]: https://img.shields.io/badge/License-Apache%202.0-lightgrey.svg 4 | [license-file]: https://github.com/MinnDevelopment/discord-webhooks/blob/master/LICENSE 5 | 6 | [WebhookClient#setErrorHandler]: https://minndevelopment.github.io/discord-webhooks/club/minnced/discord/webhook/WebhookClient.html#setErrorHandler(club.minnced.discord.webhook.util.WebhookErrorHandler) 7 | [WebhookClient#setDefaultErrorHandler]: https://minndevelopment.github.io/discord-webhooks/club/minnced/discord/webhook/WebhookClient.html#setDefaultErrorHandler(club.minnced.discord.webhook.util.WebhookErrorHandler) 8 | 9 | [ ![version] ][download] 10 | [ ![license] ][license-file] 11 | 12 | # Discord-Webhooks 13 | 14 | Originally part of JDA, this library provides easy to use bindings for the 15 | Discord Webhook API. 16 | 17 | # Introduction 18 | 19 | Here we will give a small overview of the proper usage and applicability of the resources provided by this library. 20 | 21 | Documentation is available via the GitHub pages on this repository: [Javadoc](https://minndevelopment.github.io/discord-webhooks/overview-tree.html) 22 | 23 | ## Limitations 24 | 25 | Webhooks on discord are only capable of sending messages, nothing more. For anything else you either have to use OAuth2 or a bot account. This library does not provide any functionality for creating or modifying webhooks. 26 | 27 | ## Getting Started 28 | 29 | The first thing to do is to create either a `WebhookClient` or a `WebhookCluster`. The `WebhookClient` provides functionality to send messages to one webhook based on either a webhook URL or the ID and token of a webhook. It implements automatic rate-limit handling and can be configured to use a shared thread-pool. 30 | 31 | ### Creating a WebhookClient 32 | 33 | ```java 34 | // Using the builder 35 | WebhookClientBuilder builder = new WebhookClientBuilder(url); // or id, token 36 | builder.setThreadFactory((job) -> { 37 | Thread thread = new Thread(job); 38 | thread.setName("Hello"); 39 | thread.setDaemon(true); 40 | return thread; 41 | }); 42 | builder.setWait(true); 43 | WebhookClient client = builder.build(); 44 | ``` 45 | 46 | ```java 47 | // Using the factory methods 48 | WebhookClient client = WebhookClient.withUrl(url); // or withId(id, token) 49 | ``` 50 | 51 | ### Creating a WebhookCluster 52 | 53 | ```java 54 | // Create and initialize the cluster 55 | WebhookCluster cluster = new WebhookCluster(5); // create an initial 5 slots (dynamic like lists) 56 | cluster.setDefaultHttpClient(new OkHttpClient()); 57 | cluster.setDefaultDaemon(true); 58 | 59 | // Create a webhook client 60 | cluster.buildWebhook(id, token); 61 | 62 | // Add an existing webhook client 63 | cluster.addWebhook(client); 64 | ``` 65 | 66 | ## Sending Messages 67 | 68 | Sending messages happens in a background thread (configured through the pool/factory) and thus is async by default. To access the message you have to enable the `wait` mechanic (enabled by default). With this you can use the callbacks provided by `CompletableFuture`. 69 | 70 | ```java 71 | // Send and forget 72 | client.send("Hello World"); 73 | 74 | // Send and log (using embed) 75 | WebhookEmbed embed = new WebhookEmbedBuilder() 76 | .setColor(0xFF00EE) 77 | .setDescription("Hello World") 78 | .build(); 79 | 80 | client.send(embed) 81 | .thenAccept((message) -> System.out.printf("Message with embed has been sent [%s]%n", message.getId())); 82 | 83 | // Change appearance of webhook message 84 | WebhookMessage message = new WebhookMessageBuilder() 85 | .setUsername("Minn") // use this username 86 | .setAvatarUrl(avatarUrl) // use this avatar 87 | .setContent("Hello World") 88 | .build(); 89 | client.send(message); 90 | ``` 91 | 92 | ## Threads 93 | 94 | You can use the webhook clients provided by this library to send messages in threads. There are two ways to accomplish this. 95 | 96 | Set a thread id in the client builder to send all messages in that client to the thread: 97 | 98 | ```java 99 | WebhookClient client = new WebhookClientBuilder(url) 100 | .setThreadId(threadId) 101 | .build(); 102 | 103 | client.send("Hello"); // appears in the thread 104 | ``` 105 | 106 | Use `onThread` to create a client with a thread id and all other settings inherited: 107 | 108 | ```java 109 | try (WebhookClient client = WebhookClient.withUrl(url)) { 110 | WebhookClient thread = client.onThread(123L); 111 | thread.send("Hello"); // appears only in the thread with id 123 112 | client.send("Friend"); // appears in the channel instead 113 | } // calls client.close() which automatically also closes all onThread clients as well. 114 | ``` 115 | 116 | All `WebhookClient` instances created with `onThread` will share the same thread pool used by the original client. This means that shutting down or closing any of the clients will also close all other clients associated with that underlying thread pool. 117 | 118 | ```java 119 | WebhookClient thread = null; 120 | try (WebhookClient client = WebhookClient.withUrl(url)) { 121 | thread = client.onThread(id); 122 | } // closes client 123 | thread.send("Hello"); // <- throws rejected execution due to pool being shutdown by client.close() above ^ 124 | 125 | WebhookClient client = WebhookClient.withUrl(url); 126 | try (WebhookClient thread = client.onThread(id)) { 127 | thread.send("..."); 128 | } // closes thread 129 | client.send("Hello"); // <- throws rejected execution due to pool being shutdown by thread.close() above ^ 130 | ``` 131 | 132 | ### Shutdown 133 | 134 | Since the clients use threads for sending messages you should close the client to end the threads. This can be ignored if a shared thread-pool is used between multiple clients but that pool has to be shutdown by the user accordingly. 135 | 136 | ```java 137 | try (WebhookClient client = WebhookClient.withUrl(url)) { 138 | client.send("Hello World"); 139 | } // client.close() automated 140 | 141 | webhookCluster.close(); // closes each client and can be used again 142 | ``` 143 | 144 | ## Error Handling 145 | 146 | By default, this library will log every exception encountered when sending a message using the SLF4J logger implementation. 147 | This can be configured using [WebhookClient#setErrorHandler] to custom behavior per client or [WebhookClient#setDefaultErrorHandler] for all clients. 148 | 149 | ### Example 150 | 151 | ```java 152 | WebhookClient.setDefaultErrorHandler((client, message, throwable) -> { 153 | System.err.printf("[%s] %s%n", client.getId(), message); 154 | if (throwable != null) 155 | throwable.printStackTrace(); 156 | // Shutdown the webhook client when you get 404 response (may also trigger for client#edit calls, be careful) 157 | if (throwable instanceof HttpException ex && ex.getCode() == 404) { 158 | client.close(); 159 | } 160 | }); 161 | ``` 162 | 163 | ## External Libraries 164 | 165 | This library also supports sending webhook messages with integration from other libraries such as 166 | 167 | - [JDA](/DV8FromTheWorld/JDA) (version 5.0.0-beta.12) with [JDAWebhookClient](https://github.com/MinnDevelopment/discord-webhooks/blob/master/src/main/java/club/minnced/discord/webhook/external/JDAWebhookClient.java) 168 | - [Discord4J](/Discord4J/Discord4J) (version 3.2.5) with [D4JWebhookClient](https://github.com/MinnDevelopment/discord-webhooks/blob/master/src/main/java/club/minnced/discord/webhook/external/D4JWebhookClient.java) 169 | - [Javacord](/Javacord/Javacord) (version 3.8.0) with [JavacordWebhookClient](https://github.com/MinnDevelopment/discord-webhooks/blob/master/src/main/java/club/minnced/discord/webhook/external/JavacordWebhookClient.java) 170 | 171 | ### Example JDA 172 | 173 | ```java 174 | public void sendWebhook(Webhook webhook) { 175 | MessageCreateData message = new MessageCreateBuilder().setContent("Hello World").build(); 176 | try (JDAWebhookClient client = JDAWebhookClient.from(webhook)) { // create a client instance from the JDA webhook 177 | client.send(message); // send a JDA message instance 178 | } 179 | } 180 | ``` 181 | 182 | ### Example Discord4J 183 | 184 | ```java 185 | public void sendWebhook(Webhook webhook) { 186 | try (D4JWebhookClient client = D4JWebhookClient.from(webhook)) { 187 | client.send(MessageCreateSpec.create() 188 | .withContent("Hello World") 189 | .addFile("cat.png", new FileInputStream("cat.png")) 190 | ); 191 | } 192 | } 193 | ``` 194 | 195 | # Download 196 | 197 | [ ![version] ][download] 198 | 199 | Note: Replace `%VERSION%` below with the desired version. 200 | 201 | ## Gradle 202 | 203 | ```gradle 204 | repositories { 205 | mavenCentral() 206 | } 207 | ``` 208 | 209 | ```gradle 210 | dependencies { 211 | implementation("club.minnced:discord-webhooks:%VERSION%") 212 | } 213 | ``` 214 | 215 | ## Maven 216 | 217 | ```xml 218 | 219 | club.minnced 220 | discord-webhooks 221 | %VERSION% 222 | 223 | ``` 224 | 225 | ## Compile Yourself 226 | 227 | 1. Clone repository 228 | 1. Run `gradlew shadowJar` 229 | 1. Use jar suffixed with `-all.jar` in `build/libs` 230 | 231 | 232 | # Example 233 | 234 | ```java 235 | class MyAppender extends AppenderBase { 236 | private final WebhookClient client; 237 | 238 | @Override 239 | protected void append(LoggingEvent eventObject) { 240 | if (client == null) 241 | return; 242 | WebhookEmbedBuilder builder = new WebhookEmbedBuilder(); 243 | builder.setDescription(eventObject.getFormattedMessage()); 244 | int color = -1; 245 | switch (eventObject.getLevel().toInt()) { 246 | case ERROR_INT: 247 | color = 0xFF0000; 248 | break; 249 | case INFO_INT: 250 | color = 0xF8F8FF; 251 | break; 252 | } 253 | if (color > 0) 254 | builder.setColor(color); 255 | builder.setTimestamp(Instant.ofEpochMilli(eventObject.getTimeStamp())); 256 | client.send(builder.build()); 257 | } 258 | } 259 | ``` 260 | 261 | > This is an example implementation of an Appender for logback-classic 262 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.apache.tools.ant.filters.ReplaceTokens 2 | import java.io.ByteArrayOutputStream 3 | import java.time.Duration 4 | 5 | plugins { 6 | `java-library` 7 | `maven-publish` 8 | signing 9 | 10 | id("io.github.gradle-nexus.publish-plugin") version "1.1.0" 11 | id("com.github.johnrengelman.shadow") version "7.1.2" 12 | } 13 | 14 | val major = "0" 15 | val minor = "8" 16 | val patch = "4" 17 | 18 | group = "club.minnced" 19 | version = "$major.$minor.$patch" 20 | 21 | fun getCommit() 22 | = System.getenv("GITHUB_SHA") 23 | ?: System.getenv("GIT_COMMIT") 24 | ?: try { 25 | val out = ByteArrayOutputStream() 26 | exec { 27 | commandLine("git rev-parse --verify --short HEAD".split(" ")) 28 | standardOutput = out 29 | workingDir = projectDir 30 | } 31 | out.toString("UTF-8").trim() 32 | } catch (ignored: Throwable) { "N/A" } 33 | 34 | val tokens = mapOf( 35 | "MAJOR" to major, 36 | "MINOR" to minor, 37 | "PATCH" to patch, 38 | "VERSION" to version, 39 | "COMMIT" to getCommit() 40 | ) 41 | 42 | repositories { 43 | mavenCentral() 44 | } 45 | 46 | val versions = mapOf( 47 | "slf4j" to "1.7.32", 48 | "okhttp" to "4.10.0", 49 | "json" to "20230618", 50 | "jda" to "5.0.0-beta.12", 51 | "discord4j" to "3.2.5", 52 | "javacord" to "3.8.0", 53 | "junit" to "4.13.2", 54 | "mockito" to "3.12.4", // must be compatible with powermock 55 | "powermock" to "2.0.9", 56 | "logback" to "1.2.3", 57 | "annotations" to "24.0.1", 58 | "jsr" to "3.0.2" 59 | ) 60 | 61 | dependencies { 62 | api("org.slf4j:slf4j-api:${versions["slf4j"]}") 63 | api("com.squareup.okhttp3:okhttp:${versions["okhttp"]}") 64 | api("org.json:json:${versions["json"]}") 65 | 66 | compileOnly("com.google.code.findbugs:jsr305:${versions["jsr"]}") 67 | compileOnly("org.jetbrains:annotations:${versions["annotations"]}") 68 | 69 | compileOnly("net.dv8tion:JDA:${versions["jda"]}") 70 | compileOnly("com.discord4j:discord4j-core:${versions["discord4j"]}") 71 | compileOnly("org.javacord:javacord:${versions["javacord"]}") 72 | 73 | testImplementation("junit:junit:${versions["junit"]}") 74 | testImplementation("org.mockito:mockito-core:${versions["mockito"]}") 75 | testImplementation("org.powermock:powermock-module-junit4:${versions["powermock"]}") 76 | testImplementation("org.powermock:powermock-api-mockito2:${versions["powermock"]}") 77 | testImplementation("net.dv8tion:JDA:${versions["jda"]}") 78 | testImplementation("com.discord4j:discord4j-core:${versions["discord4j"]}") 79 | testImplementation("org.javacord:javacord:${versions["javacord"]}") 80 | //testCompile("ch.qos.logback:logback-classic:${versions["logback"]}") 81 | } 82 | 83 | fun getProjectProperty(name: String) = project.properties[name] as? String 84 | 85 | val javadoc: Javadoc by tasks 86 | val jar: Jar by tasks 87 | 88 | val sources = tasks.create("sources", Copy::class.java) { 89 | from("src/main/java") 90 | into("$buildDir/sources") 91 | filter("tokens" to tokens) 92 | } 93 | 94 | javadoc.apply { 95 | dependsOn(sources) 96 | isFailOnError = false 97 | source = fileTree(sources.destinationDir) 98 | options.encoding = "UTF-8" 99 | options.memberLevel = JavadocMemberLevel.PUBLIC 100 | 101 | if (options is StandardJavadocDocletOptions) { 102 | val opt = options as StandardJavadocDocletOptions 103 | 104 | val extLinks = arrayOf( 105 | "https://docs.oracle.com/en/java/javase/11/docs/api/", 106 | "https://square.github.io/okhttp/3.x/okhttp/", 107 | ) 108 | opt.links(*extLinks) 109 | if (JavaVersion.current().isJava9Compatible) 110 | opt.addBooleanOption("html5", true) 111 | if (JavaVersion.current().isJava11Compatible) 112 | opt.addBooleanOption("-no-module-directories", true) 113 | } 114 | } 115 | 116 | 117 | val javadocJar = tasks.create("javadocJar", Jar::class.java) { 118 | dependsOn(javadoc) 119 | from(javadoc.destinationDir) 120 | archiveClassifier.set("javadoc") 121 | } 122 | 123 | val sourcesJar = tasks.create("sourcesJar", Jar::class.java) { 124 | dependsOn(sources) 125 | from(sources.destinationDir) 126 | archiveClassifier.set("sources") 127 | } 128 | 129 | tasks.withType { 130 | val arguments = mutableListOf("-Xlint:deprecation,unchecked,divzero,cast,static,varargs,try") 131 | options.isIncremental = true 132 | options.encoding = "UTF-8" 133 | if (JavaVersion.current().isJava9Compatible) doFirst { 134 | arguments += "--release" 135 | arguments += "8" 136 | } 137 | doFirst { 138 | options.compilerArgs = arguments 139 | } 140 | } 141 | 142 | val compileJava: JavaCompile by tasks 143 | compileJava.apply { 144 | source = fileTree(sources.destinationDir) 145 | dependsOn(sources) 146 | } 147 | 148 | configure { 149 | sourceCompatibility = JavaVersion.VERSION_1_8 150 | targetCompatibility = JavaVersion.VERSION_1_8 151 | } 152 | 153 | val test: Test by tasks 154 | val build: Task by tasks 155 | build.apply { 156 | dependsOn(javadocJar) 157 | dependsOn(sourcesJar) 158 | dependsOn(jar) 159 | dependsOn(test) 160 | } 161 | 162 | test.apply { 163 | if (JavaVersion.current().isJava11Compatible) doFirst { 164 | jvmArgs = listOf("--illegal-access=permit") 165 | } 166 | } 167 | 168 | // Generate pom file for maven central 169 | 170 | fun generatePom(): MavenPom.() -> Unit { 171 | return { 172 | packaging = "jar" 173 | name.set(project.name) 174 | description.set("Provides easy to use bindings for the Discord Webhook API") 175 | url.set("https://github.com/MinnDevelopment/discord-webhooks") 176 | scm { 177 | url.set("https://github.com/MinnDevelopment/discord-webhooks") 178 | connection.set("scm:git:git://github.com/MinnDevelopment/discord-webhooks") 179 | developerConnection.set("scm:git:ssh:git@github.com:MinnDevelopment/discord-webhooks") 180 | } 181 | licenses { 182 | license { 183 | name.set("The Apache Software License, Version 2.0") 184 | url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 185 | distribution.set("repo") 186 | } 187 | } 188 | developers { 189 | developer { 190 | id.set("Minn") 191 | name.set("Florian Spieß") 192 | email.set("business@minnced.club") 193 | } 194 | } 195 | } 196 | } 197 | 198 | 199 | // Publish 200 | 201 | publishing { 202 | publications { 203 | register("Release", MavenPublication::class) { 204 | from(components["java"]) 205 | 206 | artifactId = project.name 207 | groupId = project.group as String 208 | version = project.version as String 209 | 210 | artifact(sourcesJar) 211 | artifact(javadocJar) 212 | 213 | pom.apply(generatePom()) 214 | } 215 | } 216 | } 217 | 218 | 219 | // Staging and Promotion 220 | 221 | if ("signing.keyId" in properties) { 222 | signing { 223 | sign(publishing.publications["Release"]) 224 | } 225 | } 226 | 227 | nexusPublishing { 228 | repositories.sonatype { 229 | username.set(getProjectProperty("ossrhUser")) 230 | password.set(getProjectProperty("ossrhPassword")) 231 | stagingProfileId.set(getProjectProperty("stagingProfileId")) 232 | } 233 | 234 | // Sonatype is very slow :) 235 | connectTimeout.set(Duration.ofMinutes(1)) 236 | clientTimeout.set(Duration.ofMinutes(10)) 237 | 238 | transitionCheckOptions { 239 | maxRetries.set(100) 240 | delayBetween.set(Duration.ofSeconds(5)) 241 | } 242 | } 243 | 244 | 245 | // To publish run ./gradlew release 246 | 247 | val rebuild = tasks.create("rebuild") { 248 | val clean = tasks.getByName("clean") 249 | dependsOn(build) 250 | dependsOn(clean) 251 | build.mustRunAfter(clean) 252 | } 253 | 254 | // Only enable publishing task for properly configured projects 255 | val publishingTasks = tasks.withType { 256 | enabled = "ossrhUser" in properties 257 | mustRunAfter(rebuild) 258 | dependsOn(rebuild) 259 | } 260 | 261 | tasks.create("release") { 262 | dependsOn(publishingTasks) 263 | afterEvaluate { 264 | // Collect all the publishing task which upload the archives to nexus staging 265 | val closeAndReleaseSonatypeStagingRepository: Task by tasks 266 | 267 | // Make sure the close and release happens after uploading 268 | dependsOn(closeAndReleaseSonatypeStagingRepository) 269 | closeAndReleaseSonatypeStagingRepository.mustRunAfter(publishingTasks) 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinnDevelopment/discord-webhooks/544b96a7531540ac8d27afdbf702f766215ec28a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'discord-webhooks' 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/club/minnced/discord/webhook/IOUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 Florian Spieß 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package club.minnced.discord.webhook; 18 | 19 | import okhttp3.MediaType; 20 | import okhttp3.RequestBody; 21 | import okhttp3.ResponseBody; 22 | import okio.BufferedSink; 23 | import org.jetbrains.annotations.NotNull; 24 | import org.jetbrains.annotations.Nullable; 25 | import org.json.JSONObject; 26 | import org.json.JSONTokener; 27 | 28 | import java.io.IOException; 29 | import java.io.InputStream; 30 | import java.util.ArrayList; 31 | import java.util.List; 32 | import java.util.concurrent.CompletableFuture; 33 | import java.util.zip.GZIPInputStream; 34 | 35 | /** 36 | * Utility for various I/O operations used within library internals 37 | */ 38 | public class IOUtil { //TODO: test json 39 | /** 40 | * application/json 41 | */ 42 | public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); 43 | /** application/octet-stream */ 44 | public static final MediaType OCTET = MediaType.parse("application/octet-stream; charset=utf-8"); 45 | /** Empty byte-array, used for {@link #readAllBytes(java.io.InputStream)} */ 46 | public static final byte[] EMPTY_BYTES = new byte[0]; 47 | 48 | private static final CompletableFuture[] EMPTY_FUTURES = new CompletableFuture[0]; 49 | 50 | /** 51 | * Reads all bytes from an {@link java.io.InputStream} 52 | * 53 | * @param stream 54 | * The InputStream 55 | * 56 | * @throws IOException 57 | * If some I/O error occurs 58 | * 59 | * @return {@code byte[]} containing all bytes of the stream 60 | */ 61 | @NotNull 62 | public static byte[] readAllBytes(@NotNull InputStream stream) throws IOException { 63 | int count = 0, pos = 0; 64 | byte[] output = EMPTY_BYTES; 65 | byte[] buf = new byte[1024]; 66 | while ((count = stream.read(buf)) > 0) { 67 | if (pos + count >= output.length) { 68 | byte[] tmp = output; 69 | output = new byte[pos + count]; 70 | System.arraycopy(tmp, 0, output, 0, tmp.length); 71 | } 72 | 73 | for (int i = 0; i < count; i++) { 74 | output[pos++] = buf[i]; 75 | } 76 | } 77 | return output; 78 | } 79 | 80 | /** 81 | * Helper method which handles gzip encoded response bodies 82 | * 83 | * @param req 84 | * {@link okhttp3.Response} instance 85 | * 86 | * @throws IOException 87 | * If some I/O error occurs 88 | * 89 | * @return {@link java.io.InputStream} representing the response body 90 | */ 91 | @Nullable 92 | public static InputStream getBody(@NotNull okhttp3.Response req) throws IOException { 93 | List encoding = req.headers("content-encoding"); 94 | ResponseBody body = req.body(); 95 | if (!encoding.isEmpty() && body != null) { 96 | return new GZIPInputStream(body.byteStream()); 97 | } 98 | return body != null ? body.byteStream() : null; 99 | } 100 | 101 | /** 102 | * Converts an {@link java.io.InputStream} to a {@link org.json.JSONObject} 103 | * 104 | * @param input 105 | * The {@link java.io.InputStream} 106 | * 107 | * @throws org.json.JSONException 108 | * If parsing fails 109 | * 110 | * @return {@link org.json.JSONObject} for the provided input 111 | */ 112 | @NotNull 113 | public static JSONObject toJSON(@NotNull InputStream input) { 114 | return new JSONObject(new JSONTokener(input)); 115 | } 116 | 117 | /** 118 | * Converts a list of futures in a future of a list. 119 | * 120 | * @param list 121 | * The list of futures to flatten 122 | * @param 123 | * Component type of the list 124 | * 125 | * @return A future that will be completed with the resulting list 126 | */ 127 | @NotNull 128 | public static CompletableFuture> flipFuture(@NotNull List> list) { 129 | List result = new ArrayList<>(list.size()); 130 | List> updatedStages = new ArrayList<>(list.size()); 131 | 132 | list.stream() 133 | .map(it -> it.thenAccept(result::add)) 134 | .forEach(updatedStages::add); 135 | 136 | CompletableFuture tracker = CompletableFuture.allOf(updatedStages.toArray(EMPTY_FUTURES)); 137 | CompletableFuture> future = new CompletableFuture<>(); 138 | 139 | tracker.thenRun(() -> future.complete(result)).exceptionally((e) -> { 140 | future.completeExceptionally(e); 141 | return null; 142 | }); 143 | 144 | return future; 145 | } 146 | 147 | /** 148 | * Supplier that can throw checked-exceptions 149 | * 150 | * @param 151 | * The component type 152 | */ 153 | public interface SilentSupplier { 154 | @Nullable 155 | T get() throws Exception; 156 | } 157 | 158 | /** 159 | * Lazy evaluation for logging complex objects 160 | * 161 | *

Example

162 | * {@code LOG.debug("Suspicious json found", new Lazy(() -> json.toString()));} 163 | */ 164 | public static class Lazy { 165 | private final SilentSupplier supply; 166 | 167 | public Lazy(SilentSupplier supply) { 168 | this.supply = supply; 169 | } 170 | 171 | @NotNull 172 | @Override 173 | public String toString() { 174 | try { 175 | return String.valueOf(supply.get()); 176 | } 177 | catch (Exception e) { 178 | throw new IllegalStateException(e); 179 | } 180 | } 181 | } 182 | 183 | /** 184 | * Wrapper for an {@link #OCTET} request body 185 | */ 186 | public static class OctetBody extends RequestBody { 187 | private final byte[] data; 188 | 189 | public OctetBody(@NotNull byte[] data) { 190 | this.data = data; 191 | } 192 | 193 | @Override 194 | public MediaType contentType() { 195 | return OCTET; 196 | } 197 | 198 | @Override 199 | public void writeTo(BufferedSink sink) throws IOException { 200 | sink.write(data); 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/main/java/club/minnced/discord/webhook/LibraryInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 Florian Spieß 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package club.minnced.discord.webhook; 18 | 19 | public class LibraryInfo { 20 | public static final int DISCORD_API_VERSION = 9; 21 | public static final String VERSION_MAJOR = "@MAJOR@"; 22 | public static final String VERSION_MINOR = "@MINOR@"; 23 | public static final String VERSION_PATCH = "@PATCH@"; 24 | public static final String VERSION = "@VERSION@"; 25 | public static final String COMMIT = "@COMMIT@"; 26 | 27 | public static final String DEBUG_INFO = "DISCORD_API_VERSION: " + DISCORD_API_VERSION + 28 | "\nVERSION: " + VERSION + 29 | "\nCOMMIT: " + COMMIT; 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/club/minnced/discord/webhook/MessageFlags.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 Florian Spieß 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package club.minnced.discord.webhook; 18 | 19 | /** 20 | * Constants for the message flags described by the Discord Documentation. 21 | */ 22 | @SuppressWarnings("PointlessBitwiseExpression") 23 | public class MessageFlags { 24 | public static final int CROSSPOSTED = 1 << 0; 25 | public static final int IS_CROSSPOSTED = 1 << 1; 26 | public static final int SUPPRESS_EMBEDS = 1 << 2; 27 | public static final int SOURCE_MESSAGE_DELETED = 1 << 3; 28 | public static final int URGENT = 1 << 4; 29 | public static final int HAS_THREAD = 1 << 5; 30 | public static final int EPHEMERAL = 1 << 6; 31 | public static final int LOADING = 1 << 7; 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/club/minnced/discord/webhook/WebhookClientBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 Florian Spieß 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package club.minnced.discord.webhook; 18 | 19 | import club.minnced.discord.webhook.external.D4JWebhookClient; 20 | import club.minnced.discord.webhook.external.JDAWebhookClient; 21 | import club.minnced.discord.webhook.external.JavacordWebhookClient; 22 | import club.minnced.discord.webhook.send.AllowedMentions; 23 | import club.minnced.discord.webhook.util.ThreadPools; 24 | import okhttp3.OkHttpClient; 25 | import org.javacord.api.entity.webhook.IncomingWebhook; 26 | import org.jetbrains.annotations.NotNull; 27 | import org.jetbrains.annotations.Nullable; 28 | 29 | import java.util.Objects; 30 | import java.util.concurrent.ScheduledExecutorService; 31 | import java.util.concurrent.ThreadFactory; 32 | import java.util.regex.Matcher; 33 | import java.util.regex.Pattern; 34 | 35 | /** 36 | * Builder for a {@link club.minnced.discord.webhook.WebhookClient} instance. 37 | * 38 | * @see club.minnced.discord.webhook.WebhookClient#withId(long, String) 39 | * @see club.minnced.discord.webhook.WebhookClient#withUrl(String) 40 | */ 41 | public class WebhookClientBuilder { //TODO: tests 42 | /** 43 | * Pattern used to validate webhook urls 44 | * {@code (?:https?://)?(?:\w+\.)?discord(?:app)?\.com/api(?:/v\d+)?/webhooks/(\d+)/([\w-]+)(?:/(?:\w+)?)?} 45 | */ 46 | public static final Pattern WEBHOOK_PATTERN = Pattern.compile("(?:https?://)?(?:\\w+\\.)?discord(?:app)?\\.com/api(?:/v\\d+)?/webhooks/(\\d+)/([\\w-]+)(?:/(?:\\w+)?)?"); 47 | 48 | protected final long id; 49 | protected final String token; 50 | protected long threadId; 51 | protected ScheduledExecutorService pool; 52 | protected OkHttpClient client; 53 | protected ThreadFactory threadFactory; 54 | protected AllowedMentions allowedMentions = AllowedMentions.all(); 55 | protected boolean isDaemon; 56 | protected boolean parseMessage = true; 57 | 58 | /** 59 | * Creates a new WebhookClientBuilder for the specified webhook components 60 | * 61 | * @param id 62 | * The webhook id 63 | * @param token 64 | * The webhook token 65 | * 66 | * @throws java.lang.NullPointerException 67 | * If the token is null 68 | */ 69 | public WebhookClientBuilder(final long id, @NotNull final String token) { 70 | Objects.requireNonNull(token, "Token"); 71 | this.id = id; 72 | this.token = token; 73 | } 74 | 75 | /** 76 | * Creates a new WebhookClientBuilder for the specified webhook url 77 | *
The url is verified using {@link #WEBHOOK_PATTERN}. 78 | * 79 | * @param url 80 | * The url to use 81 | * 82 | * @throws java.lang.NullPointerException 83 | * If the url is null 84 | * @throws java.lang.IllegalArgumentException 85 | * If the url is not valid 86 | */ 87 | public WebhookClientBuilder(@NotNull String url) { 88 | Objects.requireNonNull(url, "Url"); 89 | Matcher matcher = WEBHOOK_PATTERN.matcher(url); 90 | if (!matcher.matches()) { 91 | throw new IllegalArgumentException("Failed to parse webhook URL"); 92 | } 93 | 94 | this.id = Long.parseUnsignedLong(matcher.group(1)); 95 | this.token = matcher.group(2); 96 | } 97 | 98 | ///////////////////////////////// 99 | /// Third-party compatibility /// 100 | ///////////////////////////////// 101 | 102 | /** 103 | * Creates a WebhookClientBuilder for the provided webhook. 104 | * 105 | * @param webhook 106 | * The webhook 107 | * 108 | * @throws NullPointerException 109 | * If the webhook is null or does not provide a token 110 | * 111 | * @return The WebhookClientBuilder 112 | */ 113 | @NotNull 114 | public static WebhookClientBuilder fromJDA(@NotNull net.dv8tion.jda.api.entities.Webhook webhook) { 115 | Objects.requireNonNull(webhook, "Webhook"); 116 | return new WebhookClientBuilder(webhook.getIdLong(), Objects.requireNonNull(webhook.getToken(), "Webhook Token")); 117 | } 118 | 119 | /** 120 | * Creates a WebhookClientBuilder for the provided webhook. 121 | * 122 | * @param webhook 123 | * The webhook 124 | * 125 | * @throws NullPointerException 126 | * If the webhook is null or does not provide a token 127 | * 128 | * @return The WebhookClientBuilder 129 | */ 130 | @NotNull 131 | public static WebhookClientBuilder fromD4J(@NotNull discord4j.core.object.entity.Webhook webhook) { 132 | Objects.requireNonNull(webhook, "Webhook"); 133 | String token = webhook.getToken().orElseThrow(() -> new NullPointerException("Webhook Token is missing")); 134 | if (token.isEmpty()) 135 | throw new NullPointerException("Webhook Token is empty"); 136 | return new WebhookClientBuilder(webhook.getId().asLong(), token); 137 | } 138 | 139 | /** 140 | * Creates a WebhookClientBuilder for the provided webhook. 141 | * 142 | * @param webhook 143 | * The webhook 144 | * 145 | * @throws NullPointerException 146 | * If the webhook is null or does not provide a token 147 | * 148 | * @return The WebhookClientBuilder 149 | */ 150 | @NotNull 151 | public static WebhookClientBuilder fromJavacord(@NotNull org.javacord.api.entity.webhook.Webhook webhook) { 152 | Objects.requireNonNull(webhook, "Webhook"); 153 | return new WebhookClientBuilder(webhook.getId(), 154 | webhook.asIncomingWebhook() 155 | .map(IncomingWebhook::getToken) 156 | .orElseThrow(() -> new NullPointerException("Webhook Token is missing")) 157 | ); 158 | } 159 | 160 | 161 | /** 162 | * The {@link java.util.concurrent.ScheduledExecutorService} that is used to execute 163 | * send requests in the resulting {@link club.minnced.discord.webhook.WebhookClient}. 164 | *
This will be closed by a call to {@link WebhookClient#close()}. 165 | * 166 | * @param executorService 167 | * The executor service to use 168 | * 169 | * @return The current builder, for chaining convenience 170 | */ 171 | @NotNull 172 | public WebhookClientBuilder setExecutorService(@Nullable ScheduledExecutorService executorService) { 173 | this.pool = executorService; 174 | return this; 175 | } 176 | 177 | /** 178 | * The {@link okhttp3.OkHttpClient} that is used to execute 179 | * send requests in the resulting {@link club.minnced.discord.webhook.WebhookClient}. 180 | *
It is usually not necessary to use multiple different clients in one application 181 | * 182 | * @param client 183 | * The http client to use 184 | * 185 | * @return The current builder, for chaining convenience 186 | */ 187 | @NotNull 188 | public WebhookClientBuilder setHttpClient(@Nullable OkHttpClient client) { 189 | this.client = client; 190 | return this; 191 | } 192 | 193 | /** 194 | * The {@link java.util.concurrent.ThreadFactory} that is used to initialize 195 | * the default {@link java.util.concurrent.ScheduledExecutorService} used if 196 | * {@link #setExecutorService(java.util.concurrent.ScheduledExecutorService)} is not configured. 197 | * 198 | * @param factory 199 | * The factory to use 200 | * 201 | * @return The current builder, for chaining convenience 202 | */ 203 | @NotNull 204 | public WebhookClientBuilder setThreadFactory(@Nullable ThreadFactory factory) { 205 | this.threadFactory = factory; 206 | return this; 207 | } 208 | 209 | /** 210 | * The default mention whitelist for every outgoing message. 211 | *
See {@link AllowedMentions} for more details. 212 | * 213 | * @param mentions 214 | * The mention whitelist 215 | * 216 | * @return This builder for chaining convenience 217 | */ 218 | @NotNull 219 | public WebhookClientBuilder setAllowedMentions(@Nullable AllowedMentions mentions) { 220 | this.allowedMentions = mentions == null ? AllowedMentions.all() : mentions; 221 | return this; 222 | } 223 | 224 | /** 225 | * Whether the default executor should use daemon threads. 226 | *
This has no effect if either {@link #setExecutorService(java.util.concurrent.ScheduledExecutorService)} 227 | * or {@link #setThreadFactory(java.util.concurrent.ThreadFactory)} are configured to non-null values. 228 | * 229 | * @param isDaemon 230 | * Whether to use daemon threads or not 231 | * 232 | * @return The current builder, for chaining convenience 233 | */ 234 | @NotNull 235 | public WebhookClientBuilder setDaemon(boolean isDaemon) { 236 | this.isDaemon = isDaemon; 237 | return this; 238 | } 239 | 240 | /** 241 | * Whether resulting messages should be parsed after sending, 242 | * if this is set to {@code false} the futures returned by {@link club.minnced.discord.webhook.WebhookClient} 243 | * will receive {@code null} instead of instances of {@link club.minnced.discord.webhook.receive.ReadonlyMessage}. 244 | * 245 | * @param waitForMessage 246 | * True, if the client should parse resulting messages (default behavior) 247 | * 248 | * @return The current builder, for chaining convenience 249 | */ 250 | @NotNull 251 | public WebhookClientBuilder setWait(boolean waitForMessage) { 252 | this.parseMessage = waitForMessage; 253 | return this; 254 | } 255 | 256 | /** 257 | * The ID for the thread you want the messages to be posted to. 258 | *
You can use {@link WebhookClient#onThread(long)} to send specific messages to threads. 259 | * 260 | * @param threadId 261 | * The target thread id, or 0 to not use threads 262 | * 263 | * @return The current builder, for chaining convenience 264 | */ 265 | @NotNull 266 | public WebhookClientBuilder setThreadId(long threadId) { 267 | this.threadId = threadId; 268 | return this; 269 | } 270 | 271 | /** 272 | * Builds the {@link club.minnced.discord.webhook.WebhookClient} 273 | * with the current settings 274 | * 275 | * @return {@link club.minnced.discord.webhook.WebhookClient} instance 276 | */ 277 | @NotNull 278 | public WebhookClient build() { 279 | OkHttpClient client = this.client == null ? new OkHttpClient() : this.client; 280 | ScheduledExecutorService pool = this.pool != null ? this.pool : ThreadPools.getDefaultPool(id, threadFactory, isDaemon); 281 | return new WebhookClient(id, token, parseMessage, client, pool, allowedMentions, threadId); 282 | } 283 | 284 | /** 285 | * Builds the {@link club.minnced.discord.webhook.external.JDAWebhookClient} 286 | * with the current settings 287 | * 288 | * @return {@link club.minnced.discord.webhook.external.JDAWebhookClient} instance 289 | */ 290 | @NotNull 291 | public JDAWebhookClient buildJDA() { 292 | OkHttpClient client = this.client == null ? new OkHttpClient() : this.client; 293 | ScheduledExecutorService pool = this.pool != null ? this.pool : ThreadPools.getDefaultPool(id, threadFactory, isDaemon); 294 | return new JDAWebhookClient(id, token, parseMessage, client, pool, allowedMentions, threadId); 295 | } 296 | 297 | /** 298 | * Builds the {@link club.minnced.discord.webhook.external.D4JWebhookClient} 299 | * with the current settings 300 | * 301 | * @return {@link club.minnced.discord.webhook.external.D4JWebhookClient} instance 302 | */ 303 | @NotNull 304 | public D4JWebhookClient buildD4J() { 305 | OkHttpClient client = this.client == null ? new OkHttpClient() : this.client; 306 | ScheduledExecutorService pool = this.pool != null ? this.pool : ThreadPools.getDefaultPool(id, threadFactory, isDaemon); 307 | return new D4JWebhookClient(id, token, parseMessage, client, pool, allowedMentions, threadId); 308 | } 309 | 310 | /** 311 | * Builds the {@link club.minnced.discord.webhook.external.JavacordWebhookClient} 312 | * with the current settings 313 | * 314 | * @return {@link club.minnced.discord.webhook.external.JavacordWebhookClient} instance 315 | */ 316 | @NotNull 317 | public JavacordWebhookClient buildJavacord() { 318 | OkHttpClient client = this.client == null ? new OkHttpClient() : this.client; 319 | ScheduledExecutorService pool = this.pool != null ? this.pool : ThreadPools.getDefaultPool(id, threadFactory, isDaemon); 320 | return new JavacordWebhookClient(id, token, parseMessage, client, pool, allowedMentions, threadId); 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /src/main/java/club/minnced/discord/webhook/exception/HttpException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 Florian Spieß 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package club.minnced.discord.webhook.exception; 18 | 19 | import okhttp3.Headers; 20 | import org.jetbrains.annotations.NotNull; 21 | 22 | 23 | /** 24 | * Exception thrown in case of unexpected non-2xx HTTP response. 25 | */ 26 | public class HttpException extends RuntimeException { 27 | 28 | private final int code; 29 | private final String body; 30 | private final Headers headers; 31 | 32 | public HttpException(int code, @NotNull String body, @NotNull Headers headers) { 33 | super("Request returned failure " + code + ": " + body); 34 | this.body = body; 35 | this.code = code; 36 | this.headers = headers; 37 | } 38 | 39 | /** 40 | * The HTTP status code 41 | * 42 | * @return The status code 43 | */ 44 | public int getCode() { 45 | return code; 46 | } 47 | 48 | /** 49 | * The body of HTTP response 50 | * 51 | * @return The body 52 | */ 53 | @NotNull 54 | public String getBody() { 55 | return body; 56 | } 57 | 58 | /** 59 | * The HTTP headers. Useful to check content-type or rate limit buckets. 60 | * 61 | * @return {@link okhttp3.Headers} 62 | */ 63 | @NotNull 64 | public Headers getHeaders() { 65 | return headers; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/club/minnced/discord/webhook/external/D4JWebhookClient.java: -------------------------------------------------------------------------------- 1 | package club.minnced.discord.webhook.external; 2 | 3 | import club.minnced.discord.webhook.WebhookClient; 4 | import club.minnced.discord.webhook.WebhookClientBuilder; 5 | import club.minnced.discord.webhook.receive.ReadonlyMessage; 6 | import club.minnced.discord.webhook.send.AllowedMentions; 7 | import club.minnced.discord.webhook.send.WebhookMessage; 8 | import club.minnced.discord.webhook.send.WebhookMessageBuilder; 9 | import club.minnced.discord.webhook.util.ThreadPools; 10 | import discord4j.core.spec.MessageCreateSpec; 11 | import discord4j.core.spec.MessageEditSpec; 12 | import okhttp3.OkHttpClient; 13 | import org.jetbrains.annotations.NotNull; 14 | import reactor.core.publisher.Mono; 15 | 16 | import javax.annotation.CheckReturnValue; 17 | import java.util.Objects; 18 | import java.util.concurrent.ScheduledExecutorService; 19 | import java.util.function.Consumer; 20 | import java.util.regex.Matcher; 21 | 22 | public class D4JWebhookClient extends WebhookClient { 23 | public D4JWebhookClient(long id, String token, boolean parseMessage, OkHttpClient client, ScheduledExecutorService pool, AllowedMentions mentions) { 24 | this(id, token, parseMessage, client, pool, mentions, 0L); 25 | } 26 | 27 | public D4JWebhookClient(long id, String token, boolean parseMessage, OkHttpClient client, ScheduledExecutorService pool, AllowedMentions mentions, long threadId) { 28 | super(id, token, parseMessage, client, pool, mentions, threadId); 29 | } 30 | 31 | protected D4JWebhookClient(D4JWebhookClient parent, long threadId) { 32 | super(parent, threadId); 33 | } 34 | 35 | /** 36 | * Creates a D4JWebhookClient for the provided webhook. 37 | * 38 | *

You can use {@link #onThread(long)} to target specific threads on the channel. 39 | * 40 | * @param webhook 41 | * The webhook 42 | * 43 | * @throws NullPointerException 44 | * If the webhook is null or does not provide a token 45 | * 46 | * @return The D4JWebhookClient 47 | */ 48 | @NotNull 49 | public static D4JWebhookClient from(@NotNull discord4j.core.object.entity.Webhook webhook) { 50 | return WebhookClientBuilder.fromD4J(webhook).buildD4J(); 51 | } 52 | 53 | /** 54 | * Factory method to create a basic D4JWebhookClient with the provided id and token. 55 | * 56 | *

You can use {@link #onThread(long)} to target specific threads on the channel. 57 | * 58 | * @param id 59 | * The webhook id 60 | * @param token 61 | * The webhook token 62 | * 63 | * @throws java.lang.NullPointerException 64 | * If provided with null 65 | * 66 | * @return The D4JWebhookClient for the provided id and token 67 | */ 68 | @NotNull 69 | public static D4JWebhookClient withId(long id, @NotNull String token) { 70 | Objects.requireNonNull(token, "Token"); 71 | ScheduledExecutorService pool = ThreadPools.getDefaultPool(id, null, false); 72 | return new D4JWebhookClient(id, token, true, new OkHttpClient(), pool, AllowedMentions.all(), 0L); 73 | } 74 | 75 | /** 76 | * Factory method to create a basic D4JWebhookClient with the provided id and token. 77 | * 78 | *

You can use {@link #onThread(long)} to target specific threads on the channel. 79 | * 80 | * @param url 81 | * The url for the webhook 82 | * 83 | * @throws java.lang.NullPointerException 84 | * If provided with null 85 | * @throws java.lang.NumberFormatException 86 | * If no valid id is part o the url 87 | * 88 | * @return The D4JWebhookClient for the provided url 89 | */ 90 | @NotNull 91 | public static D4JWebhookClient withUrl(@NotNull String url) { 92 | Objects.requireNonNull(url, "URL"); 93 | Matcher matcher = WebhookClientBuilder.WEBHOOK_PATTERN.matcher(url); 94 | if (!matcher.matches()) { 95 | throw new IllegalArgumentException("Failed to parse webhook URL"); 96 | } 97 | return withId(Long.parseUnsignedLong(matcher.group(1)), matcher.group(2)); 98 | } 99 | 100 | @NotNull 101 | @Override 102 | public D4JWebhookClient onThread(final long threadId) { 103 | return new D4JWebhookClient(this, threadId); 104 | } 105 | 106 | /** 107 | * Sends the provided {@link MessageCreateSpec} to the webhook. 108 | * 109 | * @param callback 110 | * The callback used to specify the desired message settings 111 | * 112 | * @throws NullPointerException 113 | * If null is provided 114 | * 115 | * @return {@link Mono} 116 | * 117 | * @deprecated Replace wth {@link #send(MessageCreateSpec)} 118 | * 119 | * @see #isWait() 120 | * @see WebhookMessageBuilder#fromD4J(Consumer) 121 | */ 122 | @NotNull 123 | @Deprecated 124 | @CheckReturnValue 125 | public Mono send(@NotNull Consumer callback) { 126 | throw new UnsupportedOperationException("Cannot build messages via consumers in Discord4J 3.2.0! Please change to fromD4J(spec)"); 127 | } 128 | 129 | /** 130 | * Edits the target message with the provided {@link MessageCreateSpec} to the webhook. 131 | * 132 | * @param messageId 133 | * The target message id 134 | * @param callback 135 | * The callback used to specify the desired message settings 136 | * 137 | * @throws NullPointerException 138 | * If null is provided 139 | * 140 | * @return {@link Mono} 141 | * 142 | * @deprecated Replace with {@link #edit(long, MessageEditSpec)} 143 | * 144 | * @see #isWait() 145 | * @see WebhookMessageBuilder#fromD4J(Consumer) 146 | */ 147 | @NotNull 148 | @Deprecated 149 | @CheckReturnValue 150 | public Mono edit(long messageId, @NotNull Consumer callback) { 151 | throw new UnsupportedOperationException("Cannot build messages via consumers in Discord4J 3.2.0! Please change to fromD4J(spec)"); 152 | } 153 | 154 | /** 155 | * Edits the target message with the provided {@link MessageCreateSpec} to the webhook. 156 | * 157 | * @param messageId 158 | * The target message id 159 | * @param callback 160 | * The callback used to specify the desired message settings 161 | * 162 | * @throws NullPointerException 163 | * If null is provided 164 | * 165 | * @return {@link Mono} 166 | * 167 | * @deprecated Replace with {@link #edit(long, MessageEditSpec)} 168 | * 169 | * @see #isWait() 170 | * @see WebhookMessageBuilder#fromD4J(Consumer) 171 | */ 172 | @NotNull 173 | @Deprecated 174 | @CheckReturnValue 175 | public Mono edit(@NotNull String messageId, @NotNull Consumer callback) { 176 | throw new UnsupportedOperationException("Cannot build messages via consumers in Discord4J 3.2.0! Please change to fromD4J(spec)"); 177 | } 178 | 179 | /** 180 | * Sends the provided {@link MessageCreateSpec} to the webhook. 181 | * 182 | * @param spec 183 | * The message create spec used to specify the desired message settings 184 | * 185 | * @throws NullPointerException 186 | * If null is provided 187 | * 188 | * @return {@link Mono} 189 | * 190 | * @see #isWait() 191 | * @see WebhookMessageBuilder#fromD4J(MessageCreateSpec) 192 | */ 193 | @NotNull 194 | @CheckReturnValue 195 | public Mono send(@NotNull MessageCreateSpec spec) { 196 | WebhookMessage message = WebhookMessageBuilder.fromD4J(spec).build(); 197 | return Mono.fromFuture(() -> send(message)); 198 | } 199 | 200 | /** 201 | * Edits the target message with the provided {@link MessageCreateSpec} to the webhook. 202 | * 203 | * @param messageId 204 | * The target message id 205 | * @param spec 206 | * The message edit spec used to specify the desired message settings 207 | * 208 | * @throws NullPointerException 209 | * If null is provided 210 | * 211 | * @return {@link Mono} 212 | * 213 | * @see #isWait() 214 | * @see WebhookMessageBuilder#fromD4J(MessageEditSpec) 215 | */ 216 | @NotNull 217 | @CheckReturnValue 218 | public Mono edit(long messageId, @NotNull MessageEditSpec spec) { 219 | WebhookMessage message = WebhookMessageBuilder.fromD4J(spec).build(); 220 | return Mono.fromFuture(() -> edit(messageId, message)); 221 | } 222 | 223 | /** 224 | * Edits the target message with the provided {@link MessageCreateSpec} to the webhook. 225 | * 226 | * @param messageId 227 | * The target message id 228 | * @param spec 229 | * The message edit spec used to specify the desired message settings 230 | * 231 | * @throws NullPointerException 232 | * If null is provided 233 | * 234 | * @return {@link Mono} 235 | * 236 | * @see #isWait() 237 | * @see WebhookMessageBuilder#fromD4J(MessageEditSpec) 238 | */ 239 | @NotNull 240 | @CheckReturnValue 241 | public Mono edit(@NotNull String messageId, @NotNull MessageEditSpec spec) { 242 | WebhookMessage message = WebhookMessageBuilder.fromD4J(spec).build(); 243 | return Mono.fromFuture(() -> edit(messageId, message)); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/main/java/club/minnced/discord/webhook/external/JDAWebhookClient.java: -------------------------------------------------------------------------------- 1 | package club.minnced.discord.webhook.external; 2 | 3 | import club.minnced.discord.webhook.WebhookClient; 4 | import club.minnced.discord.webhook.WebhookClientBuilder; 5 | import club.minnced.discord.webhook.receive.ReadonlyMessage; 6 | import club.minnced.discord.webhook.send.AllowedMentions; 7 | import club.minnced.discord.webhook.send.WebhookEmbedBuilder; 8 | import club.minnced.discord.webhook.send.WebhookMessageBuilder; 9 | import club.minnced.discord.webhook.util.ThreadPools; 10 | import net.dv8tion.jda.api.entities.Message; 11 | import okhttp3.OkHttpClient; 12 | import org.jetbrains.annotations.NotNull; 13 | 14 | import java.util.Objects; 15 | import java.util.concurrent.CompletableFuture; 16 | import java.util.concurrent.ScheduledExecutorService; 17 | import java.util.regex.Matcher; 18 | 19 | public class JDAWebhookClient extends WebhookClient { 20 | public JDAWebhookClient(long id, String token, boolean parseMessage, OkHttpClient client, ScheduledExecutorService pool, AllowedMentions mentions) { 21 | this(id, token, parseMessage, client, pool, mentions, 0L); 22 | } 23 | 24 | public JDAWebhookClient(long id, String token, boolean parseMessage, OkHttpClient client, ScheduledExecutorService pool, AllowedMentions mentions, long threadId) { 25 | super(id, token, parseMessage, client, pool, mentions, threadId); 26 | } 27 | 28 | protected JDAWebhookClient(JDAWebhookClient parent, long threadId) { 29 | super(parent, threadId); 30 | } 31 | 32 | /** 33 | * Creates a WebhookClient for the provided webhook. 34 | * 35 | *

You can use {@link #onThread(long)} to target specific threads on the channel. 36 | * 37 | * @param webhook 38 | * The webhook 39 | * 40 | * @throws NullPointerException 41 | * If the webhook is null or does not provide a token 42 | * 43 | * @return The JDAWebhookClient 44 | */ 45 | @NotNull 46 | public static JDAWebhookClient from(@NotNull net.dv8tion.jda.api.entities.Webhook webhook) { 47 | return WebhookClientBuilder.fromJDA(webhook).buildJDA(); 48 | } 49 | 50 | /** 51 | * Factory method to create a basic JDAWebhookClient with the provided id and token. 52 | * 53 | *

You can use {@link #onThread(long)} to target specific threads on the channel. 54 | * 55 | * @param id 56 | * The webhook id 57 | * @param token 58 | * The webhook token 59 | * 60 | * @throws java.lang.NullPointerException 61 | * If provided with null 62 | * 63 | * @return The JDAWebhookClient for the provided id and token 64 | */ 65 | @NotNull 66 | public static JDAWebhookClient withId(long id, @NotNull String token) { 67 | Objects.requireNonNull(token, "Token"); 68 | ScheduledExecutorService pool = ThreadPools.getDefaultPool(id, null, false); 69 | return new JDAWebhookClient(id, token, true, new OkHttpClient(), pool, AllowedMentions.all()); 70 | } 71 | 72 | /** 73 | * Factory method to create a basic JDAWebhookClient with the provided id and token. 74 | * 75 | *

You can use {@link #onThread(long)} to target specific threads on the channel. 76 | * 77 | * @param url 78 | * The url for the webhook 79 | * 80 | * @throws java.lang.NullPointerException 81 | * If provided with null 82 | * @throws java.lang.NumberFormatException 83 | * If no valid id is part o the url 84 | * 85 | * @return The JDAWebhookClient for the provided url 86 | */ 87 | @NotNull 88 | public static JDAWebhookClient withUrl(@NotNull String url) { 89 | Objects.requireNonNull(url, "URL"); 90 | Matcher matcher = WebhookClientBuilder.WEBHOOK_PATTERN.matcher(url); 91 | if (!matcher.matches()) { 92 | throw new IllegalArgumentException("Failed to parse webhook URL"); 93 | } 94 | return withId(Long.parseUnsignedLong(matcher.group(1)), matcher.group(2)); 95 | } 96 | 97 | @NotNull 98 | @Override 99 | public JDAWebhookClient onThread(long threadId) { 100 | return new JDAWebhookClient(this, threadId); 101 | } 102 | 103 | /** 104 | * Sends the provided {@link net.dv8tion.jda.api.entities.Message Message} to the webhook. 105 | * 106 | * @param message 107 | * The message to send 108 | * 109 | * @throws NullPointerException 110 | * If null is provided 111 | * 112 | * @return {@link CompletableFuture} 113 | * 114 | * @see #isWait() 115 | * @see WebhookMessageBuilder#fromJDA(Message) 116 | */ 117 | @NotNull 118 | public CompletableFuture send(@NotNull net.dv8tion.jda.api.entities.Message message) { 119 | return send(WebhookMessageBuilder.fromJDA(message).build()); 120 | } 121 | 122 | /** 123 | * Sends the provided {@link net.dv8tion.jda.api.entities.MessageEmbed MessageEmbed} to the webhook. 124 | * 125 | * @param embed 126 | * The embed to send 127 | * 128 | * @throws NullPointerException 129 | * If null is provided 130 | * 131 | * @return {@link CompletableFuture} 132 | * 133 | * @see #isWait() 134 | * @see WebhookEmbedBuilder#fromJDA(net.dv8tion.jda.api.entities.MessageEmbed) 135 | */ 136 | @NotNull 137 | public CompletableFuture send(@NotNull net.dv8tion.jda.api.entities.MessageEmbed embed) { 138 | return send(WebhookEmbedBuilder.fromJDA(embed).build()); 139 | } 140 | 141 | /** 142 | * Edits the target message with the provided {@link net.dv8tion.jda.api.entities.Message Message} to the webhook. 143 | * 144 | * @param messageId 145 | * The target message id 146 | * @param message 147 | * The message to send 148 | * 149 | * @throws NullPointerException 150 | * If null is provided 151 | * 152 | * @return {@link CompletableFuture} 153 | * 154 | * @see #isWait() 155 | * @see WebhookMessageBuilder#fromJDA(Message) 156 | */ 157 | @NotNull 158 | public CompletableFuture edit(long messageId, @NotNull net.dv8tion.jda.api.entities.Message message) { 159 | return edit(messageId, WebhookMessageBuilder.fromJDA(message).build()); 160 | } 161 | 162 | /** 163 | * Edits the target message with the provided {@link net.dv8tion.jda.api.entities.MessageEmbed MessageEmbed} to the webhook. 164 | * 165 | * @param messageId 166 | * The target message id 167 | * @param embed 168 | * The embed to send 169 | * 170 | * @throws NullPointerException 171 | * If null is provided 172 | * 173 | * @return {@link CompletableFuture} 174 | * 175 | * @see #isWait() 176 | * @see WebhookEmbedBuilder#fromJDA(net.dv8tion.jda.api.entities.MessageEmbed) 177 | */ 178 | @NotNull 179 | public CompletableFuture edit(long messageId, @NotNull net.dv8tion.jda.api.entities.MessageEmbed embed) { 180 | return edit(messageId, WebhookEmbedBuilder.fromJDA(embed).build()); 181 | } 182 | 183 | /** 184 | * Edits the target message with the provided {@link net.dv8tion.jda.api.entities.Message Message} to the webhook. 185 | * 186 | * @param messageId 187 | * The target message id 188 | * @param message 189 | * The message to send 190 | * 191 | * @throws NullPointerException 192 | * If null is provided 193 | * 194 | * @return {@link CompletableFuture} 195 | * 196 | * @see #isWait() 197 | * @see WebhookMessageBuilder#fromJDA(Message) 198 | */ 199 | @NotNull 200 | public CompletableFuture edit(@NotNull String messageId, @NotNull net.dv8tion.jda.api.entities.Message message) { 201 | return edit(messageId, WebhookMessageBuilder.fromJDA(message).build()); 202 | } 203 | 204 | /** 205 | * Edits the target message with the provided {@link net.dv8tion.jda.api.entities.MessageEmbed MessageEmbed} to the webhook. 206 | * 207 | * @param messageId 208 | * The target message id 209 | * @param embed 210 | * The embed to send 211 | * 212 | * @throws NullPointerException 213 | * If null is provided 214 | * 215 | * @return {@link CompletableFuture} 216 | * 217 | * @see #isWait() 218 | * @see WebhookEmbedBuilder#fromJDA(net.dv8tion.jda.api.entities.MessageEmbed) 219 | */ 220 | @NotNull 221 | public CompletableFuture edit(@NotNull String messageId, @NotNull net.dv8tion.jda.api.entities.MessageEmbed embed) { 222 | return edit(messageId, WebhookEmbedBuilder.fromJDA(embed).build()); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/main/java/club/minnced/discord/webhook/external/JavacordWebhookClient.java: -------------------------------------------------------------------------------- 1 | package club.minnced.discord.webhook.external; 2 | 3 | import club.minnced.discord.webhook.WebhookClient; 4 | import club.minnced.discord.webhook.WebhookClientBuilder; 5 | import club.minnced.discord.webhook.receive.ReadonlyMessage; 6 | import club.minnced.discord.webhook.send.AllowedMentions; 7 | import club.minnced.discord.webhook.send.WebhookEmbedBuilder; 8 | import club.minnced.discord.webhook.send.WebhookMessageBuilder; 9 | import club.minnced.discord.webhook.util.ThreadPools; 10 | import okhttp3.OkHttpClient; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | import java.util.Objects; 14 | import java.util.concurrent.CompletableFuture; 15 | import java.util.concurrent.ScheduledExecutorService; 16 | import java.util.regex.Matcher; 17 | 18 | public class JavacordWebhookClient extends WebhookClient { 19 | public JavacordWebhookClient(long id, String token, boolean parseMessage, OkHttpClient client, ScheduledExecutorService pool, AllowedMentions mentions) { 20 | this(id, token, parseMessage, client, pool, mentions, 0L); 21 | } 22 | 23 | public JavacordWebhookClient(long id, String token, boolean parseMessage, OkHttpClient client, ScheduledExecutorService pool, AllowedMentions mentions, long threadId) { 24 | super(id, token, parseMessage, client, pool, mentions, threadId); 25 | } 26 | 27 | protected JavacordWebhookClient(JavacordWebhookClient parent, long threadId) { 28 | super(parent, threadId); 29 | } 30 | 31 | /** 32 | * Creates a WebhookClient for the provided webhook. 33 | * 34 | *

You can use {@link #onThread(long)} to target specific threads on the channel. 35 | * 36 | * @param webhook 37 | * The webhook 38 | * 39 | * @throws NullPointerException 40 | * If the webhook is null or does not provide a token 41 | * 42 | * @return The JavacordWebhookClient 43 | */ 44 | @NotNull 45 | public static JavacordWebhookClient from(@NotNull org.javacord.api.entity.webhook.Webhook webhook) { 46 | return WebhookClientBuilder.fromJavacord(webhook).buildJavacord(); 47 | } 48 | 49 | /** 50 | * Factory method to create a basic JavacordWebhookClient with the provided id and token. 51 | * 52 | *

You can use {@link #onThread(long)} to target specific threads on the channel. 53 | * 54 | * @param id 55 | * The webhook id 56 | * @param token 57 | * The webhook token 58 | * 59 | * @throws java.lang.NullPointerException 60 | * If provided with null 61 | * 62 | * @return The JavacordWebhookClient for the provided id and token 63 | */ 64 | @NotNull 65 | public static JavacordWebhookClient withId(long id, @NotNull String token) { 66 | Objects.requireNonNull(token, "Token"); 67 | ScheduledExecutorService pool = ThreadPools.getDefaultPool(id, null, false); 68 | return new JavacordWebhookClient(id, token, true, new OkHttpClient(), pool, AllowedMentions.all()); 69 | } 70 | 71 | /** 72 | * Factory method to create a basic JavacordWebhookClient with the provided id and token. 73 | * 74 | *

You can use {@link #onThread(long)} to target specific threads on the channel. 75 | * 76 | * @param url 77 | * The url for the webhook 78 | * 79 | * @throws java.lang.NullPointerException 80 | * If provided with null 81 | * @throws java.lang.NumberFormatException 82 | * If no valid id is part o the url 83 | * 84 | * @return The JavacordWebhookClient for the provided url 85 | */ 86 | @NotNull 87 | public static JavacordWebhookClient withUrl(@NotNull String url) { 88 | Objects.requireNonNull(url, "URL"); 89 | Matcher matcher = WebhookClientBuilder.WEBHOOK_PATTERN.matcher(url); 90 | if (!matcher.matches()) { 91 | throw new IllegalArgumentException("Failed to parse webhook URL"); 92 | } 93 | return withId(Long.parseUnsignedLong(matcher.group(1)), matcher.group(2)); 94 | } 95 | 96 | @NotNull 97 | @Override 98 | public JavacordWebhookClient onThread(long threadId) { 99 | return new JavacordWebhookClient(this, threadId); 100 | } 101 | 102 | /** 103 | * Sends the provided {@link org.javacord.api.entity.message.Message Message} to the webhook. 104 | * 105 | * @param message 106 | * The message to send 107 | * 108 | * @throws NullPointerException 109 | * If null is provided 110 | * 111 | * @return {@link CompletableFuture} 112 | * 113 | * @see #isWait() 114 | * @see WebhookMessageBuilder#fromJavacord(org.javacord.api.entity.message.Message) 115 | */ 116 | @NotNull 117 | public CompletableFuture send(@NotNull org.javacord.api.entity.message.Message message) { 118 | return send(WebhookMessageBuilder.fromJavacord(message).build()); 119 | } 120 | 121 | /** 122 | * Sends the provided {@link org.javacord.api.entity.message.embed.Embed Embed} to the webhook. 123 | * 124 | * @param embed 125 | * The embed to send 126 | * 127 | * @throws NullPointerException 128 | * If null is provided 129 | * 130 | * @return {@link CompletableFuture} 131 | * 132 | * @see #isWait() 133 | * @see WebhookEmbedBuilder#fromJavacord(org.javacord.api.entity.message.embed.Embed) 134 | */ 135 | @NotNull 136 | public CompletableFuture send(@NotNull org.javacord.api.entity.message.embed.Embed embed) { 137 | return send(WebhookEmbedBuilder.fromJavacord(embed).build()); 138 | } 139 | 140 | /** 141 | * Edits the target message with the provided {@link org.javacord.api.entity.message.Message Message} to the webhook. 142 | * 143 | * @param messageId 144 | * The target message id 145 | * @param message 146 | * The message to send 147 | * 148 | * @throws NullPointerException 149 | * If null is provided 150 | * 151 | * @return {@link CompletableFuture} 152 | * 153 | * @see #isWait() 154 | * @see WebhookMessageBuilder#fromJavacord(org.javacord.api.entity.message.Message) 155 | */ 156 | @NotNull 157 | public CompletableFuture edit(long messageId, @NotNull org.javacord.api.entity.message.Message message) { 158 | return edit(messageId, WebhookMessageBuilder.fromJavacord(message).build()); 159 | } 160 | 161 | /** 162 | * Edits the target message with the provided {@link org.javacord.api.entity.message.embed.Embed Embed} to the webhook. 163 | * 164 | * @param messageId 165 | * The target message id 166 | * @param embed 167 | * The embed to send 168 | * 169 | * @throws NullPointerException 170 | * If null is provided 171 | * 172 | * @return {@link CompletableFuture} 173 | * 174 | * @see #isWait() 175 | * @see WebhookEmbedBuilder#fromJavacord(org.javacord.api.entity.message.embed.Embed) 176 | */ 177 | @NotNull 178 | public CompletableFuture edit(long messageId, @NotNull org.javacord.api.entity.message.embed.Embed embed) { 179 | return edit(messageId, WebhookEmbedBuilder.fromJavacord(embed).build()); 180 | } 181 | 182 | /** 183 | * Edits the target message with the provided {@link org.javacord.api.entity.message.Message Message} to the webhook. 184 | * 185 | * @param messageId 186 | * The target message id 187 | * @param message 188 | * The message to send 189 | * 190 | * @throws NullPointerException 191 | * If null is provided 192 | * 193 | * @return {@link CompletableFuture} 194 | * 195 | * @see #isWait() 196 | * @see WebhookMessageBuilder#fromJavacord(org.javacord.api.entity.message.Message) 197 | */ 198 | @NotNull 199 | public CompletableFuture edit(@NotNull String messageId, @NotNull org.javacord.api.entity.message.Message message) { 200 | return edit(messageId, WebhookMessageBuilder.fromJavacord(message).build()); 201 | } 202 | 203 | /** 204 | * Edits the target message with the provided {@link org.javacord.api.entity.message.embed.Embed Embed} to the webhook. 205 | * 206 | * @param messageId 207 | * The target message id 208 | * @param embed 209 | * The embed to send 210 | * 211 | * @throws NullPointerException 212 | * If null is provided 213 | * 214 | * @return {@link CompletableFuture} 215 | * 216 | * @see #isWait() 217 | * @see WebhookEmbedBuilder#fromJavacord(org.javacord.api.entity.message.embed.Embed) 218 | */ 219 | @NotNull 220 | public CompletableFuture edit(@NotNull String messageId, @NotNull org.javacord.api.entity.message.embed.Embed embed) { 221 | return edit(messageId, WebhookEmbedBuilder.fromJavacord(embed).build()); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/main/java/club/minnced/discord/webhook/receive/EntityFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 Florian Spieß 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package club.minnced.discord.webhook.receive; 18 | 19 | import club.minnced.discord.webhook.send.WebhookEmbed; 20 | import org.jetbrains.annotations.NotNull; 21 | import org.jetbrains.annotations.Nullable; 22 | import org.json.JSONArray; 23 | import org.json.JSONObject; 24 | 25 | import java.time.OffsetDateTime; 26 | import java.util.ArrayList; 27 | import java.util.Collections; 28 | import java.util.List; 29 | import java.util.function.Function; 30 | 31 | /** 32 | * Internal factory used to convert JSON representations 33 | * into java objects. 34 | */ 35 | public class EntityFactory { 36 | /** 37 | * Converts a user json into a {@link club.minnced.discord.webhook.receive.ReadonlyUser} 38 | * 39 | * @param json 40 | * The JSON representation 41 | * 42 | * @return {@link club.minnced.discord.webhook.receive.ReadonlyUser} 43 | */ 44 | @NotNull 45 | public static ReadonlyUser makeUser(@NotNull JSONObject json) { 46 | final long id = Long.parseUnsignedLong(json.getString("id")); 47 | final String name = json.getString("username"); 48 | final String avatar = json.optString("avatar", null); 49 | final short discriminator = Short.parseShort(json.getString("discriminator")); 50 | final boolean bot = !json.isNull("bot") && json.getBoolean("bot"); 51 | 52 | return new ReadonlyUser(id, discriminator, bot, name, avatar); 53 | } 54 | 55 | /** 56 | * Converts a attachment json into a {@link club.minnced.discord.webhook.receive.ReadonlyAttachment} 57 | * 58 | * @param json 59 | * The JSON representation 60 | * 61 | * @return {@link club.minnced.discord.webhook.receive.ReadonlyAttachment} 62 | */ 63 | @NotNull 64 | public static ReadonlyAttachment makeAttachment(@NotNull JSONObject json) { 65 | final String url = json.getString("url"); 66 | final String proxy = json.getString("proxy_url"); 67 | final String name = json.getString("filename"); 68 | final int size = json.getInt("size"); 69 | final int width = json.optInt("width", -1); 70 | final int height = json.optInt("height", -1); 71 | final long id = Long.parseUnsignedLong(json.getString("id")); 72 | return new ReadonlyAttachment(url, proxy, name, width, height, size, id); 73 | } 74 | 75 | /** 76 | * Converts a field json into a {@link club.minnced.discord.webhook.send.WebhookEmbed.EmbedField} 77 | * 78 | * @param json 79 | * The JSON representation 80 | * 81 | * @return {@link club.minnced.discord.webhook.send.WebhookEmbed.EmbedField} 82 | */ 83 | @Nullable 84 | public static WebhookEmbed.EmbedField makeEmbedField(@Nullable JSONObject json) { 85 | if (json == null) 86 | return null; 87 | final String name = json.getString("name"); 88 | final String value = json.getString("value"); 89 | final boolean inline = !json.isNull("inline") && json.getBoolean("inline"); 90 | return new WebhookEmbed.EmbedField(inline, name, value); 91 | } 92 | 93 | /** 94 | * Converts an author json into a {@link club.minnced.discord.webhook.send.WebhookEmbed.EmbedAuthor} 95 | * 96 | * @param json 97 | * The JSON representation 98 | * 99 | * @return {@link club.minnced.discord.webhook.send.WebhookEmbed.EmbedAuthor} 100 | */ 101 | @Nullable 102 | public static WebhookEmbed.EmbedAuthor makeEmbedAuthor(@Nullable JSONObject json) { 103 | if (json == null) 104 | return null; 105 | final String name = json.getString("name"); 106 | final String url = json.optString("url", null); 107 | final String icon = json.optString("icon_url", null); 108 | return new WebhookEmbed.EmbedAuthor(name, icon, url); 109 | } 110 | 111 | /** 112 | * Converts a footer json into a {@link club.minnced.discord.webhook.send.WebhookEmbed.EmbedFooter} 113 | * 114 | * @param json 115 | * The JSON representation 116 | * 117 | * @return {@link club.minnced.discord.webhook.send.WebhookEmbed.EmbedFooter} 118 | */ 119 | @Nullable 120 | public static WebhookEmbed.EmbedFooter makeEmbedFooter(@Nullable JSONObject json) { 121 | if (json == null) 122 | return null; 123 | final String text = json.getString("text"); 124 | final String icon = json.optString("icon_url", null); 125 | return new WebhookEmbed.EmbedFooter(text, icon); 126 | } 127 | 128 | /** 129 | * Converts an embed json into a {@link club.minnced.discord.webhook.send.WebhookEmbed.EmbedTitle} 130 | * 131 | * @param json 132 | * The JSON representation 133 | * 134 | * @return {@link club.minnced.discord.webhook.send.WebhookEmbed.EmbedTitle} 135 | */ 136 | @Nullable 137 | public static WebhookEmbed.EmbedTitle makeEmbedTitle(@NotNull JSONObject json) { 138 | final String text = json.optString("title", null); 139 | if (text == null) 140 | return null; 141 | final String url = json.optString("url", null); 142 | return new WebhookEmbed.EmbedTitle(text, url); 143 | } 144 | 145 | /** 146 | * Converts a image/thumbnail json into a {@link club.minnced.discord.webhook.receive.ReadonlyEmbed.EmbedImage} 147 | * 148 | * @param json 149 | * The JSON representation 150 | * 151 | * @return {@link club.minnced.discord.webhook.receive.ReadonlyEmbed.EmbedImage} 152 | */ 153 | @Nullable 154 | public static ReadonlyEmbed.EmbedImage makeEmbedImage(@Nullable JSONObject json) { 155 | if (json == null) 156 | return null; 157 | final String url = json.getString("url"); 158 | final String proxyUrl = json.getString("proxy_url"); 159 | final int width = json.getInt("width"); 160 | final int height = json.getInt("height"); 161 | return new ReadonlyEmbed.EmbedImage(url, proxyUrl, width, height); 162 | } 163 | 164 | /** 165 | * Converts a provider json into a {@link club.minnced.discord.webhook.receive.ReadonlyEmbed.EmbedProvider} 166 | * 167 | * @param json 168 | * The JSON representation 169 | * 170 | * @return {@link club.minnced.discord.webhook.receive.ReadonlyEmbed.EmbedProvider} 171 | */ 172 | @Nullable 173 | public static ReadonlyEmbed.EmbedProvider makeEmbedProvider(@Nullable JSONObject json) { 174 | if (json == null) 175 | return null; 176 | final String url = json.optString("url", null); 177 | final String name = json.optString("name", null); 178 | return new ReadonlyEmbed.EmbedProvider(name, url); 179 | } 180 | 181 | /** 182 | * Converts a video json into a {@link club.minnced.discord.webhook.receive.ReadonlyEmbed.EmbedVideo} 183 | * 184 | * @param json 185 | * The JSON representation 186 | * 187 | * @return {@link club.minnced.discord.webhook.receive.ReadonlyEmbed.EmbedVideo} 188 | */ 189 | @Nullable 190 | public static ReadonlyEmbed.EmbedVideo makeEmbedVideo(@Nullable JSONObject json) { 191 | if (json == null) 192 | return null; 193 | final String url = json.getString("url"); 194 | final int height = json.getInt("height"); 195 | final int width = json.getInt("width"); 196 | return new ReadonlyEmbed.EmbedVideo(url, width, height); 197 | } 198 | 199 | /** 200 | * Converts an embed json into a {@link club.minnced.discord.webhook.receive.ReadonlyEmbed} 201 | * 202 | * @param json 203 | * The JSON representation 204 | * 205 | * @return {@link club.minnced.discord.webhook.receive.ReadonlyEmbed} 206 | */ 207 | @NotNull 208 | public static ReadonlyEmbed makeEmbed(@NotNull JSONObject json) { 209 | final String description = json.optString("description", null); 210 | final Integer color = json.isNull("color") ? null : json.getInt("color"); 211 | final ReadonlyEmbed.EmbedImage image = makeEmbedImage(json.optJSONObject("image")); 212 | final ReadonlyEmbed.EmbedImage thumbnail = makeEmbedImage(json.optJSONObject("thumbnail")); 213 | final ReadonlyEmbed.EmbedProvider provider = makeEmbedProvider(json.optJSONObject("provider")); 214 | final ReadonlyEmbed.EmbedVideo video = makeEmbedVideo(json.optJSONObject("video")); 215 | final WebhookEmbed.EmbedFooter footer = makeEmbedFooter(json.optJSONObject("footer")); 216 | final WebhookEmbed.EmbedAuthor author = makeEmbedAuthor(json.optJSONObject("author")); 217 | final WebhookEmbed.EmbedTitle title = makeEmbedTitle(json); 218 | final OffsetDateTime timestamp; 219 | if (json.isNull("timestamp")) 220 | timestamp = null; 221 | else 222 | timestamp = OffsetDateTime.parse(json.getString("timestamp")); 223 | final JSONArray fieldArray = json.optJSONArray("fields"); 224 | final List fields = new ArrayList<>(); 225 | if (fieldArray != null) { 226 | for (int i = 0; i < fieldArray.length(); i++) { 227 | JSONObject obj = fieldArray.getJSONObject(i); 228 | WebhookEmbed.EmbedField field = makeEmbedField(obj); 229 | if (field != null) 230 | fields.add(field); 231 | } 232 | } 233 | return new ReadonlyEmbed(timestamp, color, description, thumbnail, image, footer, title, author, fields, provider, video); 234 | } 235 | 236 | /** 237 | * Converts a message json into a {@link club.minnced.discord.webhook.receive.ReadonlyMessage} 238 | * 239 | * @param json 240 | * The JSON representation 241 | * 242 | * @return {@link club.minnced.discord.webhook.receive.ReadonlyMessage} 243 | */ 244 | @NotNull 245 | public static ReadonlyMessage makeMessage(@NotNull JSONObject json) { 246 | final long id = Long.parseUnsignedLong(json.getString("id")); 247 | final long channelId = Long.parseUnsignedLong(json.getString("channel_id")); 248 | final ReadonlyUser author = makeUser(json.getJSONObject("author")); 249 | final String content = json.getString("content"); 250 | final boolean tts = json.getBoolean("tts"); 251 | final boolean mentionEveryone = json.getBoolean("mention_everyone"); 252 | final int flags = json.optInt("flags", 0); 253 | final JSONArray usersArray = json.getJSONArray("mentions"); 254 | final JSONArray rolesArray = json.getJSONArray("mention_roles"); 255 | final JSONArray embedArray = json.getJSONArray("embeds"); 256 | final JSONArray attachmentArray = json.getJSONArray("attachments"); 257 | final List mentionedUsers = convertToList(usersArray, EntityFactory::makeUser); 258 | final List embeds = convertToList(embedArray, EntityFactory::makeEmbed); 259 | final List attachments = convertToList(attachmentArray, EntityFactory::makeAttachment); 260 | final List mentionedRoles = new ArrayList<>(); 261 | for (int i = 0; i < rolesArray.length(); i++) { 262 | mentionedRoles.add(Long.parseUnsignedLong(rolesArray.getString(i))); 263 | } 264 | return new ReadonlyMessage( 265 | id, channelId, mentionEveryone, tts, 266 | flags, author, content, 267 | embeds, attachments, 268 | mentionedUsers, mentionedRoles); 269 | } 270 | 271 | private static List convertToList(JSONArray arr, Function converter) { 272 | if (arr == null) 273 | return Collections.emptyList(); 274 | final List list = new ArrayList<>(); 275 | for (int i = 0; i < arr.length(); i++) { 276 | JSONObject json = arr.getJSONObject(i); 277 | T out = converter.apply(json); 278 | if (out != null) 279 | list.add(out); 280 | } 281 | return Collections.unmodifiableList(list); 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/main/java/club/minnced/discord/webhook/receive/ReadonlyAttachment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 Florian Spieß 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package club.minnced.discord.webhook.receive; 18 | 19 | import org.jetbrains.annotations.NotNull; 20 | import org.json.JSONObject; 21 | import org.json.JSONPropertyName; 22 | import org.json.JSONString; 23 | 24 | /** 25 | * Readonly message attachment meta-data. 26 | *
This does not actually contain the file but only meta-data 27 | * useful to retrieve the actual attachment. 28 | */ 29 | public class ReadonlyAttachment implements JSONString { 30 | private final String url; 31 | private final String proxyUrl; 32 | private final String fileName; 33 | private final int width, height; 34 | private final int size; 35 | private final long id; 36 | 37 | public ReadonlyAttachment( 38 | @NotNull String url, @NotNull String proxyUrl, @NotNull String fileName, 39 | int width, int height, int size, long id) { 40 | this.url = url; 41 | this.proxyUrl = proxyUrl; 42 | this.fileName = fileName; 43 | this.width = width; 44 | this.height = height; 45 | this.size = size; 46 | this.id = id; 47 | } 48 | 49 | /** 50 | * The URL for this attachment 51 | * 52 | * @return The url 53 | */ 54 | @NotNull 55 | public String getUrl() { 56 | return url; 57 | } 58 | 59 | /** 60 | * The proxy url for this attachment, this is used by the client 61 | * to generate previews of images. 62 | * 63 | * @return The proxy url 64 | */ 65 | @NotNull 66 | @JSONPropertyName("proxy_url") 67 | public String getProxyUrl() { 68 | return proxyUrl; 69 | } 70 | 71 | /** 72 | * The name of this attachment 73 | * 74 | * @return The file name 75 | */ 76 | @NotNull 77 | @JSONPropertyName("filename") 78 | public String getFileName() { 79 | return fileName; 80 | } 81 | 82 | /** 83 | * The approximated size of this embed in bytes 84 | * 85 | * @return The approximated size in bytes 86 | */ 87 | public int getSize() { 88 | return size; 89 | } 90 | 91 | /** 92 | * Width of the attachment, this is only relevant to images and videos 93 | * 94 | * @return Width of this image, or -1 if not an image or video 95 | */ 96 | public int getWidth() { 97 | return width; 98 | } 99 | 100 | /** 101 | * Height of the attachment, this is only relevant to images and videos 102 | * 103 | * @return Height of this image, or -1 if not an image or video 104 | */ 105 | public int getHeight() { 106 | return height; 107 | } 108 | 109 | /** 110 | * The id of this attachment 111 | * 112 | * @return The idi 113 | */ 114 | public long getId() { 115 | return id; 116 | } 117 | 118 | /** 119 | * JSON representation of this attachment 120 | * 121 | * @return The JSON representation 122 | */ 123 | @Override 124 | public String toString() { 125 | return toJSONString(); 126 | } 127 | 128 | @Override 129 | public String toJSONString() { 130 | return new JSONObject(this).toString(); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/club/minnced/discord/webhook/receive/ReadonlyEmbed.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 Florian Spieß 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package club.minnced.discord.webhook.receive; 18 | 19 | import club.minnced.discord.webhook.send.WebhookEmbed; 20 | import org.jetbrains.annotations.NotNull; 21 | import org.jetbrains.annotations.Nullable; 22 | import org.json.JSONObject; 23 | import org.json.JSONPropertyName; 24 | import org.json.JSONString; 25 | 26 | import java.time.OffsetDateTime; 27 | import java.util.List; 28 | 29 | /** 30 | * Extension of {@link club.minnced.discord.webhook.send.WebhookEmbed} 31 | * with additional meta-data on receivable embeds. 32 | */ 33 | public class ReadonlyEmbed extends WebhookEmbed { 34 | private final EmbedProvider provider; 35 | private final EmbedImage thumbnail, image; 36 | private final EmbedVideo video; 37 | 38 | public ReadonlyEmbed( 39 | @Nullable OffsetDateTime timestamp, @Nullable Integer color, @Nullable String description, 40 | @Nullable EmbedImage thumbnail, @Nullable EmbedImage image, @Nullable EmbedFooter footer, 41 | @Nullable EmbedTitle title, @Nullable EmbedAuthor author, @NotNull List fields, 42 | @Nullable EmbedProvider provider, @Nullable EmbedVideo video) { 43 | super(timestamp, color, description, 44 | thumbnail == null ? null : thumbnail.getUrl(), 45 | image == null ? null : image.getUrl(), 46 | footer, title, author, fields); 47 | this.thumbnail = thumbnail; 48 | this.image = image; 49 | this.provider = provider; 50 | this.video = video; 51 | } 52 | 53 | /** 54 | * The {@link club.minnced.discord.webhook.receive.ReadonlyEmbed.EmbedProvider} 55 | *
Used for services that are automatically embedded by discord when posting a link, 56 | * this includes services like youtube or twitter. 57 | * 58 | * @return Possibly-null embed provider 59 | */ 60 | @Nullable 61 | public EmbedProvider getProvider() { 62 | return provider; 63 | } 64 | 65 | /** 66 | * The thumbnail of this embed. 67 | * 68 | * @return Possibly-null {@link club.minnced.discord.webhook.receive.ReadonlyEmbed.EmbedImage} for the thumbnail 69 | */ 70 | @Nullable 71 | public EmbedImage getThumbnail() { 72 | return thumbnail; 73 | } 74 | 75 | /** 76 | * The image of this embed. 77 | * 78 | * @return Possibly-null {@link club.minnced.discord.webhook.receive.ReadonlyEmbed.EmbedImage} for the image 79 | */ 80 | @Nullable 81 | public EmbedImage getImage() { 82 | return image; 83 | } 84 | 85 | /** 86 | * The video of this embed. 87 | *
This is a whitelisted feature only available for services like youtube 88 | * and is only populated for link embeds. 89 | * 90 | * @return Possibly-null {@link club.minnced.discord.webhook.receive.ReadonlyEmbed.EmbedVideo} 91 | */ 92 | @Nullable 93 | public EmbedVideo getVideo() { 94 | return video; 95 | } 96 | 97 | /** 98 | * Reduces this embed to a simpler {@link club.minnced.discord.webhook.send.WebhookEmbed} 99 | * instance that can be used for sending, this is done implicitly 100 | * when trying to send an instance of a readonly-embed. 101 | * 102 | * @return The reduced embed instance 103 | */ 104 | @Override 105 | @NotNull 106 | public WebhookEmbed reduced() { 107 | return new WebhookEmbed( 108 | getTimestamp(), getColor(), getDescription(), 109 | thumbnail == null ? null : thumbnail.getUrl(), 110 | image == null ? null : image.getUrl(), 111 | getFooter(), getTitle(), getAuthor(), getFields()); 112 | } 113 | 114 | /** 115 | * JSON representation of this embed. 116 | *
Note that received embeds look different compared to sent ones. 117 | * 118 | * @return The JSON representation 119 | */ 120 | @Override 121 | public String toString() { 122 | return toJSONString(); 123 | } 124 | 125 | @Override 126 | public String toJSONString() { 127 | JSONObject base = new JSONObject(super.toJSONString()); 128 | base.put("provider", provider) 129 | .put("thumbnail", thumbnail) 130 | .put("video", video) 131 | .put("image", image); 132 | if (getTitle() != null) { 133 | base.put("title", getTitle().getText()); 134 | base.put("url", getTitle().getUrl()); 135 | } 136 | return base.toString(); 137 | } 138 | 139 | /** 140 | * POJO containing meta-data for an embed provider 141 | * 142 | * @see #getProvider() 143 | */ 144 | public static class EmbedProvider implements JSONString { 145 | private final String name, url; 146 | 147 | public EmbedProvider(@Nullable String name, @Nullable String url) { 148 | this.name = name; 149 | this.url = url; 150 | } 151 | 152 | /** 153 | * The name of the provider, or {@code null} if none is set 154 | * 155 | * @return The name 156 | */ 157 | @Nullable 158 | public String getName() { 159 | return name; 160 | } 161 | 162 | /** 163 | * The url of the provider, or {@code null} if none is set 164 | * 165 | * @return The url 166 | */ 167 | @Nullable 168 | public String getUrl() { 169 | return url; 170 | } 171 | 172 | /** 173 | * JSON representation of this provider 174 | * 175 | * @return The JSON representation 176 | */ 177 | @Override 178 | public String toString() { 179 | return toJSONString(); 180 | } 181 | 182 | @Override 183 | public String toJSONString() { 184 | return new JSONObject(this).toString(); 185 | } 186 | } 187 | 188 | /** 189 | * POJO containing meta-data about an embed video 190 | * 191 | * @see #getVideo() 192 | */ 193 | public static class EmbedVideo implements JSONString { 194 | private final String url; 195 | private final int width, height; 196 | 197 | public EmbedVideo(@NotNull String url, int width, int height) { 198 | this.url = url; 199 | this.width = width; 200 | this.height = height; 201 | } 202 | 203 | /** 204 | * The URL fot this video 205 | * 206 | * @return The URL 207 | */ 208 | @NotNull 209 | public String getUrl() { 210 | return url; 211 | } 212 | 213 | /** 214 | * The width of this video 215 | * 216 | * @return The width 217 | */ 218 | public int getWidth() { 219 | return width; 220 | } 221 | 222 | /** 223 | * The height of this video 224 | * 225 | * @return The height 226 | */ 227 | public int getHeight() { 228 | return height; 229 | } 230 | 231 | /** 232 | * JSON representation of this video 233 | * 234 | * @return The JSON representation 235 | */ 236 | @Override 237 | public String toString() { 238 | return toJSONString(); 239 | } 240 | 241 | @Override 242 | public String toJSONString() { 243 | return new JSONObject(this).toString(); 244 | } 245 | } 246 | 247 | /** 248 | * POJO containing meta-data about an embed image component 249 | * 250 | * @see #getThumbnail() 251 | * @see #getImage() 252 | */ 253 | public static class EmbedImage implements JSONString { 254 | private final String url, proxyUrl; 255 | private final int width, height; 256 | 257 | public EmbedImage( 258 | @NotNull String url, @NotNull String proxyUrl, 259 | int width, int height) { 260 | this.url = url; 261 | this.proxyUrl = proxyUrl; 262 | this.width = width; 263 | this.height = height; 264 | } 265 | 266 | /** 267 | * The URL fot this image 268 | * 269 | * @return The URL 270 | */ 271 | @NotNull 272 | public String getUrl() { 273 | return url; 274 | } 275 | 276 | /** 277 | * The proxy url for this image, this is used 278 | * to render previews in the discord client. 279 | * 280 | * @return The proxy url 281 | */ 282 | @NotNull 283 | @JSONPropertyName("proxy_url") 284 | public String getProxyUrl() { 285 | return proxyUrl; 286 | } 287 | 288 | /** 289 | * The width of this image 290 | * 291 | * @return The width 292 | */ 293 | public int getWidth() { 294 | return width; 295 | } 296 | 297 | /** 298 | * The height of this image 299 | * 300 | * @return The height 301 | */ 302 | public int getHeight() { 303 | return height; 304 | } 305 | 306 | /** 307 | * JSON representation of this provider 308 | * 309 | * @return The JSON representation 310 | */ 311 | @Override 312 | public String toString() { 313 | return toJSONString(); 314 | } 315 | 316 | @Override 317 | public String toJSONString() { 318 | return new JSONObject(this).toString(); 319 | } 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /src/main/java/club/minnced/discord/webhook/receive/ReadonlyMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 Florian Spieß 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package club.minnced.discord.webhook.receive; 18 | 19 | import club.minnced.discord.webhook.send.WebhookMessage; 20 | import org.jetbrains.annotations.NotNull; 21 | import org.json.JSONObject; 22 | import org.json.JSONString; 23 | 24 | import java.util.List; 25 | 26 | /** 27 | * Readonly message representation used for responses 28 | * of {@link club.minnced.discord.webhook.WebhookClient} send methods. 29 | * 30 | * @see #toWebhookMessage() 31 | */ 32 | public class ReadonlyMessage implements JSONString { 33 | private final long id; 34 | private final long channelId; 35 | private final boolean mentionsEveryone; 36 | private final boolean tts; 37 | private final int flags; 38 | 39 | private final ReadonlyUser author; 40 | 41 | private final String content; 42 | private final List embeds; 43 | private final List attachments; 44 | 45 | private final List mentionedUsers; 46 | private final List mentionedRoles; 47 | 48 | public ReadonlyMessage( 49 | long id, long channelId, boolean mentionsEveryone, boolean tts, int flags, 50 | @NotNull ReadonlyUser author, @NotNull String content, 51 | @NotNull List embeds, @NotNull List attachments, 52 | @NotNull List mentionedUsers, @NotNull List mentionedRoles) { 53 | this.id = id; 54 | this.channelId = channelId; 55 | this.mentionsEveryone = mentionsEveryone; 56 | this.tts = tts; 57 | this.flags = flags; 58 | this.author = author; 59 | this.content = content; 60 | this.embeds = embeds; 61 | this.attachments = attachments; 62 | this.mentionedUsers = mentionedUsers; 63 | this.mentionedRoles = mentionedRoles; 64 | } 65 | 66 | /** 67 | * The id of this message. 68 | *
If this message is the beginning of a thread, then this is the thread id. 69 | * 70 | * @return The id 71 | */ 72 | public long getId() { 73 | return id; 74 | } 75 | 76 | /** 77 | * The channel id for the channel this message was sent in 78 | * 79 | * @return The channel id 80 | */ 81 | public long getChannelId() { 82 | return channelId; 83 | } 84 | 85 | /** 86 | * Whether this message mentioned everyone/here 87 | * 88 | * @return True, if this message mentioned everyone/here 89 | */ 90 | public boolean isMentionsEveryone() { 91 | return mentionsEveryone; 92 | } 93 | 94 | /** 95 | * Whether this message used Text-to-Speech (TTS) 96 | * 97 | * @return True, if this message used TTS 98 | */ 99 | public boolean isTTS() { 100 | return tts; 101 | } 102 | 103 | /** 104 | * The flags for this message. 105 | *
You can use {@link club.minnced.discord.webhook.MessageFlags} to determine which flags are set. 106 | * 107 | * @return The flags 108 | */ 109 | public int getFlags() { 110 | return flags; 111 | } 112 | 113 | /** 114 | * The author of this message, represented by a {@link club.minnced.discord.webhook.receive.ReadonlyUser} instance. 115 | * 116 | * @return The author 117 | */ 118 | @NotNull 119 | public ReadonlyUser getAuthor() { 120 | return author; 121 | } 122 | 123 | /** 124 | * The content of this message, this is displayed above embeds and attachments. 125 | * 126 | * @return The content 127 | */ 128 | @NotNull 129 | public String getContent() { 130 | return content; 131 | } 132 | 133 | /** 134 | * The embeds in this message, a webhook can send up to 10 embeds 135 | * in one message. Additionally this contains embeds generated from links. 136 | * 137 | * @return List of embeds for this message 138 | */ 139 | @NotNull 140 | public List getEmbeds() { 141 | return embeds; 142 | } 143 | 144 | /** 145 | * The attachments of this message. This contains files 146 | * added through methods such as {@link club.minnced.discord.webhook.send.WebhookMessageBuilder#addFile(java.io.File)}. 147 | *
The attachments only contain meta-data and not the actual files. 148 | * 149 | * @return List of attachments 150 | */ 151 | @NotNull 152 | public List getAttachments() { 153 | return attachments; 154 | } 155 | 156 | /** 157 | * Users mentioned by this message. 158 | *
This will not contain all users when using an everyone/here mention, 159 | * it only contains directly mentioned users. 160 | * 161 | * @return List of mentioned users. 162 | */ 163 | @NotNull 164 | public List getMentionedUsers() { 165 | return mentionedUsers; 166 | } 167 | 168 | /** 169 | * List of mentioned role ids 170 | * 171 | * @return List of ids for directly mentioned roles 172 | */ 173 | @NotNull 174 | public List getMentionedRoles() { 175 | return mentionedRoles; 176 | } 177 | 178 | /** 179 | * Converts this message to a reduced webhook message. 180 | *
This can be used for sending. 181 | * 182 | * @return {@link club.minnced.discord.webhook.send.WebhookMessage} 183 | */ 184 | @NotNull 185 | public WebhookMessage toWebhookMessage() { 186 | return WebhookMessage.from(this); 187 | } 188 | 189 | /** 190 | * JSON representation of this provider 191 | * 192 | * @return The JSON representation 193 | */ 194 | @Override 195 | public String toString() { 196 | return toJSONString(); 197 | } 198 | 199 | @Override 200 | public String toJSONString() { 201 | JSONObject json = new JSONObject(); 202 | json.put("content", content) 203 | .put("embeds", embeds) 204 | .put("mentions", mentionedUsers) 205 | .put("mention_roles", mentionedRoles) 206 | .put("attachments", attachments) 207 | .put("author", author) 208 | .put("tts", tts) 209 | .put("id", Long.toUnsignedString(id)) 210 | .put("channel_id", Long.toUnsignedString(channelId)) 211 | .put("mention_everyone", mentionsEveryone); 212 | return json.toString(); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/main/java/club/minnced/discord/webhook/receive/ReadonlyUser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 Florian Spieß 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package club.minnced.discord.webhook.receive; 18 | 19 | import org.jetbrains.annotations.NotNull; 20 | import org.jetbrains.annotations.Nullable; 21 | import org.json.JSONObject; 22 | import org.json.JSONPropertyName; 23 | import org.json.JSONString; 24 | 25 | import java.util.Locale; 26 | 27 | /** 28 | * Readonly POJO of a discord user 29 | */ 30 | public class ReadonlyUser implements JSONString { 31 | private final long id; 32 | private final short discriminator; 33 | private final boolean bot; 34 | private final String name; 35 | private final String avatar; 36 | 37 | public ReadonlyUser(long id, short discriminator, boolean bot, @NotNull String name, @Nullable String avatar) { 38 | this.id = id; 39 | this.discriminator = discriminator; 40 | this.bot = bot; 41 | this.name = name; 42 | this.avatar = avatar; 43 | } 44 | 45 | /** 46 | * The id of this user 47 | * 48 | * @return The id 49 | */ 50 | public long getId() { 51 | return id; 52 | } 53 | 54 | /** 55 | * The 4 digit discriminator of this user 56 | *
This is show in the client after the {@code #} when viewing profiles. 57 | * 58 | * @return The discriminator 59 | */ 60 | public String getDiscriminator() { 61 | return String.format(Locale.ROOT, "%04d", discriminator); 62 | } 63 | 64 | /** 65 | * Whether this is a bot or not, webhook authors are always bots. 66 | * 67 | * @return True, if this is a bot 68 | */ 69 | public boolean isBot() { 70 | return bot; 71 | } 72 | 73 | /** 74 | * The name of this user, this is the username and not the guild-specific nickname. 75 | * 76 | * @return The name of this user 77 | */ 78 | @NotNull 79 | @JSONPropertyName("username") 80 | public String getName() { 81 | return name; 82 | } 83 | 84 | /** 85 | * The avatar id of this user, or {@code null} if no avatar is set. 86 | * 87 | * @return The avatar id 88 | */ 89 | @Nullable 90 | @JSONPropertyName("avatar_id") 91 | public String getAvatarId() { 92 | return avatar; 93 | } 94 | 95 | /** 96 | * JSON representation of this user 97 | * 98 | * @return THe JSON representation of this user 99 | */ 100 | @Override 101 | public String toString() { 102 | return toJSONString(); 103 | } 104 | 105 | @Override 106 | public String toJSONString() { 107 | return new JSONObject(this).toString(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/club/minnced/discord/webhook/send/AllowedMentions.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 Florian Spieß 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package club.minnced.discord.webhook.send; 18 | 19 | import org.jetbrains.annotations.NotNull; 20 | import org.json.JSONArray; 21 | import org.json.JSONObject; 22 | import org.json.JSONString; 23 | 24 | import java.util.Collection; 25 | import java.util.Collections; 26 | import java.util.HashSet; 27 | import java.util.Set; 28 | 29 | /** 30 | * Constructs a whitelist of allowed mentions for a message. 31 | * If any argument in this class is {@code null}, a {@link NullPointerException} will be thrown. 32 | * 33 | *

Example

34 | *
{@code
 35 |  * AllowedMentions mentions = new AllowedMentions()
 36 |  *   .withUsers("86699011792191488", "107562988810027008")
 37 |  *   .withParseEveryone(false)
 38 |  *   .withParseRoles(false);
 39 |  *
 40 |  * // This will only mention the user with the id 86699011792191488 (Minn#6688)
 41 |  * // The @everyone will be ignored since the allowed mentions disabled it.
 42 |  * client.send(
 43 |  *   new WebhookMessageBuilder()
 44 |  *     .setAllowedMentions(mentions)
 45 |  *     .setContent("Hello <@86699011792191488>! And hello @everyone else!")
 46 |  *     .build()
 47 |  * );
 48 |  * }
49 | * 50 | * @see WebhookMessageBuilder#setAllowedMentions(AllowedMentions) 51 | * @see club.minnced.discord.webhook.WebhookClientBuilder#setAllowedMentions(AllowedMentions) WebhookClientBuilder#setAllowedMentions(AllowedMentions) 52 | * 53 | * @see #all() 54 | * @see #none() 55 | */ 56 | public class AllowedMentions implements JSONString { 57 | /** 58 | * Parse all mentions. 59 | * 60 | *

Equivalent: 61 | *

{@code
 62 |      * return new AllowedMentions()
 63 |      *     .withParseEveryone(true)
 64 |      *     .withParseRoles(true)
 65 |      *     .withParseUsers(true);
 66 |      * }
67 | * 68 | * @return Every mention type will be parsed. 69 | */ 70 | public static AllowedMentions all() { 71 | return new AllowedMentions() 72 | .withParseEveryone(true) 73 | .withParseRoles(true) 74 | .withParseUsers(true); 75 | } 76 | 77 | /** 78 | * Disable all mentions. 79 | * 80 | *

Equivalent: 81 | *

{@code
 82 |      * return new AllowedMentions()
 83 |      *     .withParseEveryone(false)
 84 |      *     .withParseRoles(false)
 85 |      *     .withParseUsers(false);
 86 |      * }
87 | * 88 | * @return No mentions will be parsed. 89 | */ 90 | public static AllowedMentions none() { 91 | return new AllowedMentions() 92 | .withParseEveryone(false) 93 | .withParseRoles(false) 94 | .withParseUsers(false); 95 | } 96 | 97 | private boolean parseRoles, parseUsers, parseEveryone; 98 | private final Set users = new HashSet<>(); 99 | private final Set roles = new HashSet<>(); 100 | 101 | /** 102 | * Whitelist specified users for mention. 103 | *
This will set {@link #withParseUsers(boolean)} to false. 104 | * 105 | * @param userId 106 | * The whitelist of users to mention 107 | * 108 | * @return AllowedMentions instance with applied whitelist 109 | */ 110 | @NotNull 111 | public AllowedMentions withUsers(@NotNull String... userId) 112 | { 113 | Collections.addAll(users, userId); 114 | parseUsers = false; 115 | return this; 116 | } 117 | 118 | /** 119 | * Whitelist specified roles for mention. 120 | *
This will set {@link #withParseRoles(boolean)} to false. 121 | * 122 | * @param roleId 123 | * The whitelist of roles to mention 124 | * 125 | * @return AllowedMentions instance with applied whitelist 126 | */ 127 | @NotNull 128 | public AllowedMentions withRoles(@NotNull String... roleId) 129 | { 130 | Collections.addAll(roles, roleId); 131 | parseRoles = false; 132 | return this; 133 | } 134 | 135 | /** 136 | * Whitelist specified users for mention. 137 | *
This will set {@link #withParseUsers(boolean)} to false. 138 | * 139 | * @param userId 140 | * The whitelist of users to mention 141 | * 142 | * @return AllowedMentions instance with applied whitelist 143 | */ 144 | @NotNull 145 | public AllowedMentions withUsers(@NotNull Collection userId) 146 | { 147 | users.addAll(userId); 148 | parseUsers = false; 149 | return this; 150 | } 151 | 152 | /** 153 | * Whitelist specified roles for mention. 154 | *
This will set {@link #withParseRoles(boolean)} to false. 155 | * 156 | * @param roleId 157 | * The whitelist of roles to mention 158 | * 159 | * @return AllowedMentions instance with applied whitelist 160 | */ 161 | @NotNull 162 | public AllowedMentions withRoles(@NotNull Collection roleId) 163 | { 164 | roles.addAll(roleId); 165 | parseRoles = false; 166 | return this; 167 | } 168 | 169 | /** 170 | * Whether to parse {@code @everyone} or {@code @here} mentions. 171 | * 172 | * @param allowEveryoneMention 173 | * True, if {@code @everyone} should be parsed 174 | * 175 | * @return AllowedMentions instance with applied parsing rule 176 | */ 177 | @NotNull 178 | public AllowedMentions withParseEveryone(boolean allowEveryoneMention) 179 | { 180 | parseEveryone = allowEveryoneMention; 181 | return this; 182 | } 183 | 184 | /** 185 | * Whether to parse user mentions. 186 | *
Setting this to {@code true} will clear the whitelist provided by {@link #withUsers(String...)}. 187 | * 188 | * @param allowParseUsers 189 | * True, if all user mentions should be parsed 190 | * 191 | * @return AllowedMentions instance with applied parsing rule 192 | */ 193 | @NotNull 194 | public AllowedMentions withParseUsers(boolean allowParseUsers) 195 | { 196 | parseUsers = allowParseUsers; 197 | if (parseUsers) 198 | users.clear(); 199 | return this; 200 | } 201 | 202 | /** 203 | * Whether to parse role mentions. 204 | *
Setting this to {@code true} will clear the whitelist provided by {@link #withRoles(String...)}. 205 | * 206 | * @param allowParseRoles 207 | * True, if all role mentions should be parsed 208 | * 209 | * @return AllowedMentions instance with applied parsing rule 210 | */ 211 | @NotNull 212 | public AllowedMentions withParseRoles(boolean allowParseRoles) 213 | { 214 | parseRoles = allowParseRoles; 215 | if (parseRoles) 216 | roles.clear(); 217 | return this; 218 | } 219 | 220 | @Override 221 | public String toJSONString() { 222 | JSONObject json = new JSONObject(); 223 | json.put("parse", new JSONArray()); 224 | 225 | if (!users.isEmpty()) 226 | json.put("users", users); 227 | else if (parseUsers) 228 | json.accumulate("parse", "users"); 229 | 230 | if (!roles.isEmpty()) 231 | json.put("roles", roles); 232 | else if (parseRoles) 233 | json.accumulate("parse", "roles"); 234 | 235 | if (parseEveryone) 236 | json.accumulate("parse", "everyone"); 237 | return json.toString(); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/main/java/club/minnced/discord/webhook/send/MessageAttachment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 Florian Spieß 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package club.minnced.discord.webhook.send; 18 | 19 | import club.minnced.discord.webhook.IOUtil; 20 | import org.jetbrains.annotations.NotNull; 21 | 22 | import java.io.File; 23 | import java.io.FileInputStream; 24 | import java.io.IOException; 25 | import java.io.InputStream; 26 | 27 | /** 28 | * Internal representation of attachments for outgoing messages 29 | */ 30 | public class MessageAttachment { 31 | private final String name; 32 | private final byte[] data; 33 | 34 | MessageAttachment(@NotNull String name, @NotNull byte[] data) { 35 | this.name = name; 36 | this.data = data; 37 | } 38 | 39 | MessageAttachment(@NotNull String name, @NotNull InputStream stream) throws IOException { 40 | this.name = name; 41 | try (InputStream data = stream) { 42 | this.data = IOUtil.readAllBytes(data); 43 | } 44 | } 45 | 46 | MessageAttachment(@NotNull String name, @NotNull File file) throws IOException { 47 | this(name, new FileInputStream(file)); 48 | } 49 | 50 | @NotNull 51 | public String getName() { 52 | return name; 53 | } 54 | 55 | @NotNull 56 | public byte[] getData() { 57 | return data; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/club/minnced/discord/webhook/send/WebhookEmbed.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 Florian Spieß 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package club.minnced.discord.webhook.send; 18 | 19 | import org.jetbrains.annotations.NotNull; 20 | import org.jetbrains.annotations.Nullable; 21 | import org.json.JSONObject; 22 | import org.json.JSONPropertyIgnore; 23 | import org.json.JSONPropertyName; 24 | import org.json.JSONString; 25 | 26 | import java.time.OffsetDateTime; 27 | import java.util.Collections; 28 | import java.util.List; 29 | import java.util.Objects; 30 | 31 | /** 32 | * Reduced version of an {@link club.minnced.discord.webhook.receive.ReadonlyEmbed} 33 | * used for sending. A webhook can send up to {@value WebhookMessage#MAX_EMBEDS} embeds 34 | * in a single message. 35 | * 36 | * @see club.minnced.discord.webhook.send.WebhookEmbedBuilder 37 | */ 38 | public class WebhookEmbed implements JSONString { 39 | /** 40 | * Max amount of fields an embed can hold (25) 41 | */ 42 | public static final int MAX_FIELDS = 25; 43 | 44 | private final OffsetDateTime timestamp; 45 | private final Integer color; 46 | 47 | private final String description; 48 | private final String thumbnailUrl; 49 | private final String imageUrl; 50 | 51 | private final EmbedFooter footer; 52 | private final EmbedTitle title; 53 | private final EmbedAuthor author; 54 | private final List fields; 55 | 56 | public WebhookEmbed( 57 | @Nullable OffsetDateTime timestamp, @Nullable Integer color, 58 | @Nullable String description, @Nullable String thumbnailUrl, @Nullable String imageUrl, 59 | @Nullable EmbedFooter footer, @Nullable EmbedTitle title, @Nullable EmbedAuthor author, 60 | @NotNull List fields) { 61 | this.timestamp = timestamp; 62 | this.color = color; 63 | this.description = description; 64 | this.thumbnailUrl = thumbnailUrl; 65 | this.imageUrl = imageUrl; 66 | this.footer = footer; 67 | this.title = title; 68 | this.author = author; 69 | this.fields = Collections.unmodifiableList(fields); 70 | } 71 | 72 | /** 73 | * The thumbnail url 74 | * 75 | * @return Possibly-null url 76 | */ 77 | @Nullable 78 | @JSONPropertyIgnore 79 | public String getThumbnailUrl() { 80 | return thumbnailUrl; 81 | } 82 | 83 | /** 84 | * The image url 85 | * 86 | * @return Possibly-null url 87 | */ 88 | @Nullable 89 | @JSONPropertyIgnore 90 | public String getImageUrl() { 91 | return imageUrl; 92 | } 93 | 94 | /** 95 | * The timestamp for the embed. 96 | *
The discord client displays this 97 | * in the correct timezone and locale of the viewing users. 98 | * 99 | * @return Possibly-null {@link java.time.OffsetDateTime} of the timestamp 100 | */ 101 | @Nullable 102 | public OffsetDateTime getTimestamp() { 103 | return timestamp; 104 | } 105 | 106 | /** 107 | * The title of the embed, this is displayed 108 | * above the description and below the author. 109 | * 110 | * @return Possibly-null {@link club.minnced.discord.webhook.send.WebhookEmbed.EmbedTitle} 111 | */ 112 | @Nullable 113 | @JSONPropertyIgnore 114 | public EmbedTitle getTitle() { 115 | return title; 116 | } 117 | 118 | /** 119 | * The rgb color of this embed. 120 | *
This is the colored line on the left-hand side of the embed. 121 | * 122 | * @return Possibly-null boxed integer of the color 123 | */ 124 | @Nullable 125 | public Integer getColor() { 126 | return color; 127 | } 128 | 129 | /** 130 | * The description of this embed 131 | * 132 | * @return Possibly-null description 133 | */ 134 | @Nullable 135 | public String getDescription() { 136 | return description; 137 | } 138 | 139 | /** 140 | * The footer of the embed. 141 | *
This is displayed at the very bottom of the embed, left to the timestamp. 142 | * 143 | * @return Possibly-null {@link club.minnced.discord.webhook.send.WebhookEmbed.EmbedFooter} 144 | */ 145 | @Nullable 146 | public EmbedFooter getFooter() { 147 | return footer; 148 | } 149 | 150 | /** 151 | * The embed author. 152 | *
This is displayed at the very top of the embed, 153 | * even above the {@link #getTitle()}. 154 | * 155 | * @return Possibly-null {@link club.minnced.discord.webhook.send.WebhookEmbed.EmbedAuthor} 156 | */ 157 | @Nullable 158 | public EmbedAuthor getAuthor() { 159 | return author; 160 | } 161 | 162 | /** 163 | * List of fields for this embed. 164 | *
And embed can have up to {@value MAX_FIELDS}. 165 | * 166 | * @return List of fields for this embed 167 | */ 168 | @NotNull 169 | public List getFields() { 170 | return fields; 171 | } 172 | 173 | /** 174 | * Returns this embed instance, as its already reduced. 175 | * 176 | * @return The current instance 177 | */ 178 | @NotNull 179 | public WebhookEmbed reduced() { 180 | return this; 181 | } 182 | 183 | /** 184 | * JSON representation of this embed 185 | * 186 | * @return The JSON representation 187 | */ 188 | @Override 189 | public String toString() { 190 | return toJSONString(); 191 | } 192 | 193 | @Override 194 | public String toJSONString() { 195 | JSONObject json = new JSONObject(); 196 | if (description != null) 197 | json.put("description", description); 198 | if (timestamp != null) 199 | json.put("timestamp", timestamp); 200 | if (color != null) 201 | json.put("color", color & 0xFFFFFF); 202 | if (author != null) 203 | json.put("author", author); 204 | if (footer != null) 205 | json.put("footer", footer); 206 | if (thumbnailUrl != null) 207 | json.put("thumbnail", 208 | new JSONObject() 209 | .put("url", thumbnailUrl)); 210 | if (imageUrl != null) 211 | json.put("image", 212 | new JSONObject() 213 | .put("url", imageUrl)); 214 | if (!fields.isEmpty()) 215 | json.put("fields", fields); 216 | if (title != null) { 217 | if (title.getUrl() != null) 218 | json.put("url", title.url); 219 | json.put("title", title.text); 220 | } 221 | return json.toString(); 222 | } 223 | 224 | /** 225 | * POJO for an embed field. 226 | *
An embed can have up to {@value MAX_FIELDS} fields. 227 | * A row of fields can be up 3 wide, or 2 when a thumbnail is configured. 228 | * To be displayed in the same row as other fields, the field has to be set to {@link #isInline() inline}. 229 | */ 230 | public static class EmbedField implements JSONString { 231 | private final boolean inline; 232 | private final String name, value; 233 | 234 | /** 235 | * Creates a new embed field 236 | * 237 | * @param inline 238 | * Whether or not this should share a row with other fields 239 | * @param name 240 | * The name of the field 241 | * @param value 242 | * The value of the field 243 | * 244 | * @see club.minnced.discord.webhook.send.WebhookEmbedBuilder#addField(club.minnced.discord.webhook.send.WebhookEmbed.EmbedField) 245 | */ 246 | public EmbedField(boolean inline, @NotNull String name, @NotNull String value) { 247 | this.inline = inline; 248 | this.name = Objects.requireNonNull(name); 249 | this.value = Objects.requireNonNull(value); 250 | } 251 | 252 | /** 253 | * Whether this field should share a row with other fields 254 | * 255 | * @return True, if this should be in the same row as other fields 256 | */ 257 | public boolean isInline() { 258 | return inline; 259 | } 260 | 261 | /** 262 | * The name of this field. 263 | *
This is displayed above the value in a bold font. 264 | * 265 | * @return The name 266 | */ 267 | @NotNull 268 | public String getName() { 269 | return name; 270 | } 271 | 272 | /** 273 | * The value of this field. 274 | *
This is displayed below the name in a regular font. 275 | * 276 | * @return The value 277 | */ 278 | @NotNull 279 | public String getValue() { 280 | return value; 281 | } 282 | 283 | /** 284 | * JSON representation of this field 285 | * 286 | * @return The JSON representation 287 | */ 288 | @Override 289 | public String toString() { 290 | return toJSONString(); 291 | } 292 | 293 | @Override 294 | public String toJSONString() { 295 | return new JSONObject(this).toString(); 296 | } 297 | } 298 | 299 | /** 300 | * POJO for an embed author. 301 | *
This can contain an icon (avatar), a name, and a url. 302 | * Often useful for posts from other platforms such as twitter/github. 303 | */ 304 | public static class EmbedAuthor implements JSONString { 305 | private final String name, iconUrl, url; 306 | 307 | /** 308 | * Creates a new embed author 309 | * 310 | * @param name 311 | * The name of the author 312 | * @param iconUrl 313 | * The (nullable) icon url of the author 314 | * @param url 315 | * The (nullable) hyperlink of the author 316 | * 317 | * @see club.minnced.discord.webhook.send.WebhookEmbedBuilder#setAuthor(club.minnced.discord.webhook.send.WebhookEmbed.EmbedAuthor) 318 | */ 319 | public EmbedAuthor(@NotNull String name, @Nullable String iconUrl, @Nullable String url) { 320 | this.name = Objects.requireNonNull(name); 321 | this.iconUrl = iconUrl; 322 | this.url = url; 323 | } 324 | 325 | /** 326 | * The name of the author, this is the only visible text of this component. 327 | * 328 | * @return The name 329 | */ 330 | @NotNull 331 | public String getName() { 332 | return name; 333 | } 334 | 335 | /** 336 | * The iconUrl of this author. 337 | *
This is displayed left to the name, similar to messages in discord. 338 | * 339 | * @return Possibly-null iconUrl url 340 | */ 341 | @Nullable 342 | @JSONPropertyName("icon_url") 343 | public String getIconUrl() { 344 | return iconUrl; 345 | } 346 | 347 | /** 348 | * The url of this author. 349 | *
This can be used to highlight the name as a hyperlink 350 | * to the platform's profile service. 351 | * 352 | * @return Possibly-null url 353 | */ 354 | @Nullable 355 | public String getUrl() { 356 | return url; 357 | } 358 | 359 | /** 360 | * JSON representation of this author 361 | * 362 | * @return The JSON representation 363 | */ 364 | @Override 365 | public String toString() { 366 | return toJSONString(); 367 | } 368 | 369 | @Override 370 | public String toJSONString() { 371 | return new JSONObject(this).toString(); 372 | } 373 | } 374 | 375 | /** 376 | * POJO for an embed footer. 377 | *
Useful to display meta-data about context such as 378 | * for a github comment a repository name/icon. 379 | */ 380 | public static class EmbedFooter implements JSONString { 381 | private final String text, icon; 382 | 383 | /** 384 | * Creates a new embed footer 385 | * 386 | * @param text 387 | * The visible text of the footer 388 | * @param icon 389 | * The (nullable) icon url of the footer 390 | * 391 | * @see club.minnced.discord.webhook.send.WebhookEmbedBuilder#setFooter(club.minnced.discord.webhook.send.WebhookEmbed.EmbedFooter) 392 | */ 393 | public EmbedFooter(@NotNull String text, @Nullable String icon) { 394 | this.text = Objects.requireNonNull(text); 395 | this.icon = icon; 396 | } 397 | 398 | /** 399 | * The visible text of the footer. 400 | * 401 | * @return The text 402 | */ 403 | @NotNull 404 | public String getText() { 405 | return text; 406 | } 407 | 408 | /** 409 | * The url for the icon of this footer 410 | * 411 | * @return Possibly-null icon url 412 | */ 413 | @Nullable 414 | @JSONPropertyName("icon_url") 415 | public String getIconUrl() { 416 | return icon; 417 | } 418 | 419 | /** 420 | * JSON representation of this footer 421 | * 422 | * @return The JSON representation 423 | */ 424 | @Override 425 | public String toString() { 426 | return toJSONString(); 427 | } 428 | 429 | @Override 430 | public String toJSONString() { 431 | return new JSONObject(this).toString(); 432 | } 433 | } 434 | 435 | /** 436 | * POJO for an embed title. 437 | *
This is displayed above description and below the embed author. 438 | */ 439 | public static class EmbedTitle { 440 | private final String text, url; 441 | 442 | /** 443 | * Creates a new embed title 444 | * 445 | * @param text 446 | * The visible text 447 | * @param url 448 | * The (nullable) hyperlink 449 | * 450 | * @see club.minnced.discord.webhook.send.WebhookEmbedBuilder#setTitle(club.minnced.discord.webhook.send.WebhookEmbed.EmbedTitle) 451 | */ 452 | public EmbedTitle(@NotNull String text, @Nullable String url) { 453 | this.text = Objects.requireNonNull(text); 454 | this.url = url; 455 | } 456 | 457 | /** 458 | * The visible text of this title 459 | * 460 | * @return The visible text 461 | */ 462 | @NotNull 463 | public String getText() { 464 | return text; 465 | } 466 | 467 | /** 468 | * The hyperlink for this title. 469 | * 470 | * @return Possibly-null url 471 | */ 472 | @Nullable 473 | public String getUrl() { 474 | return url; 475 | } 476 | 477 | /** 478 | * JSON representation of this title 479 | * 480 | * @return The JSON representation 481 | */ 482 | @Override 483 | public String toString() { 484 | return new JSONObject(this).toString(); 485 | } 486 | } 487 | } 488 | -------------------------------------------------------------------------------- /src/main/java/club/minnced/discord/webhook/util/ThreadPools.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 Florian Spieß 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package club.minnced.discord.webhook.util; 18 | 19 | import java.util.concurrent.Executors; 20 | import java.util.concurrent.ScheduledExecutorService; 21 | import java.util.concurrent.ThreadFactory; 22 | 23 | public class ThreadPools { // internal utils 24 | public static ScheduledExecutorService getDefaultPool(long id, ThreadFactory factory, boolean isDaemon) { 25 | return Executors.newSingleThreadScheduledExecutor(factory == null ? new DefaultWebhookThreadFactory(id, isDaemon) : factory); 26 | } 27 | 28 | public static final class DefaultWebhookThreadFactory implements ThreadFactory { 29 | private final long id; 30 | private final boolean isDaemon; 31 | 32 | public DefaultWebhookThreadFactory(long id, boolean isDaemon) { 33 | this.id = id; 34 | this.isDaemon = isDaemon; 35 | } 36 | 37 | @Override 38 | public Thread newThread(Runnable r) { 39 | final Thread thread = new Thread(r, "Webhook-RateLimit Thread WebhookID: " + id); 40 | thread.setDaemon(isDaemon); 41 | return thread; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/club/minnced/discord/webhook/util/WebhookErrorHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 Florian Spieß 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package club.minnced.discord.webhook.util; 18 | 19 | import club.minnced.discord.webhook.WebhookClient; 20 | import org.jetbrains.annotations.NotNull; 21 | import org.jetbrains.annotations.Nullable; 22 | import org.slf4j.LoggerFactory; 23 | 24 | /** 25 | * Used to dynamically handle errors for webhook requests in {@link WebhookClient} 26 | *
If not explicitly configured, this uses {@link #DEFAULT}. 27 | * 28 | * @see WebhookClient#setDefaultErrorHandler(WebhookErrorHandler) 29 | * @see WebhookClient#setErrorHandler(WebhookErrorHandler) 30 | */ 31 | @FunctionalInterface 32 | public interface WebhookErrorHandler { 33 | /** 34 | * The default error handling which simply logs the exception using SLF4J 35 | */ 36 | WebhookErrorHandler DEFAULT = (client, message, throwable) -> LoggerFactory.getLogger(WebhookClient.class).error(message, throwable); 37 | 38 | /** 39 | * Implements error handling, must not throw anything! 40 | * 41 | * @param client 42 | * The {@link WebhookClient} instance which encountered the exception 43 | * @param message 44 | * The context message used for logging 45 | * @param throwable 46 | * The encountered exception, or null if the error is only a context message 47 | */ 48 | void handle(@NotNull WebhookClient client, @NotNull String message, @Nullable Throwable throwable); 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/root/IOTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 Florian Spieß 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package root; 18 | 19 | import club.minnced.discord.webhook.IOUtil; 20 | import org.junit.*; 21 | 22 | import java.io.File; 23 | import java.io.FileInputStream; 24 | import java.io.IOException; 25 | import java.nio.file.Files; 26 | import java.nio.file.StandardOpenOption; 27 | import java.util.concurrent.ThreadLocalRandom; 28 | 29 | public class IOTest { 30 | public static String CONTENT; 31 | private File tempFile; 32 | 33 | @BeforeClass 34 | public static void randomContent() { 35 | ThreadLocalRandom random = ThreadLocalRandom.current(); 36 | int size = random.nextInt(4098); 37 | StringBuilder builder = new StringBuilder(size); 38 | for (int i = 0; i < size; i++) { 39 | builder.append(random.nextInt()); 40 | } 41 | CONTENT = builder.toString(); 42 | } 43 | 44 | @Before 45 | public void setup() throws IOException { 46 | tempFile = File.createTempFile("test", "Data"); 47 | Files.write(tempFile.toPath(), CONTENT.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); 48 | } 49 | 50 | @After 51 | public void cleanup() { 52 | tempFile.delete(); 53 | } 54 | 55 | @Test 56 | public void readAll() throws IOException { 57 | String content = new String(IOUtil.readAllBytes(new FileInputStream(tempFile))); 58 | Assert.assertEquals(CONTENT, content); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/root/IOTestUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 Florian Spieß 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package root; 18 | 19 | import okhttp3.*; 20 | import okio.Buffer; 21 | import okio.Timeout; 22 | 23 | import java.io.ByteArrayOutputStream; 24 | import java.io.IOException; 25 | import java.nio.charset.StandardCharsets; 26 | import java.util.Collections; 27 | import java.util.HashMap; 28 | import java.util.Map; 29 | import java.util.regex.Matcher; 30 | import java.util.regex.Pattern; 31 | import java.util.zip.GZIPOutputStream; 32 | 33 | @SuppressWarnings("deprecation") 34 | public class IOTestUtil { 35 | 36 | public static boolean isMultiPart(RequestBody body) { 37 | return getBoundary(body) != null; 38 | } 39 | 40 | public static String readRequestBody(RequestBody body) throws IOException { 41 | Buffer sink = new Buffer(); 42 | body.writeTo(sink); 43 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 44 | sink.copyTo(bos); 45 | return new String(bos.toByteArray(), 0, bos.size(), StandardCharsets.UTF_8); 46 | } 47 | 48 | private static final Pattern CONTENT_DISPOSITION_PATTERN = 49 | Pattern.compile("^Content-Disposition: form-data; name=\"([^\"]+)\"(?:; filename=\"([^\"]+)\")?$"); 50 | private static final String validFileContent = "Content-Type: application/octet-stream; charset=utf-8"; 51 | 52 | public static Map parseMultipart(RequestBody body) throws IOException { 53 | //primitive multipart parser 54 | String boundary = getBoundary(body); 55 | if(boundary == null) 56 | throw new IllegalArgumentException("RequestBody is not of type Multipart"); 57 | String[] lines = readRequestBody(body).split("\r\n"); 58 | if(lines.length == 0) 59 | return Collections.emptyMap(); 60 | if(!lines[0].equals(boundary) ||!lines[lines.length-1].equals(boundary+"--")) 61 | throw new IllegalArgumentException("Boundary given is invalid"); 62 | 63 | Map parts = new HashMap<>(); 64 | String name = null, filename = null; 65 | StringBuilder sb = new StringBuilder(); 66 | int lengthToCheck = -1; 67 | boolean inBody = false; 68 | 69 | for(int i = 1; i < lines.length; i++) { 70 | String line = lines[i]; 71 | if(line.equals(boundary) || i == lines.length - 1) { 72 | if(!inBody) 73 | throw new IllegalStateException("Multipart body was never reached"); 74 | if(lengthToCheck != -1) { 75 | if(sb.length() != lengthToCheck) 76 | throw new IllegalStateException("Length check for multipart content failed"); 77 | lengthToCheck = -1; 78 | } 79 | if(filename != null) { 80 | parts.put(name, new MultiPartFile(filename, sb.toString().getBytes(StandardCharsets.UTF_8))); 81 | filename = null; 82 | } else { 83 | parts.put(name, sb.toString()); 84 | } 85 | name = null; 86 | inBody = false; 87 | sb.setLength(0); 88 | } else if(inBody) { 89 | if(sb.length() > 0) 90 | sb.append("\r\n"); 91 | sb.append(line); 92 | } else { 93 | if(line.isEmpty()) { 94 | if(name == null) 95 | throw new IllegalStateException("multipart name was never given"); 96 | inBody = true; 97 | } else if(line.startsWith("Content-Disposition:")) { 98 | Matcher matcher = CONTENT_DISPOSITION_PATTERN.matcher(line); 99 | if(!matcher.matches()) 100 | throw new IllegalStateException("Content-Disposition does not match pattern"); 101 | name = matcher.group(1); 102 | if(matcher.group(2) != null) 103 | filename = matcher.group(2); 104 | } else if(line.startsWith("Content-Type:") && !line.equals(validFileContent)) { 105 | throw new IllegalStateException("Invalid Content-type provided"); 106 | } else if(line.startsWith("Content-Length:")) { 107 | lengthToCheck = Integer.parseInt(line.substring(15).trim()); 108 | } 109 | } 110 | } 111 | return parts; 112 | } 113 | 114 | public static Call forgeCall(Request req, String json, boolean useGzip) { 115 | return new FakeCall(req, json, useGzip); 116 | } 117 | 118 | private static Pattern MULTIPART_TYPE_PATTERN = Pattern.compile("^multipart/form-data; boundary=(.+)$"); 119 | 120 | private static String getBoundary(RequestBody body) { 121 | MediaType mediaType = body.contentType(); 122 | if(mediaType == null) 123 | return null; 124 | Matcher matcher = MULTIPART_TYPE_PATTERN.matcher(mediaType.toString()); 125 | if(!matcher.matches()) 126 | return null; 127 | return "--" + matcher.group(1); 128 | } 129 | 130 | public static class MultiPartFile { 131 | public final String filename; 132 | public final byte[] content; 133 | 134 | private MultiPartFile(String filename, byte[] content) { 135 | this.filename = filename; 136 | this.content = content; 137 | } 138 | } 139 | 140 | private static class FakeCall implements Call { 141 | private final Request req; 142 | private final String jsonResponse; 143 | private final boolean isGzip; 144 | 145 | public FakeCall(Request req, String jsonResponse, boolean isGzip) { 146 | this.req = req; 147 | this.jsonResponse = jsonResponse; 148 | this.isGzip = isGzip; 149 | } 150 | 151 | @Override 152 | public Timeout timeout() { 153 | return Timeout.NONE; 154 | } 155 | 156 | @Override 157 | public Request request() { 158 | return null; 159 | } 160 | 161 | @Override 162 | public Response execute() throws IOException { 163 | Response.Builder builder = new Response.Builder() 164 | .request(req) 165 | .protocol(Protocol.HTTP_1_1) 166 | .code(200) 167 | .message("OK") 168 | .header("X-RateLimit-Remaining", "199") 169 | .header("X-RateLimit-Limit", "200"); 170 | 171 | if(isGzip) { 172 | builder.header("content-encoding", "gzip"); 173 | ByteArrayOutputStream bout = new ByteArrayOutputStream(); 174 | GZIPOutputStream gzipout = new GZIPOutputStream(bout); 175 | gzipout.write(jsonResponse.getBytes(StandardCharsets.UTF_8)); 176 | gzipout.close(); 177 | builder.body(ResponseBody.create(club.minnced.discord.webhook.IOUtil.JSON, bout.toByteArray())); 178 | } else { 179 | builder.body(ResponseBody.create(club.minnced.discord.webhook.IOUtil.JSON, jsonResponse)); 180 | } 181 | 182 | return builder.build(); 183 | } 184 | 185 | @Override 186 | public void enqueue(Callback responseCallback) { 187 | 188 | } 189 | 190 | @Override 191 | public void cancel() { 192 | 193 | } 194 | 195 | @Override 196 | public boolean isExecuted() { 197 | return false; 198 | } 199 | 200 | @Override 201 | public boolean isCanceled() { 202 | return false; 203 | } 204 | 205 | @Override 206 | public Call clone() { 207 | return null; 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/test/java/root/receive/ReceiveEmbedTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 Florian Spieß 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package root.receive; 18 | 19 | import club.minnced.discord.webhook.receive.EntityFactory; 20 | import club.minnced.discord.webhook.receive.ReadonlyEmbed; 21 | import org.json.JSONArray; 22 | import org.json.JSONObject; 23 | import org.junit.Assert; 24 | import org.junit.Test; 25 | 26 | import java.time.OffsetDateTime; 27 | import java.time.ZoneId; 28 | import java.util.Iterator; 29 | 30 | public class ReceiveEmbedTest { 31 | public static final JSONObject MOCK_IMAGE_JSON = 32 | new JSONObject() 33 | .put("url", "https://avatars1.githubusercontent.com/u/18090140?s=460&v=4") 34 | .put("proxy_url", "https://images-ext-2.discordapp.net/external/szbDUqX0oTT0E460Vn14767s7VMpWig23v9vPJczatE/%3Fs%3D460%26v%3D4/https/avatars1.githubusercontent.com/u/18090140?width=135&height=135") 35 | .put("width", 100).put("height", 200); 36 | 37 | public static final JSONObject MOCK_PROVIDER_JSON = 38 | new JSONObject() 39 | .put("name", "github") 40 | .put("url", "https://github.com"); 41 | 42 | public static final JSONObject MOCK_VIDEO_JSON = 43 | new JSONObject() 44 | .put("url", "https://youtube.com") 45 | .put("width", 100) 46 | .put("height", 200); 47 | 48 | public static final JSONObject MOCK_FOOTER_JSON = 49 | new JSONObject() 50 | .put("text", "this is a footer") 51 | .put("icon_url", "https://avatars1.githubusercontent.com/u/18090140?s=460&v=4"); 52 | 53 | public static final JSONObject MOCK_AUTHOR_JSON = 54 | new JSONObject() 55 | .put("name", "MinnDevelopment") 56 | .put("url", "https://github.com/MinnDevelopment") 57 | .put("icon_url", "https://avatars1.githubusercontent.com/u/18090140?s=460&v=4"); 58 | 59 | public static final JSONObject MOCK_FIELD_JSON = 60 | new JSONObject() 61 | .put("name", "this is a field") 62 | .put("value", "this is a value of a field") 63 | .put("inline", false); 64 | 65 | public static final JSONObject MOCK_EMBED_JSON = 66 | new JSONObject() 67 | .put("title", "this is a title") 68 | .put("description", "this is a description") 69 | .put("url", "https://github.com/MinnDevelopment/discord-webhooks") 70 | .put("color", 0xff00ff) 71 | .put("timestamp", OffsetDateTime.now(ZoneId.of("UTC")).toString()) 72 | .put("image", MOCK_IMAGE_JSON) 73 | .put("thumbnail", MOCK_IMAGE_JSON) 74 | .put("provider", MOCK_PROVIDER_JSON) 75 | .put("footer", MOCK_FOOTER_JSON) 76 | .put("video", MOCK_VIDEO_JSON) 77 | .put("author", MOCK_AUTHOR_JSON) 78 | .put("fields", new JSONArray().put(MOCK_FIELD_JSON)); 79 | 80 | 81 | @Test 82 | public void parseEmbed() { 83 | ReadonlyEmbed embed = EntityFactory.makeEmbed(MOCK_EMBED_JSON); 84 | Assert.assertEquals(MOCK_EMBED_JSON.get("description"), embed.getDescription()); 85 | Assert.assertEquals(MOCK_EMBED_JSON.get("title"), embed.getTitle().getText()); 86 | Assert.assertEquals(MOCK_EMBED_JSON.get("url"), embed.getTitle().getUrl()); 87 | Assert.assertEquals(MOCK_EMBED_JSON.getInt("color"), (int) embed.getColor()); 88 | Assert.assertEquals(MOCK_IMAGE_JSON.toString(), new JSONObject(embed.getImage()).toString()); 89 | Assert.assertEquals(MOCK_IMAGE_JSON.toString(), new JSONObject(embed.getThumbnail()).toString()); 90 | Assert.assertEquals(MOCK_PROVIDER_JSON.toString(), new JSONObject(embed.getProvider()).toString()); 91 | Assert.assertEquals(MOCK_FOOTER_JSON.toString(), new JSONObject(embed.getFooter()).toString()); 92 | Assert.assertEquals(MOCK_AUTHOR_JSON.toString(), new JSONObject(embed.getAuthor()).toString()); 93 | Assert.assertEquals(MOCK_VIDEO_JSON.toString(), new JSONObject(embed.getVideo()).toString()); 94 | Assert.assertEquals(new JSONArray().put(MOCK_FIELD_JSON).toString(), new JSONArray(embed.getFields()).toString()); 95 | JSONObject parsedJson = new JSONObject(embed.toJSONString()); 96 | for (Iterator it = parsedJson.keys(); it.hasNext(); ) { 97 | String key = it.next(); 98 | String value1 = String.valueOf(MOCK_EMBED_JSON.opt(key)); 99 | String value2 = String.valueOf(parsedJson.opt(key)); 100 | Assert.assertEquals("Not matching values for key " + key, value1, value2); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/test/java/root/receive/ReceiveMessageTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 Florian Spieß 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package root.receive; 18 | 19 | import club.minnced.discord.webhook.receive.EntityFactory; 20 | import club.minnced.discord.webhook.receive.ReadonlyMessage; 21 | import club.minnced.discord.webhook.receive.ReadonlyUser; 22 | import org.json.JSONArray; 23 | import org.json.JSONObject; 24 | import org.junit.Test; 25 | 26 | import java.util.Arrays; 27 | 28 | import static org.junit.Assert.*; 29 | 30 | public class ReceiveMessageTest { 31 | public static final String WEBHOOK_ID = "350595946677594378"; 32 | 33 | public static final JSONObject MOCK_MESSAGE_USER_JSON = 34 | new JSONObject() 35 | .put("id", WEBHOOK_ID) 36 | .put("username", "Captain Hook") 37 | .put("discriminator", "0000") 38 | .put("bot", true) 39 | .put("avatar", JSONObject.NULL); 40 | 41 | public static JSONObject getMockMessageJson() { 42 | return new JSONObject() 43 | .put("id", "2") 44 | .put("content", "Dummy content") 45 | .put("channel_id", "1234") 46 | .put("author", MOCK_MESSAGE_USER_JSON) 47 | .put("tts", true) 48 | .put("attachments", new JSONArray()) 49 | .put("embeds", new JSONArray()) 50 | .put("mention_everyone", true) 51 | .put("mentions", new JSONArray()) 52 | .put("mention_roles", new JSONArray()); 53 | /*not parsed: 54 | .put("pinned", true) 55 | .put("webhook_id", WEBHOOK_ID) 56 | .put("timestamp", OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)) 57 | .put("edited_timestamp", OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)) 58 | .put("nonce", JSONObject.NULL) 59 | .put("type", 0); 60 | */ 61 | } 62 | 63 | @Test 64 | public void parseMessage() { 65 | ReadonlyMessage message = EntityFactory.makeMessage(getMockMessageJson()); 66 | assertEquals("Message id mismatches", 2L, message.getId()); 67 | assertEquals("Message content mismatches", "Dummy content", message.getContent()); 68 | assertEquals("Channel id mismatches", 1234L, message.getChannelId()); 69 | assertTrue("TTS mismatches", message.isTTS()); 70 | assertTrue("Attachments are not empty", message.getAttachments().isEmpty()); 71 | assertTrue("Embeds are not empty", message.getEmbeds().isEmpty()); 72 | assertTrue("Does not mention everyone", message.isMentionsEveryone()); 73 | assertTrue("User mentions not empty", message.getMentionedUsers().isEmpty()); 74 | assertTrue("Role mentions not empty", message.getMentionedRoles().isEmpty()); 75 | } 76 | 77 | @Test 78 | public void parseEmbed() { 79 | JSONObject json = getMockMessageJson(); 80 | json.getJSONArray("embeds").put(ReceiveEmbedTest.MOCK_EMBED_JSON); 81 | ReadonlyMessage message = EntityFactory.makeMessage(json); 82 | assertEquals("Embeds are empty", 1, message.getEmbeds().size()); 83 | assertEquals("Embed json incorrect/incomplete", 84 | ReceiveEmbedTest.MOCK_EMBED_JSON.toMap(), 85 | new JSONObject(message.getEmbeds().get(0).toJSONString()).toMap() 86 | ); 87 | } 88 | 89 | @Test 90 | public void parseMentions() { 91 | JSONObject json = getMockMessageJson(); 92 | json.getJSONArray("mentions").put( 93 | new JSONObject() 94 | .put("username", "Lucky Winner") 95 | .put("discriminator", "1234") 96 | .put("id", "2222") 97 | .put("avatar", "abc") 98 | ); 99 | json.getJSONArray("mention_roles").put("654").put("321"); 100 | ReadonlyMessage message = EntityFactory.makeMessage(json); 101 | assertEquals("User mentions are empty", 1, message.getMentionedUsers().size()); 102 | assertEquals("Role mentions are empty", 2, message.getMentionedRoles().size()); 103 | 104 | assertEquals("Role ids incorrect", Arrays.asList(654L, 321L), message.getMentionedRoles()); 105 | 106 | ReadonlyUser user = message.getMentionedUsers().get(0); 107 | assertEquals("Username mismatches", "Lucky Winner", user.getName()); 108 | assertEquals("Discriminator mismatches", "1234", user.getDiscriminator()); 109 | assertEquals("User Id mismatches", 2222L, user.getId()); 110 | assertEquals("Avatar mismatches", "abc", user.getAvatarId()); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/test/java/root/receive/ReceiveMock.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 Florian Spieß 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package root.receive; 18 | 19 | import club.minnced.discord.webhook.WebhookClient; 20 | import club.minnced.discord.webhook.WebhookClientBuilder; 21 | import club.minnced.discord.webhook.receive.EntityFactory; 22 | import club.minnced.discord.webhook.receive.ReadonlyMessage; 23 | import club.minnced.discord.webhook.receive.ReadonlyUser; 24 | import okhttp3.OkHttpClient; 25 | import org.json.JSONObject; 26 | import org.junit.After; 27 | import org.junit.Before; 28 | import org.junit.Test; 29 | import org.junit.runner.RunWith; 30 | import org.mockito.ArgumentCaptor; 31 | import org.mockito.Captor; 32 | import org.mockito.Mock; 33 | import org.mockito.MockitoAnnotations; 34 | import org.powermock.api.mockito.PowerMockito; 35 | import org.powermock.core.classloader.annotations.PrepareForTest; 36 | import org.powermock.modules.junit4.PowerMockRunner; 37 | import root.IOTestUtil; 38 | 39 | import java.util.Collections; 40 | import java.util.concurrent.ExecutionException; 41 | import java.util.concurrent.TimeUnit; 42 | import java.util.concurrent.TimeoutException; 43 | 44 | import static org.junit.Assert.*; 45 | import static org.mockito.ArgumentMatchers.any; 46 | import static org.mockito.Mockito.only; 47 | import static org.mockito.Mockito.when; 48 | 49 | @RunWith(PowerMockRunner.class) 50 | @PrepareForTest(EntityFactory.class) 51 | public class ReceiveMock { 52 | @Captor 53 | private ArgumentCaptor jsonCaptor; 54 | 55 | @Mock 56 | private OkHttpClient httpClient; 57 | 58 | private WebhookClient client; 59 | 60 | private AutoCloseable mocks; 61 | 62 | @Before 63 | public void init() { 64 | mocks = MockitoAnnotations.openMocks(this); 65 | PowerMockito.mockStatic(EntityFactory.class); 66 | client = new WebhookClientBuilder(1234, "token").setWait(true).setHttpClient(httpClient).build(); 67 | } 68 | 69 | @After 70 | public void cleanup() throws Exception { 71 | client.close(); 72 | mocks.close(); 73 | } 74 | 75 | @Test 76 | public void testPassedEntity() throws InterruptedException, ExecutionException, TimeoutException { 77 | ReadonlyMessage mockMessage = setupFakeResponse(ReceiveMessageTest.getMockMessageJson().toString(), false); 78 | ReadonlyMessage readMessage = client.send("dummy").get(5, TimeUnit.SECONDS); 79 | 80 | assertNotNull("Returned message is null", readMessage); 81 | assertSame("Returned message not same as result of EntityFactory.makeMessage", mockMessage, readMessage); 82 | } 83 | 84 | @Test 85 | public void testNonGzip() throws InterruptedException, ExecutionException, TimeoutException { 86 | JSONObject json = ReceiveMessageTest.getMockMessageJson(); 87 | setupFakeResponse(json.toString(), false); 88 | client.send("dummy").get(5, TimeUnit.SECONDS); 89 | 90 | PowerMockito.verifyStatic(EntityFactory.class, only()); 91 | EntityFactory.makeMessage(any()); 92 | JSONObject value = jsonCaptor.getValue(); 93 | assertNotNull("Null json passed to EntityFactory", value); 94 | assertEquals("Json passed to EntityFactory is not 1:1 http response", json.toMap(), value.toMap()); 95 | } 96 | 97 | @Test 98 | public void testGzip() throws InterruptedException, ExecutionException, TimeoutException { 99 | JSONObject json = ReceiveMessageTest.getMockMessageJson(); 100 | setupFakeResponse(json.toString(), true); 101 | client.send("dummy").get(5, TimeUnit.SECONDS); 102 | 103 | PowerMockito.verifyStatic(EntityFactory.class, only()); 104 | EntityFactory.makeMessage(any()); 105 | JSONObject value = jsonCaptor.getValue(); 106 | assertNotNull("Null json passed to EntityFactory", value); 107 | assertEquals("Json passed to EntityFactory is not 1:1 http response", json.toMap(), value.toMap()); 108 | } 109 | 110 | private ReadonlyMessage setupFakeResponse(String json, boolean useGzip) { 111 | when(httpClient.newCall(any())).thenAnswer(invoc -> IOTestUtil.forgeCall(invoc.getArgument(0), json, useGzip)); 112 | ReadonlyMessage msg = new ReadonlyMessage(1, 2, false, false, 0, 113 | new ReadonlyUser(3, (short)4, false, "wh", null), 114 | "content", Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList() 115 | ); 116 | when(EntityFactory.makeMessage(jsonCaptor.capture())).thenReturn(msg); 117 | return msg; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/test/java/root/send/IOMock.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 Florian Spieß 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package root.send; 18 | 19 | import club.minnced.discord.webhook.LibraryInfo; 20 | import club.minnced.discord.webhook.WebhookClient; 21 | import club.minnced.discord.webhook.WebhookClientBuilder; 22 | import club.minnced.discord.webhook.send.WebhookEmbedBuilder; 23 | import club.minnced.discord.webhook.send.WebhookMessage; 24 | import club.minnced.discord.webhook.send.WebhookMessageBuilder; 25 | import okhttp3.OkHttpClient; 26 | import okhttp3.Request; 27 | import okhttp3.RequestBody; 28 | import org.junit.After; 29 | import org.junit.Assert; 30 | import org.junit.Before; 31 | import org.junit.Test; 32 | import org.mockito.ArgumentCaptor; 33 | import org.mockito.Captor; 34 | import org.mockito.Mock; 35 | import org.mockito.MockitoAnnotations; 36 | 37 | import static org.mockito.Mockito.*; 38 | 39 | public class IOMock { 40 | 41 | @Captor 42 | private ArgumentCaptor requestCaptor; 43 | 44 | @Mock 45 | private OkHttpClient httpClient; 46 | 47 | private WebhookClient client; 48 | 49 | private AutoCloseable mocks; 50 | 51 | @Before 52 | public void init() { 53 | mocks = MockitoAnnotations.openMocks(this); 54 | when(httpClient.newCall(any())).thenReturn(null); //will make WebhookClient code throw NPE internally, which we don't care about 55 | client = new WebhookClientBuilder(1234, "token").setWait(false).setHttpClient(httpClient).build(); 56 | } 57 | 58 | @After 59 | public void cleanup() throws Exception { 60 | client.close(); 61 | mocks.close(); 62 | } 63 | 64 | @Test 65 | public void testUrl() { 66 | client.send("Hello World"); 67 | 68 | verify(httpClient, timeout(1000).only()).newCall(requestCaptor.capture()); 69 | Request req = requestCaptor.getValue(); 70 | Assert.assertEquals("POST", req.method()); 71 | String expectedUrl = String.format("https://discord.com/api/v%d/webhooks/%d/%s", LibraryInfo.DISCORD_API_VERSION, 1234, "token"); 72 | Assert.assertEquals(expectedUrl, req.url().toString()); 73 | } 74 | 75 | @Test 76 | public void messageBodyUsed() { 77 | //implicitly checks json sent due to json (requestbody) being checked in MessageTest testclass 78 | RequestBody body = new WebhookMessageBuilder() 79 | .setContent("CONTENT!") 80 | .setUsername("MrWebhook") 81 | .setAvatarUrl("linkToImage") 82 | .setTTS(true) 83 | .addEmbeds(new WebhookEmbedBuilder().setDescription("embed").build()) 84 | .build().getBody(); 85 | 86 | WebhookMessage mock = mock(WebhookMessage.class); 87 | when(mock.getBody()).thenReturn(body); 88 | 89 | client.send(mock); 90 | 91 | verify(httpClient, timeout(1000).only()).newCall(requestCaptor.capture()); 92 | Request req = requestCaptor.getValue(); 93 | Assert.assertSame(body, req.body()); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/test/java/root/send/MessageTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 Florian Spieß 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package root.send; 18 | 19 | import club.minnced.discord.webhook.IOUtil; 20 | import club.minnced.discord.webhook.send.WebhookEmbed; 21 | import club.minnced.discord.webhook.send.WebhookEmbedBuilder; 22 | import club.minnced.discord.webhook.send.WebhookMessage; 23 | import club.minnced.discord.webhook.send.WebhookMessageBuilder; 24 | import net.dv8tion.jda.api.EmbedBuilder; 25 | import net.dv8tion.jda.api.entities.Mentions; 26 | import net.dv8tion.jda.api.entities.Message; 27 | import net.dv8tion.jda.api.entities.MessageEmbed; 28 | import net.dv8tion.jda.api.entities.MessageType; 29 | import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder; 30 | import net.dv8tion.jda.api.utils.messages.MessageCreateData; 31 | import net.dv8tion.jda.internal.entities.ReceivedMessage; 32 | import okhttp3.RequestBody; 33 | import org.json.JSONArray; 34 | import org.json.JSONObject; 35 | import org.junit.Assert; 36 | import org.junit.Before; 37 | import org.junit.Test; 38 | import root.IOTestUtil; 39 | 40 | import java.io.File; 41 | import java.io.FileInputStream; 42 | import java.io.IOException; 43 | import java.nio.charset.StandardCharsets; 44 | import java.util.Arrays; 45 | import java.util.HashMap; 46 | import java.util.List; 47 | import java.util.Map; 48 | 49 | import static org.mockito.Answers.RETURNS_DEFAULTS; 50 | import static org.powermock.api.mockito.PowerMockito.*; 51 | 52 | public class MessageTest { 53 | private WebhookMessageBuilder builder; 54 | 55 | @Before 56 | public void setupBuilder() { 57 | builder = new WebhookMessageBuilder(); 58 | } 59 | 60 | @Test 61 | public void setAndReset() { 62 | //checking isEmpty and reset of those fields 63 | Assert.assertTrue("Builder should be empty at start", builder.isEmpty()); 64 | 65 | builder.setContent("CONTENT!"); 66 | Assert.assertFalse("Setting content doesn't change isEmpty to false", builder.isEmpty()); 67 | builder.reset(); 68 | Assert.assertTrue("Reset doesn't reset content", builder.isEmpty()); 69 | 70 | builder.addEmbeds(new WebhookEmbedBuilder().setDescription("test").build()); 71 | Assert.assertFalse("Adding embed doesn't change isEmpty to false", builder.isEmpty()); 72 | builder.reset(); 73 | Assert.assertTrue("Reset doesn't reset embed(s)", builder.isEmpty()); 74 | 75 | Assert.assertEquals("File count of empty builder mismatches", 0, builder.getFileAmount()); 76 | builder.addFile("notARealFile", new byte[0]); 77 | Assert.assertEquals("File count of builder mismatches", 1, builder.getFileAmount()); 78 | Assert.assertFalse("Adding file doesn't change isEmpty to false", builder.isEmpty()); 79 | builder.reset(); 80 | Assert.assertEquals("File count of empty builder mismatches", 0, builder.getFileAmount()); 81 | Assert.assertTrue("Reset doesn't reset file(s)", builder.isEmpty()); 82 | 83 | //checking remaining setters + reset on those 84 | builder.setUsername("NotAWebhook"); 85 | builder.setAvatarUrl("avatarUrl"); 86 | builder.setTTS(true); 87 | Assert.assertTrue("Some extra field set isEmpty to false", builder.isEmpty()); 88 | builder.setContent("dummy"); //needed for building 89 | WebhookMessage msg = builder.build(); 90 | Assert.assertEquals("Username mismatches", "NotAWebhook", msg.getUsername()); 91 | Assert.assertEquals("AvatarUrl mismatches", "avatarUrl", msg.getAvatarUrl()); 92 | Assert.assertTrue("TTS mismatches", msg.isTTS()); 93 | 94 | builder.reset(); 95 | builder.setContent("dummy"); //needed for building 96 | msg = builder.build(); 97 | Assert.assertNull("Username not reset by reset()", msg.getUsername()); 98 | Assert.assertNull("AvatarUrl not reset by reset()", msg.getAvatarUrl()); 99 | Assert.assertFalse("TTS not reset by reset()", msg.isTTS()); 100 | } 101 | 102 | @Test 103 | public void messageBuilds() { 104 | builder.setContent("Hello World"); 105 | builder.setUsername("Minn"); 106 | builder.build().getBody(); 107 | } 108 | 109 | @Test 110 | public void buildMessageWithEmbed() { 111 | List embedList = Arrays.asList( 112 | new WebhookEmbedBuilder() 113 | .setDescription("Hello World") 114 | .build(), 115 | new WebhookEmbedBuilder() 116 | .setDescription("World") 117 | .build() 118 | ); 119 | builder.addEmbeds(embedList.get(0)); 120 | builder.addEmbeds(embedList.subList(1, 2)); 121 | WebhookMessage message = builder.build(); 122 | for (int i = 0; i < 2; i++) { 123 | Assert.assertEquals(embedList.get(i), message.getEmbeds().get(i)); 124 | } 125 | } 126 | 127 | @Test 128 | public void buildMessageWithFiles() throws IOException { 129 | File tmp = File.createTempFile("message-test", "cat.png"); 130 | builder.addFile(tmp); 131 | builder.addFile("dog.png", new FileInputStream(tmp)); 132 | builder.addFile("bird.png", IOUtil.readAllBytes(new FileInputStream(tmp))); 133 | tmp.delete(); 134 | WebhookMessage message = builder.build(); 135 | Assert.assertNotNull(message.getAttachments()); 136 | Assert.assertEquals(3, message.getAttachments().length); 137 | Assert.assertEquals(tmp.getName(), message.getAttachments()[0].getName()); 138 | Assert.assertEquals("dog.png", message.getAttachments()[1].getName()); 139 | Assert.assertEquals("bird.png", message.getAttachments()[2].getName()); 140 | } 141 | 142 | @Test 143 | public void buildMessageWithDataMessage() { 144 | MessageEmbed jdaEmbed = new EmbedBuilder() 145 | .setTitle("myEmbed") 146 | .build(); 147 | 148 | MessageCreateData jdaMessage = new MessageCreateBuilder() 149 | .setTTS(true) 150 | .setContent("myContent") 151 | .setEmbeds(jdaEmbed) 152 | .build(); 153 | 154 | WebhookMessage webhookMessage = WebhookMessageBuilder.fromJDA(jdaMessage).build(); 155 | List webhookEmbeds = webhookMessage.getEmbeds(); 156 | 157 | Assert.assertEquals(webhookEmbeds.size(), 1); 158 | 159 | WebhookEmbed webhookEmbed = webhookEmbeds.get(0); 160 | 161 | Assert.assertTrue(webhookMessage.isTTS()); 162 | Assert.assertEquals(webhookMessage.getContent(), "myContent"); 163 | Assert.assertEquals(webhookEmbed.getTitle().getText(), "myEmbed"); 164 | } 165 | 166 | @Test 167 | public void buildMessageWithReceivedMessage() { 168 | MessageEmbed jdaEmbed = new EmbedBuilder() 169 | .setTitle("myEmbed") 170 | .build(); 171 | 172 | Message jdaMessage = mock(ReceivedMessage.class); 173 | Mentions mentions = mock(Mentions.class, RETURNS_DEFAULTS); 174 | when(jdaMessage.isTTS()).thenReturn(true); 175 | when(jdaMessage.getContentRaw()).thenReturn("myContent"); 176 | when(jdaMessage.getEmbeds()).thenReturn(Arrays.asList(jdaEmbed)); 177 | when(jdaMessage.getMentions()).thenReturn(mentions); 178 | when(jdaMessage.getType()).thenReturn(MessageType.DEFAULT); 179 | 180 | WebhookMessage webhookMessage = WebhookMessageBuilder.fromJDA(jdaMessage).build(); 181 | List webhookEmbeds = webhookMessage.getEmbeds(); 182 | 183 | Assert.assertEquals(webhookEmbeds.size(), 1); 184 | 185 | WebhookEmbed webhookEmbed = webhookEmbeds.get(0); 186 | 187 | Assert.assertTrue(jdaMessage instanceof ReceivedMessage); 188 | Assert.assertTrue(webhookMessage.isTTS()); 189 | Assert.assertEquals(webhookMessage.getContent(), "myContent"); 190 | Assert.assertEquals(webhookEmbed.getTitle().getText(), "myEmbed"); 191 | } 192 | 193 | @Test 194 | public void factoryEmbeds() { 195 | WebhookEmbed embed1 = new WebhookEmbedBuilder() 196 | .setDescription("Hello").build(); 197 | WebhookEmbed embed2 = new WebhookEmbedBuilder() 198 | .setDescription("World").build(); 199 | WebhookMessage.embeds(embed1, embed2).getBody(); 200 | WebhookMessage.embeds(Arrays.asList(embed1, embed2)).getBody(); 201 | } 202 | 203 | @Test 204 | public void factoryFiles() throws IOException { 205 | File tmp = File.createTempFile("message-test", "cat.png"); 206 | WebhookMessage.files( 207 | "cat.png", tmp, 208 | "dog.png", new FileInputStream(tmp), 209 | "bird.png", IOUtil.readAllBytes(new FileInputStream(tmp))).getBody(); 210 | Map files = new HashMap<>(); 211 | files.put("cat.png", tmp); 212 | files.put("dog.png", new FileInputStream(tmp)); 213 | files.put("bird.png", IOUtil.readAllBytes(new FileInputStream(tmp))); 214 | WebhookMessage.files(files).getBody(); 215 | tmp.delete(); 216 | } 217 | 218 | @Test 219 | public void buildEmptyMessage() { 220 | Assert.assertThrows(IllegalStateException.class, () -> builder.build()); 221 | } 222 | 223 | @Test 224 | public void checkJSONNonFile() throws IOException { 225 | JSONObject allowedMentions = new JSONObject() 226 | .put("parse", new JSONArray() 227 | .put("users") 228 | .put("roles") 229 | .put("everyone")); 230 | 231 | Map expected = new JSONObject() 232 | .put("content", "CONTENT!") 233 | .put("username", "MrWebhook") 234 | .put("avatar_url", "linkToImage") 235 | .put("tts", true) 236 | .put("embeds", new JSONArray().put(new JSONObject().put("description", "embed"))) 237 | .put("allowed_mentions", allowedMentions) 238 | .put("flags", 0) 239 | .toMap(); 240 | 241 | WebhookMessage msg = builder 242 | .setContent("CONTENT!") 243 | .setUsername("MrWebhook") 244 | .setAvatarUrl("linkToImage") 245 | .setTTS(true) 246 | .addEmbeds(new WebhookEmbedBuilder().setDescription("embed").build()) 247 | .build(); 248 | Assert.assertFalse("Message should not be of type file", msg.isFile()); 249 | RequestBody body = msg.getBody(); 250 | Assert.assertEquals("Request type mismatch", IOUtil.JSON, body.contentType()); 251 | 252 | String bodyContent = IOTestUtil.readRequestBody(body); 253 | 254 | Map provided = new JSONObject(bodyContent).toMap(); 255 | 256 | Assert.assertEquals("Json output is incorrect", expected, provided); 257 | 258 | // This is no longer expected behavior, we intentionally include optional fields due to the PATCH endpoint behavior 259 | // //check if optional fields are omitted if not used (tts is always sent) 260 | // expected = new JSONObject() 261 | // .put("content", "...") 262 | // .put("tts", false) 263 | // .put("allowed_mentions", allowedMentions) 264 | // .toMap(); 265 | // 266 | // msg = builder 267 | // .reset() 268 | // .setContent("...") 269 | // .build(); 270 | // 271 | // bodyContent = IOTestUtil.readRequestBody(msg.getBody()); 272 | // provided = new JSONObject(bodyContent).toMap(); 273 | // 274 | // Assert.assertEquals("Json output has additional fields", expected, provided); 275 | } 276 | 277 | @Test 278 | public void checkMultipart() throws IOException { 279 | JSONObject allowedMentions = new JSONObject() 280 | .put("parse", new JSONArray() 281 | .put("users") 282 | .put("roles") 283 | .put("everyone")); 284 | 285 | String fileContent = "Hello World!\nNext line...\r\nAnother line"; 286 | WebhookMessage msg = builder 287 | .setContent("CONTENT!") 288 | .addFile("myFile.txt", fileContent.getBytes(StandardCharsets.UTF_8)) 289 | .build(); 290 | Assert.assertTrue("Message should be of type file", msg.isFile()); 291 | 292 | RequestBody body = msg.getBody(); 293 | Assert.assertTrue("Request type mismatch", IOTestUtil.isMultiPart(body)); 294 | 295 | Map multiPart = IOTestUtil.parseMultipart(body); 296 | 297 | Assert.assertTrue("Multipart doesn't contain payload json", multiPart.containsKey("payload_json")); 298 | Assert.assertTrue("Multipart json is not of correct type", multiPart.get("payload_json") instanceof String); 299 | Assert.assertEquals("Multipart json mismatches", 300 | new JSONObject() 301 | .put("allowed_mentions", allowedMentions) 302 | .put("content", "CONTENT!") 303 | .put("embeds", new JSONArray()) 304 | .put("tts", false) 305 | .put("flags", 0) 306 | .toMap(), 307 | new JSONObject((String) multiPart.get("payload_json")).toMap() 308 | ); 309 | 310 | Assert.assertTrue("Multipart doesn't contain file", multiPart.containsKey("file0")); 311 | Assert.assertTrue("Multipart file is not of correct type", multiPart.get("file0") instanceof IOTestUtil.MultiPartFile); 312 | Assert.assertEquals("Multipart file mismatches", 313 | fileContent, 314 | new String(((IOTestUtil.MultiPartFile) multiPart.get("file0")).content, StandardCharsets.UTF_8) 315 | ); 316 | } 317 | 318 | } 319 | -------------------------------------------------------------------------------- /src/test/java/root/send/SendEmbedTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 Florian Spieß 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package root.send; 18 | 19 | import club.minnced.discord.webhook.send.WebhookEmbed; 20 | import club.minnced.discord.webhook.send.WebhookEmbedBuilder; 21 | import org.json.JSONArray; 22 | import org.json.JSONObject; 23 | import org.junit.Assert; 24 | import org.junit.Before; 25 | import org.junit.Test; 26 | 27 | import java.time.Instant; 28 | import java.time.OffsetDateTime; 29 | import java.time.format.DateTimeFormatter; 30 | import java.time.temporal.ChronoUnit; 31 | import java.util.Map; 32 | import java.util.concurrent.atomic.AtomicInteger; 33 | 34 | public class SendEmbedTest { 35 | private WebhookEmbedBuilder builder; 36 | 37 | @Before 38 | public void setup() { 39 | builder = new WebhookEmbedBuilder(); 40 | } 41 | 42 | @Test 43 | public void checkEmptyAndReset() { 44 | Assert.assertTrue("Builder is supposed to be empty at the very start", builder.isEmpty()); 45 | 46 | builder.setAuthor(new WebhookEmbed.EmbedAuthor("Author", null, null)); 47 | Assert.assertFalse("Setting author doesn't change isEmpty to false", builder.isEmpty()); 48 | builder.reset(); 49 | Assert.assertTrue("Reset doesn't reset author", builder.isEmpty()); 50 | 51 | builder.setDescription(""); 52 | Assert.assertTrue("Empty description should still be empty", builder.isEmpty()); 53 | builder.setDescription("desc"); 54 | Assert.assertFalse("Setting description doesn't change isEmpty to false", builder.isEmpty()); 55 | builder.reset(); 56 | Assert.assertTrue("Reset doesn't reset description", builder.isEmpty()); 57 | 58 | builder.setFooter(new WebhookEmbed.EmbedFooter("Text", null)); 59 | Assert.assertFalse("Setting footer doesn't change isEmpty to false", builder.isEmpty()); 60 | builder.reset(); 61 | Assert.assertTrue("Reset doesn't reset footer", builder.isEmpty()); 62 | 63 | builder.setImageUrl(""); 64 | Assert.assertTrue("Empty image should still be empty", builder.isEmpty()); 65 | builder.setImageUrl("imgurl"); 66 | Assert.assertFalse("Setting image doesn't change isEmpty to false", builder.isEmpty()); 67 | builder.reset(); 68 | Assert.assertTrue("Reset doesn't reset image", builder.isEmpty()); 69 | 70 | builder.setThumbnailUrl(""); 71 | Assert.assertTrue("Empty thumbnail should still be empty", builder.isEmpty()); 72 | builder.setThumbnailUrl("thumb"); 73 | Assert.assertFalse("Setting thumbnail doesn't change isEmpty to false", builder.isEmpty()); 74 | builder.reset(); 75 | Assert.assertTrue("Reset doesn't reset thumbnail", builder.isEmpty()); 76 | 77 | builder.setTimestamp(Instant.now()); 78 | Assert.assertFalse("Setting timestamp doesn't change isEmpty to false", builder.isEmpty()); 79 | builder.reset(); 80 | Assert.assertTrue("Reset doesn't reset timestamp", builder.isEmpty()); 81 | 82 | builder.setTitle(new WebhookEmbed.EmbedTitle("title", null)); 83 | Assert.assertFalse("Setting title doesn't change isEmpty to false", builder.isEmpty()); 84 | builder.reset(); 85 | Assert.assertTrue("Reset doesn't reset title", builder.isEmpty()); 86 | 87 | builder.addField(new WebhookEmbed.EmbedField(true, "FieldKey", "FieldValue")); 88 | Assert.assertFalse("Adding field doesn't change isEmpty to false", builder.isEmpty()); 89 | builder.reset(); 90 | Assert.assertTrue("Reset doesn't reset field(s)", builder.isEmpty()); 91 | 92 | builder.setColor(0xFFFFFF); 93 | builder.setDescription("dummy"); //needed for build to work 94 | Assert.assertEquals("Color was not properly set", (Object) 0xFFFFFF, builder.build().getColor()); 95 | builder.reset(); 96 | builder.setDescription("dummy"); 97 | Assert.assertNull("Reset doesn't reset color", builder.build().getColor()); 98 | } 99 | 100 | @Test 101 | public void nonNullConstructors() { 102 | AtomicInteger numFailed = new AtomicInteger(0); 103 | 104 | try { 105 | new WebhookEmbed.EmbedAuthor(null, null, null); 106 | } catch(NullPointerException | IllegalArgumentException ex) { numFailed.incrementAndGet(); } 107 | 108 | try { 109 | new WebhookEmbed.EmbedTitle(null, null); 110 | } catch(NullPointerException | IllegalArgumentException ex) { numFailed.incrementAndGet(); } 111 | 112 | try { 113 | new WebhookEmbed.EmbedFooter(null, null); 114 | } catch(NullPointerException | IllegalArgumentException ex) { numFailed.incrementAndGet(); } 115 | 116 | try { 117 | new WebhookEmbed.EmbedField(true, "key", null); 118 | } catch(NullPointerException | IllegalArgumentException ex) { numFailed.incrementAndGet(); } 119 | 120 | try { 121 | new WebhookEmbed.EmbedField(true, null, "val"); 122 | } catch(NullPointerException | IllegalArgumentException ex) { numFailed.incrementAndGet(); } 123 | 124 | Assert.assertEquals("Not all constructors with non-null field annotation threw errors", 5, numFailed.get()); 125 | } 126 | 127 | @Test 128 | public void embedBuildsSuccessfully() { 129 | populateBuilder(); 130 | builder.build(); 131 | } 132 | 133 | @Test 134 | public void buildEmptyEmbed() { 135 | Assert.assertThrows(IllegalStateException.class, () -> builder.build()); 136 | } 137 | 138 | @Test 139 | public void checkJSON() { 140 | Map expected = new JSONObject() 141 | .put("title", "Title") 142 | .put("url", "titleUrl") 143 | .put("author", new JSONObject() 144 | .put("name", "Minn") 145 | .put("url", "authorUrl") 146 | .put("icon_url", "authorIcon")) 147 | .put("color", 0xFF00FF) 148 | .put("description", "Hello World!") 149 | .put("image", new JSONObject().put("url", "imgUrl")) 150 | .put("thumbnail", new JSONObject().put("url", "thumbUrl")) 151 | .put("fields", new JSONArray().put(new JSONObject() 152 | .put("inline", true) 153 | .put("name", "key") 154 | .put("value", "val"))) 155 | .put("footer", new JSONObject() 156 | .put("text", "Footer text") 157 | .put("icon_url", "footerIcon")) 158 | .toMap(); 159 | 160 | populateBuilder(); 161 | Map provided = new JSONObject((builder.build().toJSONString())).toMap(); 162 | 163 | Assert.assertTrue("Timestamp is not correctly set in json", provided.containsKey("timestamp") 164 | && Math.abs(Instant.from(DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse((String) provided.get("timestamp"))).until(Instant.now(), ChronoUnit.MILLIS)) < 50); 165 | 166 | provided.remove("timestamp"); 167 | Assert.assertEquals("Json output is incorrect", expected, provided); 168 | 169 | //check if optional fields are (properly) omitted 170 | builder.reset(); 171 | builder.setDescription("desc"); 172 | expected = new JSONObject().put("description", "desc").toMap(); 173 | provided = new JSONObject(builder.build().toJSONString()).toMap(); 174 | Assert.assertEquals("Json output is adding extra (non-set) fields", expected, provided); 175 | 176 | builder.reset(); 177 | builder.setTitle(new WebhookEmbed.EmbedTitle("title", null)); 178 | builder.setAuthor(new WebhookEmbed.EmbedAuthor("author", null, null)); 179 | builder.setFooter(new WebhookEmbed.EmbedFooter("footer", null)); 180 | expected = new JSONObject() 181 | .put("title", "title") 182 | .put("author", new JSONObject().put("name", "author")) 183 | .put("footer", new JSONObject().put("text", "footer")) 184 | .toMap(); 185 | provided = new JSONObject(builder.build().toJSONString()).toMap(); 186 | Assert.assertEquals("Json output is adding extra (non-set) fields", expected, provided); 187 | } 188 | 189 | private void populateBuilder() { 190 | builder.setAuthor(new WebhookEmbed.EmbedAuthor("Minn", "authorIcon", "authorUrl")); 191 | builder.setColor(0xff00ff); 192 | builder.setDescription("Hello World!"); 193 | builder.setFooter(new WebhookEmbed.EmbedFooter("Footer text", "footerIcon")); 194 | builder.setImageUrl("imgUrl"); 195 | builder.setThumbnailUrl("thumbUrl"); 196 | builder.setTitle(new WebhookEmbed.EmbedTitle("Title", "titleUrl")); 197 | builder.setTimestamp(OffsetDateTime.now()); 198 | builder.setTimestamp(Instant.now()); 199 | builder.addField(new WebhookEmbed.EmbedField(true, "key", "val")); 200 | } 201 | } 202 | 203 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | >%d{HH:mm:ss} %boldCyan(%-34.-34thread) %red(%10.10X{jda.shard}) %boldGreen(%-15.-15logger{0}) 22 | %highlight(%-6level) %msg%n 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | --------------------------------------------------------------------------------