diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..44b4224
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+* eol=lf
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..39ba6cb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.gradle/
+build/
\ No newline at end of file
diff --git a/README.md b/README.md
index fdd1f5b..b5cac69 100644
--- a/README.md
+++ b/README.md
@@ -1,106 +1,396 @@
-# DBL Java Library
-A Java wrapper for the [top.gg API](https://top.gg/api/docs)
+# Top.gg Java SDK
+
+The community-maintained Java library for Top.gg.
+
+## Chapters
+
+- [Installation](#installation)
+- [Capabilities](#capabilities)
+- [Setting up](#setting-up)
+- [Usage](#usage)
+ - [Getting your project's information](#getting-your-projects-information)
+ - [Getting your project's vote information of a user](#getting-your-projects-vote-information-of-a-user)
+ - [Getting a cursor-based paginated list of votes for your project](#getting-a-cursor-based-paginated-list-of-votes-for-your-project)
+ - [Posting your bot's application commands list](#posting-your-bots-application-commands-list)
+ - [Generating widget URLs](#generating-widget-urls)
+ - [Webhooks](#webhooks)
+
+## Installation
+
+### Gradle
+
+Add the following line to the `dependencies` section of your `build.gradle`:
+
+```groovy
+implementation 'org.discordbots:DBL-Java-Library:3.0.0'
+```
+
+### Maven
+
+Add the following line to the `dependencies` section of your `pom.xml`:
+
+```xml
+
+ com.discordbots
+ DBL-Java-Library
+ 3.0.0
+
+```
+
+## Capabilities
+
+This library provides several capabilities that can be enabled/disabled, such as:
+
+- **`jdaWrapper`**: Additional wrappers for working with [JDA](https://github.com/discord-jda/JDA).
+- **`discord4jWrapper`**: Additional wrappers for working with [Discord4J](https://github.com/Discord4J/Discord4J).
+- **`webhooks`**: Accessing deserializable webhook payload classes.
+ - **`dropwizardWebhooks`**: Wrapper for working with the [Dropwizard](https://www.dropwizard.io/en/stable/) web framework.
+ - **`eclipseJettyWebhooks`**: Wrapper for working with the [Eclipse Jetty](https://jetty.org/index.html) web framework.
+ - **`springBootWebhooks`**: Wrapper for working with the [Spring Boot](https://spring.io/projects/spring-boot/) web framework.
+
+## Setting up
+
+```java
+import org.discordbots.api.DBLAPI;
+
+final DBLAPI client = new DBLAPI(System.getenv("TOPGG_TOKEN"));
+```
## Usage
-First, build a DiscordBotListAPI object.
+### Getting your project's information
```java
-DiscordBotListAPI api = new DiscordBotListAPI.Builder()
- .token("token")
- .botId("botId")
- .build();
+client.getSelf().whenComplete((project, error) -> {
+ if (error != null) {
+ System.err.println("Error: " + error.getMessage());
+ } else {
+ // ...
+ }
+});
```
-#### Posting stats
+### Getting your project's vote information of a user
+
+#### Discord ID
+
+```java
+import org.discordbots.api.entity.UserSource;
+
+client.getVote(UserSource.DISCORD, "661200758510977084").whenComplete((vote, error) -> {
+ if (error != null) {
+ System.err.println("Error: " + error.getMessage());
+ } else if (vote == null) {
+ System.out.println("The user has not voted.");
+ } else {
+ // ...
+ }
+});
+```
-DBL provides three ways to post your bots stats.
+#### Top.gg ID
-**#1**
-Posts the server count for the whole bot.
```java
-int serverCount = ...; // the total amount of servers across all shards
+import org.discordbots.api.entity.UserSource;
-api.setStats(serverCount);
+client.getVote(UserSource.TOPGG, "8226924471638491136").whenComplete((vote, error) -> {
+ if (error != null) {
+ System.err.println("Error: " + error.getMessage());
+ } else if (vote == null) {
+ System.out.println("The user has not voted.");
+ } else {
+ // ...
+ }
+});
```
-**#2**
-Posts the server count for an individual shard.
+### Getting a cursor-based paginated list of votes for your project
+
```java
-int shardId = ...; // the id of this shard
-int shardCount = ...; // the amount of shards
-int serverCount = ...; // the server count of this shard
+import org.discordbots.api.entity.Vote;
+
+final OffsetDateTime since = OffsetDateTime.of(2026, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC);
+
+client.getVotes(since).whenComplete((firstPage, error) -> {
+ if (error != null) {
+ System.err.println("Error: " + error.getMessage());
+ } else {
+ final List firstPageVotes = firstPage.getVotes();
+
+ firstPage.next().whenComplete((secondPage, secondError) => {
+ if (secondError != null) {
+ System.err.println("Error: " + secondError.getMessage());
+ } else {
+ final List secondPageVotes = secondPage.getVotes();
-api.setStats(shardId, shardCount, serverCount);
+ // ...
+ }
+ });
+ }
+});
```
-**#3**
-Posts the server counts for every shard in one request.
+### Posting your bot's application commands list
+
+#### JDA
+
+> **NOTE**: Requires the `jdaWrapper` capability.
+
```java
-List shardServerCounts = ...; // a list of all the shards' server counts
+final JDA jda = ...;
-api.setStats(shardServerCounts);
+client.postCommands(jda);
```
-#### Checking votes
+#### Discord4J
+
+> **NOTE**: Requires the `discord4jWrapper` capability.
```java
-String userId = ...; // ID of the user you're checking
-api.hasVoted(userId).whenComplete((hasVoted, e) -> {
- if(hasVoted)
- System.out.println("This person has voted!");
- else
- System.out.println("This person has not voted!");
-});
+final DiscordClient bot = ...;
+
+client.postCommands(bot);
```
-#### Getting voting multiplier
+#### Raw
```java
-api.getVotingMultiplier().whenComplete((multiplier, e) -> {
- if(multiplier.isWeekend())
- System.out.println("It's the weekend, so votes are worth 2x!");
- else
- System.out.println("It's not the weekend :pensive:");
-});
+import com.google.gson.JsonArray;
+import com.google.gson.JsonParser;
+
+// Array of application commands that
+// can be serialized to Discord API's raw JSON format.
+final String commandsJson =
+ "[{" +
+ " \"id\": \"1\"," +
+ " \"type\": 1," +
+ " \"application_id\": \"1\"," +
+ " \"name\": \"test\"," +
+ " \"description\": \"command description\"," +
+ " \"default_member_permissions\": \"\"," +
+ " \"version\": \"1\"" +
+ "}]";
+
+final JsonArray commands = JsonParser.parseString(commandsJson).getAsJsonArray();
+
+client.postCommands(commands);
```
-## Download
+### Generating widget URLs
-[](https://jitpack.io/#top-gg/java-sdk)
+#### Large
-Replace `VERSION` with the latest version or commit hash. The latest version can be found under releases.
+```java
+import org.discordbots.api.DBLWidget;
+import org.discordbots.api.entity.ProjectType;
-#### Maven
+final String widgetUrl = DBLWidget.large(ProjectType.DISCORD_BOT, "574652751745777665");
+```
-```xml
-
-
- jitpack.io
- https://jitpack.io
-
-
+#### Votes
+
+```java
+import org.discordbots.api.DBLWidget;
+import org.discordbots.api.entity.ProjectType;
+
+final String widgetUrl = DBLWidget.votes(ProjectType.DISCORD_BOT, "574652751745777665");
```
-```xml
-
-
- com.github.top-gg
- java-sdk
- VERSION
-
-
-```
-
-#### Gradle
-```gradle
-repositories {
- maven { url 'https://jitpack.io' }
+
+#### Owner
+
+```java
+import org.discordbots.api.DBLWidget;
+import org.discordbots.api.entity.ProjectType;
+
+final String widgetUrl = DBLWidget.owner(ProjectType.DISCORD_BOT, "574652751745777665");
+```
+
+#### Social
+
+```java
+import org.discordbots.api.DBLWidget;
+import org.discordbots.api.entity.ProjectType;
+
+final String widgetUrl = DBLWidget.social(ProjectType.DISCORD_BOT, "574652751745777665");
+```
+
+### Webhooks
+
+#### Dropwizard
+
+> **NOTE**: Requires the `dropwizardWebhooks` capability.
+
+In your `Webhooks.java`:
+
+```java
+import org.discordbots.webhooks.dropwizard.DBLWebhooks;
+import org.discordbots.webhooks.payload.IntegrationCreatePayload;
+import org.discordbots.webhooks.payload.IntegrationDeletePayload;
+import org.discordbots.webhooks.payload.TestPayload;
+import org.discordbots.webhooks.payload.VoteCreatePayload;
+
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.core.Response;
+
+// POST /webhook
+@Path("/webhook")
+public class Webhooks extends DBLWebhooks {
+ public Webhooks() {
+ super(System.getenv("TOPGG_WEBHOOK_SECRET"));
+ }
+
+ // Optional
+ @Override
+ public Response onIntegrationCreate(final IntegrationCreatePayload payload, final String trace) {
+ return Response.status(Response.Status.NO_CONTENT).build();
+ }
+
+ // Optional
+ @Override
+ public Response onIntegrationDelete(final IntegrationDeletePayload payload, final String trace) {
+ return Response.status(Response.Status.NO_CONTENT).build();
+ }
+
+ // Optional
+ @Override
+ public Response onTest(final TestPayload payload, final String trace) {
+ return Response.status(Response.Status.NO_CONTENT).build();
+ }
+
+ // Optional
+ @Override
+ public Response onVoteCreate(final VoteCreatePayload payload, final String trace) {
+ return Response.status(Response.Status.NO_CONTENT).build();
+ }
}
```
-```gradle
-dependencies {
- compile 'com.github.top-gg:java-sdk:VERSION'
+
+Later, in your server's `run` function:
+
+```java
+env.jersey().register(new Webhooks());
+```
+
+#### Eclipse Jetty
+
+> **NOTE**: Requires the `eclipseJettyWebhooks` capability.
+
+In your `Webhooks.java`:
+
+```java
+import org.discordbots.webhooks.eclipsejetty.DBLWebhooks;
+import org.discordbots.webhooks.payload.IntegrationCreatePayload;
+import org.discordbots.webhooks.payload.IntegrationDeletePayload;
+import org.discordbots.webhooks.payload.TestPayload;
+import org.discordbots.webhooks.payload.VoteCreatePayload;
+
+import jakarta.servlet.http.HttpServletResponse;
+
+public class Webhooks extends DBLWebhooks {
+ public Webhooks() {
+ super(System.getenv("TOPGG_WEBHOOK_SECRET"));
+ }
+
+ // Optional
+ @Override
+ public void onIntegrationCreate(
+ final HttpServletResponse response,
+ final IntegrationCreatePayload payload,
+ final String trace) {
+ response.setStatus(HttpServletResponse.SC_NO_CONTENT);
+ }
+
+ // Optional
+ @Override
+ public void onIntegrationDelete(
+ final HttpServletResponse response,
+ final IntegrationDeletePayload payload,
+ final String trace) {
+ response.setStatus(HttpServletResponse.SC_NO_CONTENT);
+ }
+
+ // Optional
+ @Override
+ public void onTest(
+ final HttpServletResponse response, final TestPayload payload, final String trace) {
+ response.setStatus(HttpServletResponse.SC_NO_CONTENT);
+ }
+
+ // Optional
+ @Override
+ public void onVoteCreate(
+ final HttpServletResponse response, final VoteCreatePayload payload, final String trace) {
+ response.setStatus(HttpServletResponse.SC_NO_CONTENT);
+ }
}
```
+Later, in your server's setup:
+
+```java
+// POST /webhook
+context.addServlet(new ServletHolder(new Webhooks()), "/webhook");
+```
+#### Spring Boot
+
+> **NOTE**: Requires the `springBootWebhooks` capability.
+
+In your `Webhooks.java`:
+
+```java
+import org.discordbots.webhooks.springboot.DBLWebhooks;
+import org.discordbots.webhooks.payload.IntegrationCreatePayload;
+import org.discordbots.webhooks.payload.IntegrationDeletePayload;
+import org.discordbots.webhooks.payload.TestPayload;
+import org.discordbots.webhooks.payload.VoteCreatePayload;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class Webhooks extends DBLWebhooks {
+ public Webhooks() {
+ super(System.getenv("TOPGG_WEBHOOK_SECRET"));
+ }
+
+ // POST /webhook
+ @PostMapping("/webhook")
+ public ResponseEntity main(
+ @RequestBody final String body,
+ @RequestHeader("x-topgg-signature") final String signature,
+ @RequestHeader("x-topgg-trace") final String trace) {
+ return dispatch(body, signature, trace);
+ }
+
+ // Optional
+ @Override
+ public ResponseEntity onIntegrationCreate(
+ final IntegrationCreatePayload payload, final String trace) {
+ return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
+ }
+
+ // Optional
+ @Override
+ public ResponseEntity onIntegrationDelete(
+ final IntegrationDeletePayload payload, final String trace) {
+ return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
+ }
+
+ // Optional
+ @Override
+ public ResponseEntity onTest(final TestPayload payload, final String trace) {
+ return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
+ }
+
+ // Optional
+ @Override
+ public ResponseEntity onVoteCreate(final VoteCreatePayload payload, final String trace) {
+ return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
+ }
+}
+```
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 023b55d..6267511 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,19 +1,221 @@
-group 'org.discordbots'
+plugins {
+ id 'java-library'
+ id 'maven-publish'
+}
+
+def ver = '3.0.0'
+
+group = 'org.discordbots'
+version = ver
+description = 'The community-maintained Java library for Top.gg.'
-apply plugin: 'java'
-apply plugin: 'maven'
+if (JavaVersion.current() < JavaVersion.VERSION_17) {
+ throw new GradleException('Top.gg\'s Java SDK requires Java 17 or later. Java ${JavaVersion.current()} is not supported.')
+}
repositories {
- mavenCentral()
+ mavenCentral()
+}
+
+sourceSets {
+ jdaWrapper
+ discord4jWrapper
+
+ webhooks
+ dropwizardWebhooks
+ eclipseJettyWebhooks
+ springBootWebhooks
+}
+
+configurations {
+ dropwizardWebhooksImplementation.extendsFrom webhooksImplementation
+ eclipseJettyWebhooksImplementation.extendsFrom webhooksImplementation
+ springBootWebhooksImplementation.extendsFrom webhooksImplementation
+
+
+ testImplementation.extendsFrom dropwizardWebhooksImplementation
+ testImplementation.extendsFrom eclipseJettyWebhooksImplementation
+ testImplementation.extendsFrom springBootWebhooksImplementation
+
+ googleJavaFormat
+}
+
+test {
+ useJUnitPlatform()
+}
+
+java {
+ withSourcesJar()
+
+ registerFeature('jdaWrapper') {
+ usingSourceSet sourceSets.jdaWrapper
+
+ capability('org.discordbots', 'jdaWrapper', ver)
+ }
+
+ registerFeature('discord4jWrapper') {
+ usingSourceSet sourceSets.discord4jWrapper
+
+ capability('org.discordbots', 'discord4jWrapper', ver)
+ }
+
+ registerFeature('webhooks') {
+ usingSourceSet sourceSets.webhooks
+
+ capability('org.discordbots', 'webhooks', ver)
+ }
+
+ registerFeature('dropwizardWebhooks') {
+ usingSourceSet sourceSets.webhooks
+ usingSourceSet sourceSets.dropwizardWebhooks
+
+ capability('org.discordbots', 'dropwizardWebhooks', ver)
+ }
+
+ registerFeature('eclipseJettyWebhooks') {
+ usingSourceSet sourceSets.webhooks
+ usingSourceSet sourceSets.eclipseJettyWebhooks
+
+ capability('org.discordbots', 'eclipseJettyWebhooks', ver)
+ }
+
+ registerFeature('springBootWebhooks') {
+ usingSourceSet sourceSets.webhooks
+ usingSourceSet sourceSets.springBootWebhooks
+
+ capability('org.discordbots', 'springBootWebhooks', ver)
+ }
+}
+
+publishing {
+ publications {
+ maven(MavenPublication) {
+ artifactId = 'DBL-Java-Library'
+ groupId = 'org.discordbots'
+ version = ver
+
+ from components.java
+
+ pom {
+ name = 'DBL-Java-Library'
+ description = 'The community-maintained Java library for Top.gg.'
+ url = 'https://github.com/top-gg-community/java-sdk'
+ inceptionYear = '2020'
+
+ licenses {
+ license {
+ name = 'Apache License 2.0'
+ distribution = 'repo'
+ url = 'https://github.com/top-gg-community/java-sdk/blob/$ver/LICENSE'
+ }
+ }
+
+ developers {
+ developer {
+ id = 'top-gg-community'
+ name = 'Top.gg'
+ url = 'https://github.com/top-gg-community'
+ }
+ }
+
+ scm {
+ url = 'https://github.com/top-gg-community/java-sdk'
+ connection = 'scm:git:github.com/top-gg-community/java-sdk.git'
+ developerConnection = 'scm:git:ssh://github.com/top-gg-community/java-sdk.git'
+ }
+
+ issueManagement {
+ system = 'GitHub'
+ url = 'https://github.com/top-gg-community/java-sdk/issues'
+ }
+
+ ciManagement {
+ system = 'Github Actions'
+ url = 'https://github.com/top-gg-community/java-sdk/actions'
+ }
+ }
+ }
+ }
+
+ repositories {
+ maven {
+ url = layout.buildDirectory.dir 'staging-deploy'
+ }
+ }
}
dependencies {
+ implementation 'org.slf4j:slf4j-api:2.0.17'
+
+ implementation 'org.json:json:20251224'
+ implementation 'com.squareup.okhttp3:okhttp:5.3.2'
+ implementation 'com.google.code.gson:gson:2.13.2'
+ implementation 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2'
+
+ jdaWrapperImplementation sourceSets.main.output
+ jdaWrapperImplementation 'com.google.code.gson:gson:2.13.2'
+ jdaWrapperImplementation('net.dv8tion:JDA:6.3.1') {
+ exclude module: 'opus-java'
+ exclude module: 'tink'
+ }
+
+ discord4jWrapperImplementation sourceSets.main.output
+ discord4jWrapperImplementation 'com.discord4j:discord4j-core:3.3.1'
+
+ webhooksImplementation 'com.google.code.gson:gson:2.13.2'
+ webhooksImplementation 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2'
+
+ dropwizardWebhooksImplementation sourceSets.webhooks.output
+ dropwizardWebhooksImplementation 'jakarta.ws.rs:jakarta.ws.rs-api:4.0.0'
+ dropwizardWebhooksImplementation 'jakarta.servlet:jakarta.servlet-api:6.1.0'
+
+ eclipseJettyWebhooksImplementation sourceSets.webhooks.output
+ eclipseJettyWebhooksImplementation 'jakarta.servlet:jakarta.servlet-api:6.1.0'
- //Logger
- compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.25'
+ springBootWebhooksImplementation sourceSets.webhooks.output
+ springBootWebhooksImplementation 'org.springframework.boot:spring-boot-starter-web:4.0.3'
+ springBootWebhooksImplementation 'jakarta.servlet:jakarta.servlet-api:6.1.0'
+
+ testImplementation 'org.junit.jupiter:junit-jupiter-api:6.0.3'
+ testImplementation 'org.junit.platform:junit-platform-suite-api:6.0.3'
+ testImplementation 'com.google.code.gson:gson:2.13.2'
+ testImplementation 'io.dropwizard:dropwizard-testing:5.0.1'
+ testImplementation 'org.springframework.boot:spring-boot-starter-test-classic:4.0.3'
+ testImplementation 'org.springframework.boot:spring-boot-starter-security-test:4.0.3'
+
+ testImplementation sourceSets.dropwizardWebhooks.output
+ testImplementation sourceSets.eclipseJettyWebhooks.output
+ testImplementation sourceSets.springBootWebhooks.output
+
+ testCompileOnly 'org.junit.jupiter:junit-jupiter-params:6.0.3'
+ testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:6.0.3'
+ testRuntimeOnly 'org.junit.platform:junit-platform-launcher:6.0.3'
+
+ googleJavaFormat 'com.google.googlejavaformat:google-java-format:1.34.1'
+}
+
+tasks.register('format', JavaExec) {
+ group = 'format'
+ description = 'Format code'
+ classpath = configurations.googleJavaFormat
+ mainClass = 'com.google.googlejavaformat.java.Main'
+
+ def sources = fileTree(dir: 'src', include: '**/*.java')
+ args = ['-i'] + sources
+
+ inputs.files sources
+ outputs.files sources
+
+ jvmArgs(
+ '--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED',
+ '--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED',
+ '--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED',
+ '--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED',
+ '--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED',
+ '--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED'
+ )
+}
- compile group: 'org.json', name: 'json', version: '20180130'
- compile group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.11.0'
- compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5'
- compile group: 'com.fatboyindustrial.gson-javatime-serialisers', name: 'gson-javatime-serialisers', version: '1.1.1'
+tasks.withType(JavaCompile) {
+ options.compilerArgs << '-Xlint:deprecation'
}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 1948b90..61285a6 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 3eba147..37f78a6 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,7 @@
-#Sat Jul 14 00:30:34 EDT 2018
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.8-all.zip
diff --git a/gradlew b/gradlew
index cccdd3d..adff685 100644
--- a/gradlew
+++ b/gradlew
@@ -1,78 +1,128 @@
-#!/usr/bin/env sh
+#!/bin/sh
+
+#
+# Copyright © 2015 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
##############################################################################
-##
-## Gradle start up script for UN*X
-##
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
##############################################################################
# Attempt to set APP_HOME
+
# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
-
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
+MAX_FD=maximum
warn () {
echo "$*"
-}
+} >&2
die () {
echo
echo "$*"
echo
exit 1
-}
+} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
-case "`uname`" in
- CYGWIN* )
- cygwin=true
- ;;
- Darwin* )
- darwin=true
- ;;
- MINGW* )
- msys=true
- ;;
- NONSTOP* )
- nonstop=true
- ;;
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
esac
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
+ JAVACMD=$JAVA_HOME/jre/sh/java
else
- JAVACMD="$JAVA_HOME/bin/java"
+ JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -81,92 +131,118 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
- JAVACMD="java"
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
+ fi
fi
# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
- MAX_FD_LIMIT=`ulimit -H -n`
- if [ $? -eq 0 ] ; then
- if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
- MAX_FD="$MAX_FD_LIMIT"
- fi
- ulimit -n $MAX_FD
- if [ $? -ne 0 ] ; then
- warn "Could not set maximum file descriptor limit: $MAX_FD"
- fi
- else
- warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
- fi
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
fi
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
- GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin ; then
- APP_HOME=`cygpath --path --mixed "$APP_HOME"`
- CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
- JAVACMD=`cygpath --unix "$JAVACMD"`
-
- # We build the pattern for arguments to be converted via cygpath
- ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
- SEP=""
- for dir in $ROOTDIRSRAW ; do
- ROOTDIRS="$ROOTDIRS$SEP$dir"
- SEP="|"
- done
- OURCYGPATTERN="(^($ROOTDIRS))"
- # Add a user-defined pattern to the cygpath arguments
- if [ "$GRADLE_CYGPATTERN" != "" ] ; then
- OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
- fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
- i=0
- for arg in "$@" ; do
- CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
- CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
-
- if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
- eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
- else
- eval `echo args$i`="\"$arg\""
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
fi
- i=$((i+1))
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
done
- case $i in
- (0) set -- ;;
- (1) set -- "$args0" ;;
- (2) set -- "$args0" "$args1" ;;
- (3) set -- "$args0" "$args1" "$args2" ;;
- (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
- esac
fi
-# Escape application args
-save () {
- for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
- echo " "
-}
-APP_ARGS=$(save "$@")
-
-# Collect all arguments for the java command, following the shell quoting and substitution rules
-eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
-# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
-if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
- cd "$(dirname "$0")"
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
fi
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
index f955316..e509b2d 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -1,84 +1,93 @@
-@if "%DEBUG%" == "" @echo off
-@rem ##########################################################################
-@rem
-@rem Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS=
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
-
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto init
-
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:init
-@rem Get command-line arguments, handling Windows variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
-
-:end
-@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/src/discord4jWrapper/java/org/discordbots/api/discord4j/Discord4JPostCommandsTransformer.java b/src/discord4jWrapper/java/org/discordbots/api/discord4j/Discord4JPostCommandsTransformer.java
new file mode 100644
index 0000000..89fa7de
--- /dev/null
+++ b/src/discord4jWrapper/java/org/discordbots/api/discord4j/Discord4JPostCommandsTransformer.java
@@ -0,0 +1,37 @@
+package org.discordbots.api.discord4j;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import discord4j.common.JacksonResources;
+import discord4j.core.DiscordClient;
+import java.util.concurrent.CompletionStage;
+import org.discordbots.api.io.PostCommandsTransformer;
+
+public class Discord4JPostCommandsTransformer implements PostCommandsTransformer {
+ private final DiscordClient client;
+
+ public Discord4JPostCommandsTransformer(final DiscordClient client) {
+ this.client = client;
+ }
+
+ @Override
+ public CompletionStage toJsonString() {
+ return this.client
+ .getApplicationId()
+ .toFuture()
+ .thenCompose(
+ applicationId ->
+ this.client
+ .getApplicationService()
+ .getGlobalApplicationCommands(applicationId)
+ .collectList()
+ .toFuture())
+ .thenApply(
+ commands -> {
+ try {
+ return JacksonResources.create().getObjectMapper().writeValueAsString(commands);
+ } catch (final JsonProcessingException ignored) {
+ return "[]";
+ }
+ });
+ }
+}
diff --git a/src/dropwizardWebhooks/java/org/discordbots/webhooks/dropwizard/DBLWebhooks.java b/src/dropwizardWebhooks/java/org/discordbots/webhooks/dropwizard/DBLWebhooks.java
new file mode 100644
index 0000000..817c98e
--- /dev/null
+++ b/src/dropwizardWebhooks/java/org/discordbots/webhooks/dropwizard/DBLWebhooks.java
@@ -0,0 +1,121 @@
+package org.discordbots.webhooks.dropwizard;
+
+import com.fatboyindustrial.gsonjavatime.OffsetDateTimeConverter;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonIOException;
+import com.google.gson.JsonSyntaxException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.Response;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.time.OffsetDateTime;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HexFormat;
+import java.util.stream.Collectors;
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import org.discordbots.webhooks.payload.IntegrationCreatePayload;
+import org.discordbots.webhooks.payload.IntegrationDeletePayload;
+import org.discordbots.webhooks.payload.Payload;
+import org.discordbots.webhooks.payload.TestPayload;
+import org.discordbots.webhooks.payload.VoteCreatePayload;
+
+public abstract class DBLWebhooks implements DBLWebhooksListener {
+ private byte[] secret;
+ private final Gson gson;
+
+ public DBLWebhooks(final String secret) {
+ this.secret = secret.getBytes(StandardCharsets.UTF_8);
+ this.gson =
+ new GsonBuilder()
+ .registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeConverter())
+ .create();
+ }
+
+ public String getSecret() {
+ return new String(secret, StandardCharsets.UTF_8);
+ }
+
+ public void setSecret(final String newSecret) {
+ secret = newSecret.getBytes(StandardCharsets.UTF_8);
+ }
+
+ @POST
+ @SuppressWarnings("UseSpecificCatch")
+ public Response handle(@Context HttpServletRequest request) throws WebApplicationException {
+ try {
+ final String signatureHeader = request.getHeader("x-topgg-signature");
+
+ assert signatureHeader != null;
+
+ final HashMap parsedSignature =
+ Arrays.stream(signatureHeader.split(","))
+ .map(part -> part.split("=", 2))
+ .collect(
+ Collectors.toMap(
+ part -> part[0].trim(),
+ part -> part[1].trim(),
+ (existing, replacement) -> replacement,
+ HashMap::new));
+
+ final String signature = parsedSignature.get("v1");
+ final String timestamp = parsedSignature.get("t");
+
+ assert signature != null && timestamp != null;
+
+ final SecretKeySpec key = new SecretKeySpec(secret, "HmacSHA256");
+ final Mac hmac = Mac.getInstance("HmacSHA256");
+
+ hmac.init(key);
+
+ final String body =
+ new String(request.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
+ final byte[] digest =
+ hmac.doFinal(String.format("%s.%s", timestamp, body).getBytes(StandardCharsets.UTF_8));
+
+ if (!signature.equals(HexFormat.of().formatHex(digest))) {
+ return Response.status(Response.Status.UNAUTHORIZED)
+ .entity("Invalid Authorization")
+ .build();
+ }
+
+ final Payload payload = gson.fromJson(body, Payload.class);
+ final String trace = request.getHeader("x-topgg-trace");
+
+ try {
+ return switch (payload.getType()) {
+ case "integration.create" ->
+ onIntegrationCreate(payload.getData(gson, IntegrationCreatePayload.class), trace);
+ case "integration.delete" ->
+ onIntegrationDelete(payload.getData(gson, IntegrationDeletePayload.class), trace);
+ case "webhook.test" -> onTest(payload.getData(gson, TestPayload.class), trace);
+ case "vote.create" -> onVoteCreate(payload.getData(gson, VoteCreatePayload.class), trace);
+ default -> Response.status(Response.Status.BAD_REQUEST).entity("Bad Request").build();
+ };
+ } catch (final Throwable ignored) {
+ return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+ .entity("Internal Server Error")
+ .build();
+ }
+ } catch (final NoSuchAlgorithmException
+ | InvalidKeyException
+ | ArrayIndexOutOfBoundsException
+ | AssertionError
+ | JsonSyntaxException
+ | JsonIOException
+ | IOException error) {
+ if (error instanceof NoSuchAlgorithmException || error instanceof InvalidKeyException) {
+ throw new WebApplicationException("Unable to find HMAC SHA-256 algorithm", error);
+ } else {
+ return Response.status(Response.Status.BAD_REQUEST).entity("Bad Request").build();
+ }
+ }
+ }
+}
diff --git a/src/dropwizardWebhooks/java/org/discordbots/webhooks/dropwizard/DBLWebhooksListener.java b/src/dropwizardWebhooks/java/org/discordbots/webhooks/dropwizard/DBLWebhooksListener.java
new file mode 100644
index 0000000..5a76818
--- /dev/null
+++ b/src/dropwizardWebhooks/java/org/discordbots/webhooks/dropwizard/DBLWebhooksListener.java
@@ -0,0 +1,25 @@
+package org.discordbots.webhooks.dropwizard;
+
+import jakarta.ws.rs.core.Response;
+import org.discordbots.webhooks.payload.IntegrationCreatePayload;
+import org.discordbots.webhooks.payload.IntegrationDeletePayload;
+import org.discordbots.webhooks.payload.TestPayload;
+import org.discordbots.webhooks.payload.VoteCreatePayload;
+
+public interface DBLWebhooksListener {
+ default Response onIntegrationCreate(final IntegrationCreatePayload payload, final String trace) {
+ return Response.status(Response.Status.NO_CONTENT).build();
+ }
+
+ default Response onIntegrationDelete(final IntegrationDeletePayload payload, final String trace) {
+ return Response.status(Response.Status.NO_CONTENT).build();
+ }
+
+ default Response onTest(final TestPayload payload, final String trace) {
+ return Response.status(Response.Status.NO_CONTENT).build();
+ }
+
+ default Response onVoteCreate(final VoteCreatePayload payload, final String trace) {
+ return Response.status(Response.Status.NO_CONTENT).build();
+ }
+}
diff --git a/src/eclipseJettyWebhooks/java/org/discordbots/webhooks/eclipsejetty/DBLWebhooks.java b/src/eclipseJettyWebhooks/java/org/discordbots/webhooks/eclipsejetty/DBLWebhooks.java
new file mode 100644
index 0000000..c27d3f7
--- /dev/null
+++ b/src/eclipseJettyWebhooks/java/org/discordbots/webhooks/eclipsejetty/DBLWebhooks.java
@@ -0,0 +1,128 @@
+package org.discordbots.webhooks.eclipsejetty;
+
+import com.fatboyindustrial.gsonjavatime.OffsetDateTimeConverter;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonIOException;
+import com.google.gson.JsonSyntaxException;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.time.OffsetDateTime;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HexFormat;
+import java.util.stream.Collectors;
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import org.discordbots.webhooks.payload.IntegrationCreatePayload;
+import org.discordbots.webhooks.payload.IntegrationDeletePayload;
+import org.discordbots.webhooks.payload.Payload;
+import org.discordbots.webhooks.payload.TestPayload;
+import org.discordbots.webhooks.payload.VoteCreatePayload;
+
+public class DBLWebhooks extends HttpServlet implements DBLWebhooksListener {
+ private byte[] secret;
+ private final Gson gson;
+
+ public DBLWebhooks(final String secret) {
+ this.secret = secret.getBytes(StandardCharsets.UTF_8);
+ this.gson =
+ new GsonBuilder()
+ .registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeConverter())
+ .create();
+ }
+
+ public String getSecret() {
+ return new String(secret, StandardCharsets.UTF_8);
+ }
+
+ public void setSecret(final String newSecret) {
+ secret = newSecret.getBytes(StandardCharsets.UTF_8);
+ }
+
+ @Override
+ @SuppressWarnings("UseSpecificCatch")
+ protected void doPost(final HttpServletRequest request, final HttpServletResponse response)
+ throws IOException, ServletException {
+ try {
+ final String signatureHeader = request.getHeader("x-topgg-signature");
+
+ assert signatureHeader != null;
+
+ final HashMap parsedSignature =
+ Arrays.stream(signatureHeader.split(","))
+ .map(part -> part.split("=", 2))
+ .collect(
+ Collectors.toMap(
+ part -> part[0].trim(),
+ part -> part[1].trim(),
+ (existing, replacement) -> replacement,
+ HashMap::new));
+
+ final String signature = parsedSignature.get("v1");
+ final String timestamp = parsedSignature.get("t");
+
+ assert signature != null && timestamp != null;
+
+ final SecretKeySpec key = new SecretKeySpec(secret, "HmacSHA256");
+ final Mac hmac = Mac.getInstance("HmacSHA256");
+
+ hmac.init(key);
+
+ final String body =
+ new String(request.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
+ final byte[] digest =
+ hmac.doFinal(String.format("%s.%s", timestamp, body).getBytes(StandardCharsets.UTF_8));
+
+ if (!signature.equals(HexFormat.of().formatHex(digest))) {
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ response.getWriter().write("Invalid Authorization");
+
+ return;
+ }
+
+ final Payload payload = gson.fromJson(body, Payload.class);
+ final String trace = request.getHeader("x-topgg-trace");
+
+ try {
+ switch (payload.getType()) {
+ case "integration.create" ->
+ onIntegrationCreate(
+ response, payload.getData(gson, IntegrationCreatePayload.class), trace);
+ case "integration.delete" ->
+ onIntegrationDelete(
+ response, payload.getData(gson, IntegrationDeletePayload.class), trace);
+ case "webhook.test" -> onTest(response, payload.getData(gson, TestPayload.class), trace);
+ case "vote.create" ->
+ onVoteCreate(response, payload.getData(gson, VoteCreatePayload.class), trace);
+ default -> {
+ response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ response.getWriter().write("Bad Request");
+ }
+ }
+ } catch (final Throwable ignored) {
+ response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ response.getWriter().write("Internal Server Error");
+ }
+ } catch (final NoSuchAlgorithmException
+ | InvalidKeyException
+ | ArrayIndexOutOfBoundsException
+ | AssertionError
+ | JsonSyntaxException
+ | JsonIOException
+ | IOException error) {
+ if (error instanceof NoSuchAlgorithmException || error instanceof InvalidKeyException) {
+ throw new ServletException("Unable to find HMAC SHA-256 algorithm", error);
+ } else {
+ response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ response.getWriter().write("Bad Request");
+ }
+ }
+ }
+}
diff --git a/src/eclipseJettyWebhooks/java/org/discordbots/webhooks/eclipsejetty/DBLWebhooksListener.java b/src/eclipseJettyWebhooks/java/org/discordbots/webhooks/eclipsejetty/DBLWebhooksListener.java
new file mode 100644
index 0000000..cee3a90
--- /dev/null
+++ b/src/eclipseJettyWebhooks/java/org/discordbots/webhooks/eclipsejetty/DBLWebhooksListener.java
@@ -0,0 +1,33 @@
+package org.discordbots.webhooks.eclipsejetty;
+
+import jakarta.servlet.http.HttpServletResponse;
+import org.discordbots.webhooks.payload.IntegrationCreatePayload;
+import org.discordbots.webhooks.payload.IntegrationDeletePayload;
+import org.discordbots.webhooks.payload.TestPayload;
+import org.discordbots.webhooks.payload.VoteCreatePayload;
+
+public interface DBLWebhooksListener {
+ default void onIntegrationCreate(
+ final HttpServletResponse response,
+ final IntegrationCreatePayload payload,
+ final String trace) {
+ response.setStatus(HttpServletResponse.SC_NO_CONTENT);
+ }
+
+ default void onIntegrationDelete(
+ final HttpServletResponse response,
+ final IntegrationDeletePayload payload,
+ final String trace) {
+ response.setStatus(HttpServletResponse.SC_NO_CONTENT);
+ }
+
+ default void onTest(
+ final HttpServletResponse response, final TestPayload payload, final String trace) {
+ response.setStatus(HttpServletResponse.SC_NO_CONTENT);
+ }
+
+ default void onVoteCreate(
+ final HttpServletResponse response, final VoteCreatePayload payload, final String trace) {
+ response.setStatus(HttpServletResponse.SC_NO_CONTENT);
+ }
+}
diff --git a/src/jdaWrapper/java/org/discordbots/api/jda/JDAPostCommandsTransformer.java b/src/jdaWrapper/java/org/discordbots/api/jda/JDAPostCommandsTransformer.java
new file mode 100644
index 0000000..471094f
--- /dev/null
+++ b/src/jdaWrapper/java/org/discordbots/api/jda/JDAPostCommandsTransformer.java
@@ -0,0 +1,34 @@
+package org.discordbots.api.jda;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonParser;
+import java.util.concurrent.CompletionStage;
+import net.dv8tion.jda.api.JDA;
+import net.dv8tion.jda.api.interactions.commands.Command;
+import net.dv8tion.jda.api.interactions.commands.build.CommandData;
+import org.discordbots.api.io.PostCommandsTransformer;
+
+public class JDAPostCommandsTransformer implements PostCommandsTransformer {
+ private final JDA jda;
+
+ public JDAPostCommandsTransformer(final JDA jda) {
+ this.jda = jda;
+ }
+
+ @Override
+ public CompletionStage toJsonString() {
+ return jda.retrieveCommands()
+ .submit()
+ .thenApply(
+ commands -> {
+ final JsonArray object = new JsonArray();
+
+ for (final Command command : commands) {
+ object.add(
+ JsonParser.parseString(CommandData.fromCommand(command).toData().toString()));
+ }
+
+ return object.toString();
+ });
+ }
+}
diff --git a/src/main/java/org/discordbots/api/DBLAPI.java b/src/main/java/org/discordbots/api/DBLAPI.java
new file mode 100644
index 0000000..2fe5abf
--- /dev/null
+++ b/src/main/java/org/discordbots/api/DBLAPI.java
@@ -0,0 +1,192 @@
+package org.discordbots.api;
+
+import com.fatboyindustrial.gsonjavatime.OffsetDateTimeConverter;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import java.io.IOException;
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.TemporalAccessor;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.CompletionStage;
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.HttpUrl;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.discordbots.api.entity.PaginatedVotes;
+import org.discordbots.api.entity.PartialVote;
+import org.discordbots.api.entity.Project;
+import org.discordbots.api.entity.UserSource;
+import org.discordbots.api.io.DefaultResponseTransformer;
+import org.discordbots.api.io.EmptyResponseTransformer;
+import org.discordbots.api.io.PaginatedVotesConverter;
+import org.discordbots.api.io.PostCommandsTransformer;
+import org.discordbots.api.io.RawPostCommandsTransformer;
+import org.discordbots.api.io.ResponseTransformer;
+import org.discordbots.api.io.UnsuccessfulHttpException;
+
+public class DBLAPI {
+ private static final HttpUrl BASE_URL =
+ new HttpUrl.Builder()
+ .scheme("https")
+ .host("top.gg")
+ .addPathSegment("api")
+ .addPathSegment("v1")
+ .build();
+
+ private final OkHttpClient httpClient;
+ private final Gson gson;
+
+ public DBLAPI(final OkHttpClient httpClient) {
+ this.gson =
+ new GsonBuilder()
+ .registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeConverter())
+ .registerTypeAdapter(PaginatedVotes.class, new PaginatedVotesConverter(this))
+ .create();
+
+ this.httpClient = httpClient;
+ }
+
+ public DBLAPI(final String token) {
+ this(
+ new OkHttpClient.Builder()
+ .addInterceptor(
+ (chain) ->
+ chain.proceed(
+ chain
+ .request()
+ .newBuilder()
+ .addHeader("Authorization", "Bearer " + token)
+ .build()))
+ .build());
+ }
+
+ public CompletionStage getSelf() {
+ final HttpUrl url =
+ BASE_URL.newBuilder().addPathSegment("projects").addPathSegment("@me").build();
+
+ return get(url, Project.class);
+ }
+
+ public CompletionStage postCommands(final PostCommandsTransformer commands) {
+ final HttpUrl url =
+ BASE_URL
+ .newBuilder()
+ .addPathSegment("projects")
+ .addPathSegment("@me")
+ .addPathSegment("commands")
+ .build();
+
+ return commands
+ .toJsonString()
+ .thenCompose(jsonBody -> post(url, jsonBody, new EmptyResponseTransformer()));
+ }
+
+ public CompletionStage postCommands(final JsonArray commands) {
+ return postCommands(new RawPostCommandsTransformer(commands));
+ }
+
+ public CompletionStage getVote(final UserSource userSource, final String id) {
+ final HttpUrl url =
+ BASE_URL
+ .newBuilder()
+ .addPathSegment("projects")
+ .addPathSegment("@me")
+ .addPathSegment("votes")
+ .addPathSegment(id)
+ .addQueryParameter("source", gson.toJson(userSource))
+ .build();
+
+ return get(url, PartialVote.class)
+ .exceptionally(
+ error -> {
+ if (error instanceof UnsuccessfulHttpException
+ && ((UnsuccessfulHttpException) error).getResponse().code() == 404) {
+ return null;
+ }
+
+ throw new CompletionException(error);
+ });
+ }
+
+ public CompletionStage getVotes(final TemporalAccessor since) {
+ final HttpUrl url =
+ BASE_URL
+ .newBuilder()
+ .addPathSegment("projects")
+ .addPathSegment("@me")
+ .addPathSegment("votes")
+ .addQueryParameter("startDate", DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(since))
+ .build();
+
+ return get(url, PaginatedVotes.class);
+ }
+
+ public CompletionStage getVotes(final String cursor) {
+ final HttpUrl url =
+ BASE_URL
+ .newBuilder()
+ .addPathSegment("projects")
+ .addPathSegment("@me")
+ .addPathSegment("votes")
+ .addQueryParameter("cursor", cursor)
+ .build();
+
+ return get(url, PaginatedVotes.class);
+ }
+
+ private CompletionStage get(final HttpUrl url, final Class aClass) {
+ return get(url, new DefaultResponseTransformer<>(aClass, gson));
+ }
+
+ private CompletionStage get(
+ final HttpUrl url, final ResponseTransformer responseTransformer) {
+ return execute(new Request.Builder().get().url(url).build(), responseTransformer);
+ }
+
+ private CompletionStage post(
+ final HttpUrl url, final String jsonBody, final ResponseTransformer responseTransformer) {
+ final RequestBody body = RequestBody.create(jsonBody, MediaType.parse("application/json"));
+ final Request req = new Request.Builder().post(body).url(url).build();
+
+ return execute(req, responseTransformer);
+ }
+
+ private CompletionStage execute(
+ final Request request, final ResponseTransformer responseTransformer) {
+ final Call call = httpClient.newCall(request);
+ final CompletableFuture future = new CompletableFuture<>();
+
+ call.enqueue(
+ new Callback() {
+ @Override
+ public void onFailure(final Call call, final IOException error) {
+ future.completeExceptionally(error);
+ }
+
+ @Override
+ @SuppressWarnings("UseSpecificCatch")
+ public void onResponse(final Call call, final Response response) {
+ try {
+ if (response.isSuccessful()) {
+ future.complete(responseTransformer.transform(response));
+ } else {
+ future.completeExceptionally(new UnsuccessfulHttpException(response));
+ }
+ } catch (final Throwable error) {
+ future.completeExceptionally(error);
+ } finally {
+ response.body().close();
+ }
+ }
+ });
+
+ return future;
+ }
+}
diff --git a/src/main/java/org/discordbots/api/DBLWidget.java b/src/main/java/org/discordbots/api/DBLWidget.java
new file mode 100644
index 0000000..f724dc8
--- /dev/null
+++ b/src/main/java/org/discordbots/api/DBLWidget.java
@@ -0,0 +1,23 @@
+package org.discordbots.api;
+
+import org.discordbots.api.entity.ProjectType;
+
+public final class DBLWidget {
+ private static final String BASE_URL = "https://top.gg/api/v1/widgets";
+
+ public static String large(final ProjectType projectType, final String id) {
+ return BASE_URL + "/large/" + projectType.asWidgetPath() + "/" + id;
+ }
+
+ public static String votes(final ProjectType projectType, final String id) {
+ return BASE_URL + "/small/votes/" + projectType.asWidgetPath() + "/" + id;
+ }
+
+ public static String owner(final ProjectType projectType, final String id) {
+ return BASE_URL + "/small/owner/" + projectType.asWidgetPath() + "/" + id;
+ }
+
+ public static String social(final ProjectType projectType, final String id) {
+ return BASE_URL + "/small/social/" + projectType.asWidgetPath() + "/" + id;
+ }
+}
diff --git a/src/main/java/org/discordbots/api/client/DiscordBotListAPI.java b/src/main/java/org/discordbots/api/client/DiscordBotListAPI.java
deleted file mode 100644
index 1dbd21c..0000000
--- a/src/main/java/org/discordbots/api/client/DiscordBotListAPI.java
+++ /dev/null
@@ -1,59 +0,0 @@
-package org.discordbots.api.client;
-
-import org.discordbots.api.client.entity.*;
-import org.discordbots.api.client.impl.DiscordBotListAPIImpl;
-
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.CompletionStage;
-
-public interface DiscordBotListAPI {
-
- CompletionStage setStats(int shardId, int shardTotal, int serverCount);
- CompletionStage setStats(List shardServerCounts);
- CompletionStage setStats(int serverCount);
-
- CompletionStage getStats(String botId);
-
- @Deprecated
- CompletionStage> getVoters(String botId);
- CompletionStage hasVoted(String userId);
-
- CompletionStage getBots(Map search, int limit, int offset);
- CompletionStage getBots(Map search, int limit, int offset, String sort);
- CompletionStage getBots(Map search, int limit, int offset, String sort, List fields);
- CompletionStage getBot(String botId);
-
- CompletionStage getUser(String userId);
-
- CompletionStage getVotingMultiplier();
-
- class Builder {
-
- // Required
- private String botId = null;
- private String token = null;
-
- public Builder token(String token) {
- this.token = token;
- return this;
- }
-
- public Builder botId(String botId) {
- this.botId = botId;
- return this;
- }
-
- public DiscordBotListAPI build() {
- if(token == null)
- throw new IllegalArgumentException("The provided token cannot be null!");
-
- if(botId == null)
- throw new IllegalArgumentException("The provided bot ID cannot be null!");
-
- return new DiscordBotListAPIImpl(token, botId);
- }
-
- }
-
-}
diff --git a/src/main/java/org/discordbots/api/client/entity/Bot.java b/src/main/java/org/discordbots/api/client/entity/Bot.java
deleted file mode 100644
index 0576dd9..0000000
--- a/src/main/java/org/discordbots/api/client/entity/Bot.java
+++ /dev/null
@@ -1,144 +0,0 @@
-package org.discordbots.api.client.entity;
-
-import com.google.gson.annotations.SerializedName;
-
-import java.time.OffsetDateTime;
-import java.util.List;
-
-public class Bot {
-
- private String id;
- @SerializedName("clientid")
- private String clientId;
- private String username;
- private String discriminator;
-
- private String avatar;
- @SerializedName("defAvatar")
- private String defaultAvatar;
-
- private String prefix;
- private String invite;
- private String website;
- private String vanity;
- private String support;
- private List tags;
-
- @SerializedName("longdesc")
- private String longDescription;
- @SerializedName("shortdesc")
- private String shortDescription;
- @SerializedName("betadesc")
- private String betaDescription;
-
- @SerializedName("certifiedBot")
- private boolean certified;
-
- @SerializedName("date") // rename so that the naming actually makes sense
- private OffsetDateTime approvalTime;
-
- @SerializedName("server_count")
- private long serverCount;
-
- private List guilds;
- private List shards;
- private int monthlyPoints;
- private int points;
-
- private boolean legacy;
-
-
-
- public String getId() {
- return id;
- }
-
- public String getClientId() {
- return clientId;
- }
-
- public String getUsername() {
- return username;
- }
-
- public String getDiscriminator() {
- return discriminator;
- }
-
- public String getAvatar() {
- return avatar;
- }
-
- public String getDefaultAvatar() {
- return defaultAvatar;
- }
-
- public String getPrefix() {
- return prefix;
- }
-
- public String getInvite() {
- return invite;
- }
-
- public String getWebsite() {
- return website;
- }
-
- public String getVanity() {
- return vanity;
- }
-
- public String getSupport() {
- return support;
- }
-
- public List getTags() {
- return tags;
- }
-
- public String getLongDescription() {
- return longDescription;
- }
-
- public String getShortDescription() {
- return shortDescription;
- }
-
- public String getBetaDescription() {
- return betaDescription;
- }
-
- public boolean isCertified() {
- return certified;
- }
-
- public OffsetDateTime getApprovalTime() {
- return approvalTime;
- }
-
- public long getServerCount() {
- return serverCount;
- }
-
- public List getGuilds() {
- return guilds;
- }
-
- public List getShards() {
- return shards;
- }
-
- public int getMonthlyPoints() {
- return monthlyPoints;
- }
-
- public int getPoints() {
- return points;
- }
-
- public boolean isLegacy() {
- return legacy;
- }
-
-}
diff --git a/src/main/java/org/discordbots/api/client/entity/BotResult.java b/src/main/java/org/discordbots/api/client/entity/BotResult.java
deleted file mode 100644
index 7581b60..0000000
--- a/src/main/java/org/discordbots/api/client/entity/BotResult.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package org.discordbots.api.client.entity;
-
-// This class is needed because of the way Java generics work. I can't reference Result as a class
-// so this is just a simple workaround for that
-public class BotResult extends Result {}
diff --git a/src/main/java/org/discordbots/api/client/entity/BotStats.java b/src/main/java/org/discordbots/api/client/entity/BotStats.java
deleted file mode 100644
index 209edfd..0000000
--- a/src/main/java/org/discordbots/api/client/entity/BotStats.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package org.discordbots.api.client.entity;
-
-import com.google.gson.annotations.SerializedName;
-
-import java.util.Collections;
-import java.util.List;
-
-public class BotStats {
-
- @SerializedName("server_count")
- private int serverCount;
- private List shards;
-
- public int getServerCount() { return serverCount; }
- public List getShards() { return Collections.unmodifiableList(shards); }
-}
diff --git a/src/main/java/org/discordbots/api/client/entity/Result.java b/src/main/java/org/discordbots/api/client/entity/Result.java
deleted file mode 100644
index 1dd0130..0000000
--- a/src/main/java/org/discordbots/api/client/entity/Result.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package org.discordbots.api.client.entity;
-
-import java.util.List;
-
-public class Result {
-
- private List results;
- private int limit, offset, count, total;
-
-
-
- public List getResults() {
- return results;
- }
-
- public int getLimit() {
- return limit;
- }
-
- public int getOffset() {
- return offset;
- }
-
- public int getCount() {
- return count;
- }
-
- public int getTotal() {
- return total;
- }
-
-}
diff --git a/src/main/java/org/discordbots/api/client/entity/SimpleUser.java b/src/main/java/org/discordbots/api/client/entity/SimpleUser.java
deleted file mode 100644
index 562f0ed..0000000
--- a/src/main/java/org/discordbots/api/client/entity/SimpleUser.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package org.discordbots.api.client.entity;
-
-public class SimpleUser {
-
- private String id;
- private String username;
- private String discriminator;
-
- private String avatar;
-
-
-
- public String getId() {
- return id;
- }
-
- public String getUsername() {
- return username;
- }
-
- public String getDiscriminator() {
- return discriminator;
- }
-
- public String getAvatar() {
- return avatar;
- }
-
-}
diff --git a/src/main/java/org/discordbots/api/client/entity/Social.java b/src/main/java/org/discordbots/api/client/entity/Social.java
deleted file mode 100644
index d7a64e0..0000000
--- a/src/main/java/org/discordbots/api/client/entity/Social.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package org.discordbots.api.client.entity;
-
-public class Social {
-
- String youtube, reddit, twitter, instagram, github;
-
-}
diff --git a/src/main/java/org/discordbots/api/client/entity/User.java b/src/main/java/org/discordbots/api/client/entity/User.java
deleted file mode 100644
index bfed556..0000000
--- a/src/main/java/org/discordbots/api/client/entity/User.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package org.discordbots.api.client.entity;
-
-import com.google.gson.annotations.SerializedName;
-
-public class User extends SimpleUser {
-
- @SerializedName("defAvatar")
- private String defaultAvatar;
-
- private boolean admin, mod, webMod;
- private boolean artist, certifiedDev, supporter;
-
- private Social social;
-
-
-
- public String getDefaultAvatar() {
- return defaultAvatar;
- }
-
- public boolean isAdmin() {
- return admin;
- }
-
- public boolean isMod() {
- return mod;
- }
-
- public boolean isWebMod() {
- return webMod;
- }
-
- public boolean isArtist() {
- return artist;
- }
-
- public boolean isCertifiedDev() {
- return certifiedDev;
- }
-
- public boolean isSupporter() {
- return supporter;
- }
-
- public Social getSocial() {
- return social;
- }
-
-}
diff --git a/src/main/java/org/discordbots/api/client/entity/Vote.java b/src/main/java/org/discordbots/api/client/entity/Vote.java
deleted file mode 100644
index 047f6de..0000000
--- a/src/main/java/org/discordbots/api/client/entity/Vote.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package org.discordbots.api.client.entity;
-
-import com.google.gson.annotations.SerializedName;
-
-public class Vote {
-
- @SerializedName("bot")
- private String botId;
- @SerializedName("user")
- private String userId;
-
- private String type;
-
- private String query;
-
- @SerializedName("isWeekend")
- private boolean weekend;
-
-
-
- public String getBotId() {
- return botId;
- }
-
- public String getUserId() {
- return userId;
- }
-
- public String getType() {
- return type;
- }
-
- public String getQuery() {
- return query;
- }
-
- public boolean isWeekend() {
- return weekend;
- }
-
-}
diff --git a/src/main/java/org/discordbots/api/client/entity/VotingMultiplier.java b/src/main/java/org/discordbots/api/client/entity/VotingMultiplier.java
deleted file mode 100644
index 6f927fe..0000000
--- a/src/main/java/org/discordbots/api/client/entity/VotingMultiplier.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package org.discordbots.api.client.entity;
-
-import com.google.gson.annotations.SerializedName;
-
-public class VotingMultiplier {
-
- @SerializedName("is_weekend")
- private boolean weekend;
-
-
-
- public boolean isWeekend() {
- return weekend;
- }
-
-}
diff --git a/src/main/java/org/discordbots/api/client/impl/DiscordBotListAPIImpl.java b/src/main/java/org/discordbots/api/client/impl/DiscordBotListAPIImpl.java
deleted file mode 100644
index 546df9a..0000000
--- a/src/main/java/org/discordbots/api/client/impl/DiscordBotListAPIImpl.java
+++ /dev/null
@@ -1,264 +0,0 @@
-package org.discordbots.api.client.impl;
-
-import com.fatboyindustrial.gsonjavatime.OffsetDateTimeConverter;
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import okhttp3.*;
-import org.discordbots.api.client.DiscordBotListAPI;
-import org.discordbots.api.client.entity.*;
-import org.discordbots.api.client.io.DefaultResponseTransformer;
-import org.discordbots.api.client.io.ResponseTransformer;
-import org.discordbots.api.client.io.UnsuccessfulHttpException;
-import org.json.JSONObject;
-
-import java.io.IOException;
-import java.time.OffsetDateTime;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionStage;
-import java.util.stream.Collectors;
-
-public class DiscordBotListAPIImpl implements DiscordBotListAPI {
-
- private static final HttpUrl baseUrl = new HttpUrl.Builder()
- .scheme("https")
- .host("top.gg")
- .addPathSegment("api")
- .build();
-
- private final OkHttpClient httpClient;
- private final Gson gson;
-
- private final String token, botId;
-
- public DiscordBotListAPIImpl(String token, String botId) {
- this.token = token;
- this.botId = botId;
-
- this.gson = new GsonBuilder()
- .registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeConverter())
- .create();
-
- this.httpClient = new OkHttpClient.Builder()
- .addInterceptor((chain) -> {
- Request req = chain.request().newBuilder()
- .addHeader("Authorization", this.token)
- .build();
- return chain.proceed(req);
- })
- .build();
- }
-
- public CompletionStage setStats(int shardId, int shardTotal, int serverCount) {
- JSONObject json = new JSONObject()
- .put("shard_id", shardId)
- .put("shard_count", shardTotal)
- .put("server_count", serverCount);
-
- return setStats(json);
- }
-
- public CompletionStage setStats(List shardServerCounts) {
- JSONObject json = new JSONObject()
- .put("shards", shardServerCounts);
-
- return setStats(json);
- }
-
- public CompletionStage setStats(int serverCount) {
- JSONObject json = new JSONObject()
- .put("server_count", serverCount);
-
- return setStats(json);
- }
-
- private CompletionStage setStats(JSONObject jsonBody) {
- HttpUrl url = baseUrl.newBuilder()
- .addPathSegment("bots")
- .addPathSegment(botId)
- .addPathSegment("stats")
- .build();
-
- return post(url, jsonBody, Void.class);
- }
-
- public CompletionStage getStats(String botId) {
- HttpUrl url = baseUrl.newBuilder()
- .addPathSegment("bots")
- .addPathSegment(botId)
- .addPathSegment("stats")
- .build();
-
- return get(url, BotStats.class);
- }
-
- public CompletionStage> getVoters(String botId) {
- HttpUrl url = baseUrl.newBuilder()
- .addPathSegment("bots")
- .addPathSegment(botId)
- .addPathSegment("votes")
- .build();
-
- return get(url, resp -> {
- // This is kinda awkward but this is done so that it can return it was a list instead of
- // an array
- ResponseTransformer arrayTransformer = new DefaultResponseTransformer<>(SimpleUser[].class, gson);
- return Arrays.asList(arrayTransformer.transform(resp));
- });
- }
-
- public CompletionStage getBot(String botId) {
- HttpUrl url = baseUrl.newBuilder()
- .addPathSegment("bots")
- .addPathSegment(botId)
- .build();
-
- return get(url, Bot.class);
- }
-
- public CompletionStage getBots(Map search, int limit, int offset) {
- return getBots(search, limit, offset, null);
- }
-
- public CompletionStage getBots(Map search, int limit, int offset, String sort) {
- return getBots(search, limit, offset, sort, null);
- }
-
- public CompletionStage getBots(Map search, int limit, int offset, String sort, List fields) {
- // DBL search uses this format: field1: value1 field2: value2
- String searchString = search.entrySet().stream()
- .map(entry -> entry.getKey() + ": " + entry.getValue())
- .collect(Collectors.joining(" "));
-
- HttpUrl.Builder urlBuilder = baseUrl.newBuilder()
- .addPathSegment("bots")
- .addQueryParameter("search", searchString)
- .addQueryParameter("limit", String.valueOf(limit))
- .addQueryParameter("offset", String.valueOf(offset));
-
- if(sort != null) {
- urlBuilder.addQueryParameter("sort", sort);
- }
-
- if(fields != null) {
- String fieldsString = fields.stream()
- .collect(Collectors.joining(" "));
-
- urlBuilder.addQueryParameter("fields", fieldsString);
- }
-
- return get(urlBuilder.build(), BotResult.class);
- }
-
- public CompletionStage getUser(String userId) {
- HttpUrl url = baseUrl.newBuilder()
- .addPathSegment("users")
- .addPathSegment(userId)
- .build();
-
- return get(url, User.class);
- }
-
- public CompletionStage hasVoted(String userId) {
- HttpUrl url = baseUrl.newBuilder()
- .addPathSegment("bots")
- .addPathSegment(botId)
- .addPathSegment("check")
- .addQueryParameter("userId", userId)
- .build();
-
- return get(url, (resp) -> {
- JSONObject json = new JSONObject(resp.body().string());
- return json.getInt("voted") == 1;
- });
- }
-
- public CompletionStage getVotingMultiplier() {
- HttpUrl url = baseUrl.newBuilder()
- .addPathSegment("weekend")
- .build();
-
- return get(url, VotingMultiplier.class);
- }
-
- private CompletionStage get(HttpUrl url, Class aClass) {
- return get(url, new DefaultResponseTransformer<>(aClass, gson));
- }
-
- private CompletionStage get(HttpUrl url, ResponseTransformer responseTransformer) {
- Request req = new Request.Builder()
- .get()
- .url(url)
- .build();
-
- return execute(req, responseTransformer);
- }
-
- // The class provided in this is kinda unneeded because the only thing ever given to it
- // is Void, but I wanted to make it expandable (maybe some post methods will return objects
- // in the future)
- private CompletionStage post(HttpUrl url, JSONObject jsonBody, Class aClass) {
- return post(url, jsonBody, new DefaultResponseTransformer<>(aClass, gson));
- }
-
- private CompletionStage post(HttpUrl url, JSONObject jsonBody, ResponseTransformer responseTransformer) {
- MediaType mediaType = MediaType.parse("application/json");
- RequestBody body = RequestBody.create(mediaType, jsonBody.toString());
-
- Request req = new Request.Builder()
- .post(body)
- .url(url)
- .build();
-
- return execute(req, responseTransformer);
- }
-
- private CompletionStage execute(Request request, ResponseTransformer responseTransformer) {
- Call call = httpClient.newCall(request);
-
- final CompletableFuture future = new CompletableFuture<>();
-
- call.enqueue(new Callback() {
- @Override
- public void onFailure(Call call, IOException e) {
- future.completeExceptionally(e);
- }
-
- @Override
- public void onResponse(Call call, Response response) {
- try {
-
- if (response.isSuccessful()) {
- E transformed = responseTransformer.transform(response);
- future.complete(transformed);
- } else {
- String message = response.message();
-
- // DBL sends error messages as part of the body and leaves the
- // actual message blank so this will just pull that instead because
- // it's 1000x more useful than the actual message
- if (message == null || message.isEmpty()) {
- try {
- JSONObject body = new JSONObject(response.body().string());
- message = body.getString("error");
- } catch (Exception ignored) {}
- }
-
- Exception e = new UnsuccessfulHttpException(response.code(), message);
- future.completeExceptionally(e);
- }
-
- } catch (Exception e) {
- future.completeExceptionally(e);
- } finally {
- response.body().close();
- }
- }
- });
-
- return future;
- }
-
-}
diff --git a/src/main/java/org/discordbots/api/client/io/DefaultResponseTransformer.java b/src/main/java/org/discordbots/api/client/io/DefaultResponseTransformer.java
deleted file mode 100644
index 8639e10..0000000
--- a/src/main/java/org/discordbots/api/client/io/DefaultResponseTransformer.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package org.discordbots.api.client.io;
-
-import com.google.gson.Gson;
-import okhttp3.Response;
-
-import java.io.IOException;
-
-public class DefaultResponseTransformer implements ResponseTransformer {
-
- private final Class aClass;
- private final Gson gson;
-
- public DefaultResponseTransformer(Class aClass, Gson gson) {
- this.aClass = aClass;
- this.gson = gson;
- }
-
- @Override
- public E transform(Response response) throws IOException {
- String body = response.body().string();
- return gson.fromJson(body, aClass);
- }
-
-}
diff --git a/src/main/java/org/discordbots/api/client/io/ResponseTransformer.java b/src/main/java/org/discordbots/api/client/io/ResponseTransformer.java
deleted file mode 100644
index 730b783..0000000
--- a/src/main/java/org/discordbots/api/client/io/ResponseTransformer.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package org.discordbots.api.client.io;
-
-import okhttp3.Response;
-
-public interface ResponseTransformer {
-
- E transform(Response response) throws Exception;
-
-}
diff --git a/src/main/java/org/discordbots/api/client/io/UnsuccessfulHttpException.java b/src/main/java/org/discordbots/api/client/io/UnsuccessfulHttpException.java
deleted file mode 100644
index ae8a294..0000000
--- a/src/main/java/org/discordbots/api/client/io/UnsuccessfulHttpException.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package org.discordbots.api.client.io;
-
-public class UnsuccessfulHttpException extends Exception {
-
- public UnsuccessfulHttpException(int code, String message) {
- super("The server responded with code: " + code + ", message: " + message);
- }
-
-}
diff --git a/src/main/java/org/discordbots/api/entity/PaginatedVotes.java b/src/main/java/org/discordbots/api/entity/PaginatedVotes.java
new file mode 100644
index 0000000..8344493
--- /dev/null
+++ b/src/main/java/org/discordbots/api/entity/PaginatedVotes.java
@@ -0,0 +1,25 @@
+package org.discordbots.api.entity;
+
+import java.util.List;
+import java.util.concurrent.CompletionStage;
+import org.discordbots.api.DBLAPI;
+
+public class PaginatedVotes {
+ private final List votes;
+ private final String cursor;
+ private final DBLAPI client;
+
+ public PaginatedVotes(final List votes, final String cursor, final DBLAPI client) {
+ this.votes = votes;
+ this.cursor = cursor;
+ this.client = client;
+ }
+
+ public List getVotes() {
+ return votes;
+ }
+
+ public CompletionStage next() {
+ return client.getVotes(cursor);
+ }
+}
diff --git a/src/main/java/org/discordbots/api/entity/PartialVote.java b/src/main/java/org/discordbots/api/entity/PartialVote.java
new file mode 100644
index 0000000..f27c90b
--- /dev/null
+++ b/src/main/java/org/discordbots/api/entity/PartialVote.java
@@ -0,0 +1,26 @@
+package org.discordbots.api.entity;
+
+import com.google.gson.annotations.SerializedName;
+import java.time.OffsetDateTime;
+
+public class PartialVote {
+ @SerializedName("created_at")
+ private OffsetDateTime votedAt;
+
+ @SerializedName("expires_at")
+ private OffsetDateTime expiresAt;
+
+ private int weight;
+
+ public OffsetDateTime getVotedAt() {
+ return votedAt;
+ }
+
+ public OffsetDateTime getExpiresAt() {
+ return expiresAt;
+ }
+
+ public int getWeight() {
+ return weight;
+ }
+}
diff --git a/src/main/java/org/discordbots/api/entity/Platform.java b/src/main/java/org/discordbots/api/entity/Platform.java
new file mode 100644
index 0000000..cd01779
--- /dev/null
+++ b/src/main/java/org/discordbots/api/entity/Platform.java
@@ -0,0 +1,8 @@
+package org.discordbots.api.entity;
+
+import com.google.gson.annotations.SerializedName;
+
+public enum Platform {
+ @SerializedName("discord")
+ DISCORD
+}
diff --git a/src/main/java/org/discordbots/api/entity/Project.java b/src/main/java/org/discordbots/api/entity/Project.java
new file mode 100644
index 0000000..4256280
--- /dev/null
+++ b/src/main/java/org/discordbots/api/entity/Project.java
@@ -0,0 +1,65 @@
+package org.discordbots.api.entity;
+
+import com.google.gson.annotations.SerializedName;
+import java.util.List;
+
+public class Project {
+ private String id;
+ private String name;
+ private Platform platform;
+ private ProjectType type;
+ private String headline;
+ private List tags;
+
+ @SerializedName("votes")
+ private long currentVotes;
+
+ @SerializedName("votes_total")
+ private long totalVotes;
+
+ @SerializedName("review_score")
+ private float reviewScore;
+
+ @SerializedName("review_count")
+ private long reviewCount;
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Platform getPlatform() {
+ return platform;
+ }
+
+ public ProjectType getType() {
+ return type;
+ }
+
+ public String getHeadline() {
+ return headline;
+ }
+
+ public List getTags() {
+ return tags;
+ }
+
+ public long getCurrentVotes() {
+ return currentVotes;
+ }
+
+ public long getTotalVotes() {
+ return totalVotes;
+ }
+
+ public float getReviewScore() {
+ return reviewScore;
+ }
+
+ public long getReviewCount() {
+ return reviewCount;
+ }
+}
diff --git a/src/main/java/org/discordbots/api/entity/ProjectType.java b/src/main/java/org/discordbots/api/entity/ProjectType.java
new file mode 100644
index 0000000..367f34b
--- /dev/null
+++ b/src/main/java/org/discordbots/api/entity/ProjectType.java
@@ -0,0 +1,18 @@
+package org.discordbots.api.entity;
+
+import com.google.gson.annotations.SerializedName;
+
+public enum ProjectType {
+ @SerializedName("bot")
+ DISCORD_BOT,
+
+ @SerializedName("server")
+ DISCORD_SERVER;
+
+ public String asWidgetPath() {
+ return switch (this) {
+ case ProjectType.DISCORD_BOT -> "discord/bot";
+ case ProjectType.DISCORD_SERVER -> "discord/server";
+ };
+ }
+}
diff --git a/src/main/java/org/discordbots/api/entity/UserSource.java b/src/main/java/org/discordbots/api/entity/UserSource.java
new file mode 100644
index 0000000..effca21
--- /dev/null
+++ b/src/main/java/org/discordbots/api/entity/UserSource.java
@@ -0,0 +1,11 @@
+package org.discordbots.api.entity;
+
+import com.google.gson.annotations.SerializedName;
+
+public enum UserSource {
+ @SerializedName("discord")
+ DISCORD,
+
+ @SerializedName("topgg")
+ TOPGG
+}
diff --git a/src/main/java/org/discordbots/api/entity/Vote.java b/src/main/java/org/discordbots/api/entity/Vote.java
new file mode 100644
index 0000000..a8d79ea
--- /dev/null
+++ b/src/main/java/org/discordbots/api/entity/Vote.java
@@ -0,0 +1,40 @@
+package org.discordbots.api.entity;
+
+import com.google.gson.annotations.SerializedName;
+import java.time.OffsetDateTime;
+
+public class Vote {
+ @SerializedName("user_id")
+ private String userId;
+
+ @SerializedName("platform_id")
+ private String platformId;
+
+ @SerializedName("created_at")
+ private OffsetDateTime votedAt;
+
+ @SerializedName("expires_at")
+ private OffsetDateTime expiresAt;
+
+ private int weight;
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public String getPlatformId() {
+ return platformId;
+ }
+
+ public OffsetDateTime getVotedAt() {
+ return votedAt;
+ }
+
+ public OffsetDateTime getExpiresAt() {
+ return expiresAt;
+ }
+
+ public int getWeight() {
+ return weight;
+ }
+}
diff --git a/src/main/java/org/discordbots/api/io/DefaultResponseTransformer.java b/src/main/java/org/discordbots/api/io/DefaultResponseTransformer.java
new file mode 100644
index 0000000..a7e4cba
--- /dev/null
+++ b/src/main/java/org/discordbots/api/io/DefaultResponseTransformer.java
@@ -0,0 +1,20 @@
+package org.discordbots.api.io;
+
+import com.google.gson.Gson;
+import java.io.IOException;
+import okhttp3.Response;
+
+public class DefaultResponseTransformer implements ResponseTransformer {
+ private final Class aClass;
+ private final Gson gson;
+
+ public DefaultResponseTransformer(final Class aClass, final Gson gson) {
+ this.aClass = aClass;
+ this.gson = gson;
+ }
+
+ @Override
+ public E transform(final Response response) throws IOException {
+ return gson.fromJson(response.body().string(), aClass);
+ }
+}
diff --git a/src/main/java/org/discordbots/api/io/EmptyResponseTransformer.java b/src/main/java/org/discordbots/api/io/EmptyResponseTransformer.java
new file mode 100644
index 0000000..61f989c
--- /dev/null
+++ b/src/main/java/org/discordbots/api/io/EmptyResponseTransformer.java
@@ -0,0 +1,10 @@
+package org.discordbots.api.io;
+
+import okhttp3.Response;
+
+public class EmptyResponseTransformer implements ResponseTransformer {
+ @Override
+ public Void transform(final Response response) {
+ return null;
+ }
+}
diff --git a/src/main/java/org/discordbots/api/io/PaginatedVotesConverter.java b/src/main/java/org/discordbots/api/io/PaginatedVotesConverter.java
new file mode 100644
index 0000000..a3216ff
--- /dev/null
+++ b/src/main/java/org/discordbots/api/io/PaginatedVotesConverter.java
@@ -0,0 +1,35 @@
+package org.discordbots.api.io;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+import org.discordbots.api.DBLAPI;
+import org.discordbots.api.entity.PaginatedVotes;
+import org.discordbots.api.entity.Vote;
+
+public class PaginatedVotesConverter implements JsonDeserializer {
+ private final DBLAPI client;
+
+ public PaginatedVotesConverter(final DBLAPI client) {
+ this.client = client;
+ }
+
+ @Override
+ public PaginatedVotes deserialize(
+ JsonElement json, Type typeOfT, JsonDeserializationContext context) {
+ final JsonObject object = json.getAsJsonObject();
+
+ final List votes =
+ StreamSupport.stream(object.getAsJsonArray("data").spliterator(), false)
+ .map(vote -> (Vote) context.deserialize(vote, Vote.class))
+ .collect(Collectors.toList());
+ final String cursor = object.get("cursor").getAsString();
+
+ return new PaginatedVotes(votes, cursor, client);
+ }
+}
diff --git a/src/main/java/org/discordbots/api/io/PostCommandsTransformer.java b/src/main/java/org/discordbots/api/io/PostCommandsTransformer.java
new file mode 100644
index 0000000..c1846e8
--- /dev/null
+++ b/src/main/java/org/discordbots/api/io/PostCommandsTransformer.java
@@ -0,0 +1,7 @@
+package org.discordbots.api.io;
+
+import java.util.concurrent.CompletionStage;
+
+public interface PostCommandsTransformer {
+ CompletionStage toJsonString();
+}
diff --git a/src/main/java/org/discordbots/api/io/RawPostCommandsTransformer.java b/src/main/java/org/discordbots/api/io/RawPostCommandsTransformer.java
new file mode 100644
index 0000000..06ae3fe
--- /dev/null
+++ b/src/main/java/org/discordbots/api/io/RawPostCommandsTransformer.java
@@ -0,0 +1,18 @@
+package org.discordbots.api.io;
+
+import com.google.gson.JsonArray;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+public class RawPostCommandsTransformer implements PostCommandsTransformer {
+ private final JsonArray object;
+
+ public RawPostCommandsTransformer(final JsonArray object) {
+ this.object = object;
+ }
+
+ @Override
+ public CompletionStage toJsonString() {
+ return CompletableFuture.completedFuture(object.toString());
+ }
+}
diff --git a/src/main/java/org/discordbots/api/io/ResponseTransformer.java b/src/main/java/org/discordbots/api/io/ResponseTransformer.java
new file mode 100644
index 0000000..6eb55f1
--- /dev/null
+++ b/src/main/java/org/discordbots/api/io/ResponseTransformer.java
@@ -0,0 +1,7 @@
+package org.discordbots.api.io;
+
+import okhttp3.Response;
+
+public interface ResponseTransformer {
+ E transform(final Response response) throws Exception;
+}
diff --git a/src/main/java/org/discordbots/api/io/UnsuccessfulHttpException.java b/src/main/java/org/discordbots/api/io/UnsuccessfulHttpException.java
new file mode 100644
index 0000000..1665aa1
--- /dev/null
+++ b/src/main/java/org/discordbots/api/io/UnsuccessfulHttpException.java
@@ -0,0 +1,18 @@
+package org.discordbots.api.io;
+
+import okhttp3.Response;
+
+public class UnsuccessfulHttpException extends Exception {
+ private final Response response;
+
+ public UnsuccessfulHttpException(final Response response) {
+ super(
+ "The server responded with code: " + response.code() + ", message: " + response.message());
+
+ this.response = response;
+ }
+
+ public Response getResponse() {
+ return response;
+ }
+}
diff --git a/src/springBootWebhooks/java/org/discordbots/webhooks/springboot/DBLWebhooks.java b/src/springBootWebhooks/java/org/discordbots/webhooks/springboot/DBLWebhooks.java
new file mode 100644
index 0000000..e6ebee0
--- /dev/null
+++ b/src/springBootWebhooks/java/org/discordbots/webhooks/springboot/DBLWebhooks.java
@@ -0,0 +1,105 @@
+package org.discordbots.webhooks.springboot;
+
+import com.fatboyindustrial.gsonjavatime.OffsetDateTimeConverter;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonIOException;
+import com.google.gson.JsonSyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.time.OffsetDateTime;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HexFormat;
+import java.util.stream.Collectors;
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import org.discordbots.webhooks.payload.IntegrationCreatePayload;
+import org.discordbots.webhooks.payload.IntegrationDeletePayload;
+import org.discordbots.webhooks.payload.Payload;
+import org.discordbots.webhooks.payload.TestPayload;
+import org.discordbots.webhooks.payload.VoteCreatePayload;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+
+public class DBLWebhooks implements DBLWebhooksListener {
+ private byte[] secret;
+ private final Gson gson;
+
+ public DBLWebhooks(final String secret) {
+ this.secret = secret.getBytes(StandardCharsets.UTF_8);
+ this.gson =
+ new GsonBuilder()
+ .registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeConverter())
+ .create();
+ }
+
+ public String getSecret() {
+ return new String(secret, StandardCharsets.UTF_8);
+ }
+
+ public void setSecret(final String newSecret) {
+ secret = newSecret.getBytes(StandardCharsets.UTF_8);
+ }
+
+ @SuppressWarnings("UseSpecificCatch")
+ protected ResponseEntity dispatch(
+ final String body, final String signatureHeader, final String trace) {
+ try {
+ final HashMap parsedSignature =
+ Arrays.stream(signatureHeader.split(","))
+ .map(part -> part.split("=", 2))
+ .collect(
+ Collectors.toMap(
+ part -> part[0].trim(),
+ part -> part[1].trim(),
+ (existing, replacement) -> replacement,
+ HashMap::new));
+
+ final String signature = parsedSignature.get("v1");
+ final String timestamp = parsedSignature.get("t");
+
+ assert signature != null && timestamp != null;
+
+ final SecretKeySpec key = new SecretKeySpec(secret, "HmacSHA256");
+ final Mac hmac = Mac.getInstance("HmacSHA256");
+
+ hmac.init(key);
+
+ final byte[] digest =
+ hmac.doFinal(String.format("%s.%s", timestamp, body).getBytes(StandardCharsets.UTF_8));
+
+ if (!signature.equals(HexFormat.of().formatHex(digest))) {
+ return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
+ }
+
+ final Payload payload = gson.fromJson(body, Payload.class);
+
+ try {
+ return switch (payload.getType()) {
+ case "integration.create" ->
+ onIntegrationCreate(payload.getData(gson, IntegrationCreatePayload.class), trace);
+ case "integration.delete" ->
+ onIntegrationDelete(payload.getData(gson, IntegrationDeletePayload.class), trace);
+ case "webhook.test" -> onTest(payload.getData(gson, TestPayload.class), trace);
+ case "vote.create" -> onVoteCreate(payload.getData(gson, VoteCreatePayload.class), trace);
+ default -> ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
+ };
+ } catch (final Throwable ignored) {
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
+ }
+ } catch (final NoSuchAlgorithmException
+ | InvalidKeyException
+ | ArrayIndexOutOfBoundsException
+ | AssertionError
+ | JsonSyntaxException
+ | JsonIOException error) {
+ return ResponseEntity.status(
+ (error instanceof NoSuchAlgorithmException || error instanceof InvalidKeyException)
+ ? HttpStatus.INTERNAL_SERVER_ERROR
+ : HttpStatus.BAD_REQUEST)
+ .build();
+ }
+ }
+}
diff --git a/src/springBootWebhooks/java/org/discordbots/webhooks/springboot/DBLWebhooksListener.java b/src/springBootWebhooks/java/org/discordbots/webhooks/springboot/DBLWebhooksListener.java
new file mode 100644
index 0000000..c1f2df6
--- /dev/null
+++ b/src/springBootWebhooks/java/org/discordbots/webhooks/springboot/DBLWebhooksListener.java
@@ -0,0 +1,28 @@
+package org.discordbots.webhooks.springboot;
+
+import org.discordbots.webhooks.payload.IntegrationCreatePayload;
+import org.discordbots.webhooks.payload.IntegrationDeletePayload;
+import org.discordbots.webhooks.payload.TestPayload;
+import org.discordbots.webhooks.payload.VoteCreatePayload;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+
+public interface DBLWebhooksListener {
+ default ResponseEntity onIntegrationCreate(
+ final IntegrationCreatePayload payload, final String trace) {
+ return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
+ }
+
+ default ResponseEntity onIntegrationDelete(
+ final IntegrationDeletePayload payload, final String trace) {
+ return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
+ }
+
+ default ResponseEntity onTest(final TestPayload payload, final String trace) {
+ return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
+ }
+
+ default ResponseEntity onVoteCreate(final VoteCreatePayload payload, final String trace) {
+ return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
+ }
+}
diff --git a/src/test/java/org/discordbots/api/DBLAPITest.java b/src/test/java/org/discordbots/api/DBLAPITest.java
new file mode 100644
index 0000000..99fb651
--- /dev/null
+++ b/src/test/java/org/discordbots/api/DBLAPITest.java
@@ -0,0 +1,69 @@
+package org.discordbots.api;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonParser;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import okhttp3.OkHttpClient;
+import org.discordbots.api.entity.PaginatedVotes;
+import org.discordbots.api.entity.UserSource;
+import org.discordbots.api.interceptors.GetSelfInterceptor;
+import org.discordbots.api.interceptors.GetVoteInterceptor;
+import org.discordbots.api.interceptors.GetVotesInterceptor;
+import org.discordbots.api.interceptors.PostCommandsInterceptor;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+
+public class DBLAPITest {
+ private static DBLAPI CLIENT;
+
+ @BeforeAll
+ public static void setup() {
+ CLIENT =
+ new DBLAPI(
+ new OkHttpClient.Builder()
+ .addInterceptor(new GetSelfInterceptor())
+ .addInterceptor(new GetVoteInterceptor())
+ .addInterceptor(new GetVotesInterceptor())
+ .addInterceptor(new PostCommandsInterceptor())
+ .build());
+ }
+
+ @Test
+ public void getSelf() {
+ CLIENT.getSelf().toCompletableFuture().join();
+ }
+
+ @Test
+ public void postCommands() {
+ final JsonArray commands =
+ JsonParser.parseReader(
+ new InputStreamReader(
+ getClass().getClassLoader().getResourceAsStream("PostCommands.json"),
+ StandardCharsets.UTF_8))
+ .getAsJsonArray();
+
+ CLIENT.postCommands(commands).toCompletableFuture().join();
+ }
+
+ @ParameterizedTest
+ @EnumSource(UserSource.class)
+ public void getVote(final UserSource userSource) {
+ CLIENT.getVote(userSource, "123456").toCompletableFuture().join();
+ }
+
+ @Test
+ @SuppressWarnings("unused")
+ public void getVotes() {
+ final PaginatedVotes firstPage =
+ CLIENT
+ .getVotes(OffsetDateTime.of(2026, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC))
+ .toCompletableFuture()
+ .join();
+ final PaginatedVotes secondPage = firstPage.next().toCompletableFuture().join();
+ }
+}
diff --git a/src/test/java/org/discordbots/api/DBLWidgetTest.java b/src/test/java/org/discordbots/api/DBLWidgetTest.java
new file mode 100644
index 0000000..1d8780f
--- /dev/null
+++ b/src/test/java/org/discordbots/api/DBLWidgetTest.java
@@ -0,0 +1,31 @@
+package org.discordbots.api;
+
+import org.discordbots.api.entity.ProjectType;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+
+public class DBLWidgetTest {
+ @ParameterizedTest
+ @EnumSource(ProjectType.class)
+ public void large(final ProjectType projectType) {
+ DBLWidget.large(projectType, "123456");
+ }
+
+ @ParameterizedTest
+ @EnumSource(ProjectType.class)
+ public void votes(final ProjectType projectType) {
+ DBLWidget.votes(projectType, "123456");
+ }
+
+ @ParameterizedTest
+ @EnumSource(ProjectType.class)
+ public void owner(final ProjectType projectType) {
+ DBLWidget.owner(projectType, "123456");
+ }
+
+ @ParameterizedTest
+ @EnumSource(ProjectType.class)
+ public void social(final ProjectType projectType) {
+ DBLWidget.social(projectType, "123456");
+ }
+}
diff --git a/src/test/java/org/discordbots/api/interceptors/BaseInterceptor.java b/src/test/java/org/discordbots/api/interceptors/BaseInterceptor.java
new file mode 100644
index 0000000..63147a6
--- /dev/null
+++ b/src/test/java/org/discordbots/api/interceptors/BaseInterceptor.java
@@ -0,0 +1,60 @@
+package org.discordbots.api.interceptors;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import okhttp3.HttpUrl;
+import okhttp3.Interceptor;
+import okhttp3.MediaType;
+import okhttp3.Protocol;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+
+public abstract class BaseInterceptor implements Interceptor {
+ @SuppressWarnings("FieldMayBeFinal")
+ private String response;
+
+ public BaseInterceptor() {
+ try {
+ final String className = getClass().getSimpleName();
+
+ final InputStream inputStream =
+ BaseInterceptor.class.getResourceAsStream(
+ "/" + className.substring(0, className.length() - 11) + "Response.json");
+
+ response = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
+ } catch (final IOException | NullPointerException ignored) {
+ response = "";
+ }
+ }
+
+ protected abstract boolean isCorrect(final String method, final String path, final HttpUrl url);
+
+ protected abstract int getStatusCode();
+
+ protected abstract String getMessage();
+
+ @Override
+ public Response intercept(final Chain chain) throws IOException {
+ final Request request = chain.request();
+
+ final HttpUrl url = request.url();
+ final String path = String.join("/", url.pathSegments());
+
+ if (url.host().equals("top.gg")
+ && path.startsWith("api/v1")
+ && isCorrect(request.method(), path, url)) {
+ return new Response.Builder()
+ .request(request)
+ .protocol(Protocol.HTTP_1_1)
+ .code(getStatusCode())
+ .message(getMessage())
+ .body(ResponseBody.create(response, MediaType.get("application/json")))
+ .addHeader("content-type", "application/json")
+ .build();
+ }
+
+ return chain.proceed(request);
+ }
+}
diff --git a/src/test/java/org/discordbots/api/interceptors/GetSelfInterceptor.java b/src/test/java/org/discordbots/api/interceptors/GetSelfInterceptor.java
new file mode 100644
index 0000000..33cc308
--- /dev/null
+++ b/src/test/java/org/discordbots/api/interceptors/GetSelfInterceptor.java
@@ -0,0 +1,20 @@
+package org.discordbots.api.interceptors;
+
+import okhttp3.HttpUrl;
+
+public class GetSelfInterceptor extends BaseInterceptor {
+ @Override
+ protected boolean isCorrect(final String method, final String path, final HttpUrl url) {
+ return method.equals("GET") && path.endsWith("/projects/@me");
+ }
+
+ @Override
+ protected int getStatusCode() {
+ return 200;
+ }
+
+ @Override
+ protected String getMessage() {
+ return "OK";
+ }
+}
diff --git a/src/test/java/org/discordbots/api/interceptors/GetVoteInterceptor.java b/src/test/java/org/discordbots/api/interceptors/GetVoteInterceptor.java
new file mode 100644
index 0000000..dd99906
--- /dev/null
+++ b/src/test/java/org/discordbots/api/interceptors/GetVoteInterceptor.java
@@ -0,0 +1,22 @@
+package org.discordbots.api.interceptors;
+
+import okhttp3.HttpUrl;
+
+public class GetVoteInterceptor extends BaseInterceptor {
+ @Override
+ protected boolean isCorrect(final String method, final String path, final HttpUrl url) {
+ return method.equals("GET")
+ && path.contains("/projects/@me/votes/")
+ && url.queryParameter("source") != null;
+ }
+
+ @Override
+ protected int getStatusCode() {
+ return 200;
+ }
+
+ @Override
+ protected String getMessage() {
+ return "OK";
+ }
+}
diff --git a/src/test/java/org/discordbots/api/interceptors/GetVotesInterceptor.java b/src/test/java/org/discordbots/api/interceptors/GetVotesInterceptor.java
new file mode 100644
index 0000000..829329b
--- /dev/null
+++ b/src/test/java/org/discordbots/api/interceptors/GetVotesInterceptor.java
@@ -0,0 +1,22 @@
+package org.discordbots.api.interceptors;
+
+import okhttp3.HttpUrl;
+
+public class GetVotesInterceptor extends BaseInterceptor {
+ @Override
+ protected boolean isCorrect(final String method, final String path, final HttpUrl url) {
+ return method.equals("GET")
+ && path.endsWith("/projects/@me/votes")
+ && (url.queryParameter("startDate") != null || url.queryParameter("cursor") != null);
+ }
+
+ @Override
+ protected int getStatusCode() {
+ return 200;
+ }
+
+ @Override
+ protected String getMessage() {
+ return "OK";
+ }
+}
diff --git a/src/test/java/org/discordbots/api/interceptors/PostCommandsInterceptor.java b/src/test/java/org/discordbots/api/interceptors/PostCommandsInterceptor.java
new file mode 100644
index 0000000..9ff895e
--- /dev/null
+++ b/src/test/java/org/discordbots/api/interceptors/PostCommandsInterceptor.java
@@ -0,0 +1,20 @@
+package org.discordbots.api.interceptors;
+
+import okhttp3.HttpUrl;
+
+public class PostCommandsInterceptor extends BaseInterceptor {
+ @Override
+ protected boolean isCorrect(final String method, final String path, final HttpUrl url) {
+ return method.equals("POST") && path.endsWith("/projects/@me/commands");
+ }
+
+ @Override
+ protected int getStatusCode() {
+ return 204;
+ }
+
+ @Override
+ protected String getMessage() {
+ return "No Content";
+ }
+}
diff --git a/src/test/java/org/discordbots/webhooks/DBLWebhooksTestSuite.java b/src/test/java/org/discordbots/webhooks/DBLWebhooksTestSuite.java
new file mode 100644
index 0000000..66f58ae
--- /dev/null
+++ b/src/test/java/org/discordbots/webhooks/DBLWebhooksTestSuite.java
@@ -0,0 +1,15 @@
+package org.discordbots.webhooks;
+
+import org.discordbots.webhooks.dropwizard.DBLDropwizardWebhooksTest;
+import org.discordbots.webhooks.eclipsejetty.DBLEclipseJettyWebhooksTest;
+import org.discordbots.webhooks.springboot.DBLSpringBootWebhooksTest;
+import org.junit.platform.suite.api.SelectClasses;
+import org.junit.platform.suite.api.Suite;
+
+@Suite
+@SelectClasses({
+ DBLDropwizardWebhooksTest.class,
+ DBLEclipseJettyWebhooksTest.class,
+ DBLSpringBootWebhooksTest.class
+})
+public class DBLWebhooksTestSuite {}
diff --git a/src/test/java/org/discordbots/webhooks/Mocks.java b/src/test/java/org/discordbots/webhooks/Mocks.java
new file mode 100644
index 0000000..c471d13
--- /dev/null
+++ b/src/test/java/org/discordbots/webhooks/Mocks.java
@@ -0,0 +1,47 @@
+package org.discordbots.webhooks;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.time.Instant;
+import java.util.HexFormat;
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+public class Mocks {
+ public final String integrationCreatePayload;
+ public final String integrationDeletePayload;
+ public final String testPayload;
+ public final String voteCreatePayload;
+
+ public Mocks() throws IOException, NullPointerException {
+ integrationCreatePayload = read("IntegrationCreate");
+ integrationDeletePayload = read("IntegrationDelete");
+ testPayload = read("Test");
+ voteCreatePayload = read("VoteCreate");
+ }
+
+ private static String read(final String name) throws IOException, NullPointerException {
+ final InputStream inputStream = Mocks.class.getResourceAsStream("/" + name + "Payload.json");
+
+ return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
+ }
+
+ public static String signature(final String secret, final String body)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+ final long timestamp = Instant.now().getEpochSecond();
+
+ final SecretKeySpec key =
+ new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
+ final Mac hmac = Mac.getInstance("HmacSHA256");
+
+ hmac.init(key);
+
+ final byte[] digest =
+ hmac.doFinal(String.format("%s.%s", timestamp, body).getBytes(StandardCharsets.UTF_8));
+
+ return "t=" + Long.toString(timestamp) + ",v1=" + HexFormat.of().formatHex(digest);
+ }
+}
diff --git a/src/test/java/org/discordbots/webhooks/dropwizard/CustomServer.java b/src/test/java/org/discordbots/webhooks/dropwizard/CustomServer.java
new file mode 100644
index 0000000..0885b6c
--- /dev/null
+++ b/src/test/java/org/discordbots/webhooks/dropwizard/CustomServer.java
@@ -0,0 +1,16 @@
+package org.discordbots.webhooks.dropwizard;
+
+import io.dropwizard.core.Application;
+import io.dropwizard.core.Configuration;
+import io.dropwizard.core.setup.Environment;
+
+public class CustomServer extends Application {
+ public static void main(final String[] args) throws Exception {
+ new CustomServer().run(args);
+ }
+
+ @Override
+ public void run(final Configuration config, final Environment env) {
+ env.jersey().register(new CustomWebhooks());
+ }
+}
diff --git a/src/test/java/org/discordbots/webhooks/dropwizard/CustomWebhooks.java b/src/test/java/org/discordbots/webhooks/dropwizard/CustomWebhooks.java
new file mode 100644
index 0000000..246abd8
--- /dev/null
+++ b/src/test/java/org/discordbots/webhooks/dropwizard/CustomWebhooks.java
@@ -0,0 +1,35 @@
+package org.discordbots.webhooks.dropwizard;
+
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.core.Response;
+import org.discordbots.webhooks.payload.IntegrationCreatePayload;
+import org.discordbots.webhooks.payload.IntegrationDeletePayload;
+import org.discordbots.webhooks.payload.TestPayload;
+import org.discordbots.webhooks.payload.VoteCreatePayload;
+
+@Path("/webhook")
+public class CustomWebhooks extends DBLWebhooks {
+ public CustomWebhooks() {
+ super(System.getenv("TOPGG_WEBHOOK_SECRET"));
+ }
+
+ @Override
+ public Response onIntegrationCreate(final IntegrationCreatePayload payload, final String trace) {
+ return Response.status(Response.Status.OK).entity("dw:integrationCreate," + trace).build();
+ }
+
+ @Override
+ public Response onIntegrationDelete(final IntegrationDeletePayload payload, final String trace) {
+ return Response.status(Response.Status.OK).entity("dw:integrationDelete," + trace).build();
+ }
+
+ @Override
+ public Response onTest(final TestPayload payload, final String trace) {
+ return Response.status(Response.Status.OK).entity("dw:test," + trace).build();
+ }
+
+ @Override
+ public Response onVoteCreate(final VoteCreatePayload payload, final String trace) {
+ return Response.status(Response.Status.OK).entity("dw:voteCreate," + trace).build();
+ }
+}
diff --git a/src/test/java/org/discordbots/webhooks/dropwizard/DBLDropwizardWebhooksTest.java b/src/test/java/org/discordbots/webhooks/dropwizard/DBLDropwizardWebhooksTest.java
new file mode 100644
index 0000000..0456eee
--- /dev/null
+++ b/src/test/java/org/discordbots/webhooks/dropwizard/DBLDropwizardWebhooksTest.java
@@ -0,0 +1,68 @@
+package org.discordbots.webhooks.dropwizard;
+
+import io.dropwizard.core.Configuration;
+import io.dropwizard.testing.ResourceHelpers;
+import io.dropwizard.testing.junit5.DropwizardAppExtension;
+import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import org.discordbots.webhooks.Mocks;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+@ExtendWith(DropwizardExtensionsSupport.class)
+public class DBLDropwizardWebhooksTest {
+ private static final DropwizardAppExtension APP =
+ new DropwizardAppExtension<>(
+ CustomServer.class, ResourceHelpers.resourceFilePath("dropwizard-test-config.yml"));
+
+ private static final String SECRET = System.getenv("TOPGG_WEBHOOK_SECRET");
+ private static final String TRACE = "trace";
+ private static Mocks MOCKS;
+
+ @BeforeAll
+ public static void setup() throws IOException, NullPointerException {
+ MOCKS = new Mocks();
+ }
+
+ private void send(final String name, final String payload)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+ final Response response =
+ APP.client()
+ .target(String.format("http://localhost:%d/webhook", APP.getLocalPort()))
+ .request()
+ .header("Content-Type", "application/json")
+ .header("x-topgg-signature", Mocks.signature(SECRET, payload))
+ .header("x-topgg-trace", TRACE)
+ .post(Entity.entity(payload, MediaType.APPLICATION_JSON));
+
+ Assertions.assertEquals(200, response.getStatus());
+ Assertions.assertEquals("dw:" + name + "," + TRACE, response.readEntity(String.class));
+ }
+
+ @Test
+ public void integrationCreate() throws NoSuchAlgorithmException, InvalidKeyException {
+ send("integrationCreate", MOCKS.integrationCreatePayload);
+ }
+
+ @Test
+ public void integrationDelete() throws NoSuchAlgorithmException, InvalidKeyException {
+ send("integrationDelete", MOCKS.integrationDeletePayload);
+ }
+
+ @Test
+ public void test() throws NoSuchAlgorithmException, InvalidKeyException {
+ send("test", MOCKS.testPayload);
+ }
+
+ @Test
+ public void voteCreate() throws NoSuchAlgorithmException, InvalidKeyException {
+ send("voteCreate", MOCKS.voteCreatePayload);
+ }
+}
diff --git a/src/test/java/org/discordbots/webhooks/eclipsejetty/CustomWebhooks.java b/src/test/java/org/discordbots/webhooks/eclipsejetty/CustomWebhooks.java
new file mode 100644
index 0000000..3bcfdcf
--- /dev/null
+++ b/src/test/java/org/discordbots/webhooks/eclipsejetty/CustomWebhooks.java
@@ -0,0 +1,51 @@
+package org.discordbots.webhooks.eclipsejetty;
+
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import org.discordbots.webhooks.payload.IntegrationCreatePayload;
+import org.discordbots.webhooks.payload.IntegrationDeletePayload;
+import org.discordbots.webhooks.payload.TestPayload;
+import org.discordbots.webhooks.payload.VoteCreatePayload;
+
+public class CustomWebhooks extends DBLWebhooks {
+ public CustomWebhooks() {
+ super(System.getenv("TOPGG_WEBHOOK_SECRET"));
+ }
+
+ private void reply(final String name, final HttpServletResponse response, final String trace) {
+ try {
+ response.setStatus(HttpServletResponse.SC_OK);
+ response.getWriter().write("ej:" + name + "," + trace);
+ } catch (final IOException ignored) {
+ response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @Override
+ public void onIntegrationCreate(
+ final HttpServletResponse response,
+ final IntegrationCreatePayload payload,
+ final String trace) {
+ reply("integrationCreate", response, trace);
+ }
+
+ @Override
+ public void onIntegrationDelete(
+ final HttpServletResponse response,
+ final IntegrationDeletePayload payload,
+ final String trace) {
+ reply("integrationDelete", response, trace);
+ }
+
+ @Override
+ public void onTest(
+ final HttpServletResponse response, final TestPayload payload, final String trace) {
+ reply("test", response, trace);
+ }
+
+ @Override
+ public void onVoteCreate(
+ final HttpServletResponse response, final VoteCreatePayload payload, final String trace) {
+ reply("voteCreate", response, trace);
+ }
+}
diff --git a/src/test/java/org/discordbots/webhooks/eclipsejetty/DBLEclipseJettyWebhooksTest.java b/src/test/java/org/discordbots/webhooks/eclipsejetty/DBLEclipseJettyWebhooksTest.java
new file mode 100644
index 0000000..7450a20
--- /dev/null
+++ b/src/test/java/org/discordbots/webhooks/eclipsejetty/DBLEclipseJettyWebhooksTest.java
@@ -0,0 +1,105 @@
+package org.discordbots.webhooks.eclipsejetty;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.ProtocolException;
+import java.net.URI;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import org.discordbots.webhooks.Mocks;
+import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
+import org.eclipse.jetty.ee10.servlet.ServletHolder;
+import org.eclipse.jetty.server.Server;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+public class DBLEclipseJettyWebhooksTest {
+ private static Server SERVER = null;
+
+ private static final String SECRET = System.getenv("TOPGG_WEBHOOK_SECRET");
+ private static final String TRACE = "trace";
+ private static Mocks MOCKS;
+
+ @BeforeAll
+ public static void setup() throws IOException, NullPointerException, Exception {
+ MOCKS = new Mocks();
+
+ SERVER = new Server(8080);
+
+ final ServletContextHandler context = new ServletContextHandler();
+
+ context.setContextPath("/");
+ context.addServlet(new ServletHolder(new CustomWebhooks()), "/webhook");
+
+ SERVER.setHandler(context);
+ SERVER.start();
+ }
+
+ private void send(final String name, final String payload)
+ throws NoSuchAlgorithmException, InvalidKeyException, ProtocolException, IOException {
+ final HttpURLConnection connection =
+ (HttpURLConnection) URI.create("http://localhost:8080/webhook").toURL().openConnection();
+
+ connection.setRequestMethod("POST");
+ connection.setRequestProperty("Content-Type", "application/json");
+ connection.setRequestProperty("x-topgg-signature", Mocks.signature(SECRET, payload));
+ connection.setRequestProperty("x-topgg-trace", TRACE);
+ connection.setDoOutput(true);
+
+ try (final OutputStream outputStream = connection.getOutputStream()) {
+ final byte[] payloadBytes = payload.getBytes("utf-8");
+
+ outputStream.write(payloadBytes, 0, payloadBytes.length);
+ }
+
+ Assertions.assertEquals(200, connection.getResponseCode());
+
+ try (final BufferedReader reader =
+ new BufferedReader(new InputStreamReader(connection.getInputStream(), "utf-8"))) {
+ final StringBuilder response = new StringBuilder();
+ String responseLine;
+
+ while ((responseLine = reader.readLine()) != null) {
+ response.append(responseLine.trim());
+ }
+
+ Assertions.assertEquals("ej:" + name + "," + TRACE, response.toString());
+ }
+ }
+
+ @Test
+ public void integrationCreate()
+ throws NoSuchAlgorithmException, InvalidKeyException, ProtocolException, IOException {
+ send("integrationCreate", MOCKS.integrationCreatePayload);
+ }
+
+ @Test
+ public void integrationDelete()
+ throws NoSuchAlgorithmException, InvalidKeyException, ProtocolException, IOException {
+ send("integrationDelete", MOCKS.integrationDeletePayload);
+ }
+
+ @Test
+ public void test()
+ throws NoSuchAlgorithmException, InvalidKeyException, ProtocolException, IOException {
+ send("test", MOCKS.testPayload);
+ }
+
+ @Test
+ public void voteCreate()
+ throws NoSuchAlgorithmException, InvalidKeyException, ProtocolException, IOException {
+ send("voteCreate", MOCKS.voteCreatePayload);
+ }
+
+ @AfterAll
+ public static void cleanup() throws Exception {
+ if (SERVER != null) {
+ SERVER.stop();
+ }
+ }
+}
diff --git a/src/test/java/org/discordbots/webhooks/springboot/CustomServer.java b/src/test/java/org/discordbots/webhooks/springboot/CustomServer.java
new file mode 100644
index 0000000..e5c584f
--- /dev/null
+++ b/src/test/java/org/discordbots/webhooks/springboot/CustomServer.java
@@ -0,0 +1,11 @@
+package org.discordbots.webhooks.springboot;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class CustomServer {
+ public static void main(final String[] args) throws Exception {
+ SpringApplication.run(CustomServer.class, args);
+ }
+}
diff --git a/src/test/java/org/discordbots/webhooks/springboot/CustomServerSecurityConfiguration.java b/src/test/java/org/discordbots/webhooks/springboot/CustomServerSecurityConfiguration.java
new file mode 100644
index 0000000..9805872
--- /dev/null
+++ b/src/test/java/org/discordbots/webhooks/springboot/CustomServerSecurityConfiguration.java
@@ -0,0 +1,16 @@
+package org.discordbots.webhooks.springboot;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.web.SecurityFilterChain;
+
+@Configuration
+public class CustomServerSecurityConfiguration {
+ @Bean
+ public SecurityFilterChain filterChain(final HttpSecurity http) {
+ return http.csrf(csrf -> csrf.disable())
+ .authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
+ .build();
+ }
+}
diff --git a/src/test/java/org/discordbots/webhooks/springboot/CustomWebhooks.java b/src/test/java/org/discordbots/webhooks/springboot/CustomWebhooks.java
new file mode 100644
index 0000000..c6eca75
--- /dev/null
+++ b/src/test/java/org/discordbots/webhooks/springboot/CustomWebhooks.java
@@ -0,0 +1,49 @@
+package org.discordbots.webhooks.springboot;
+
+import org.discordbots.webhooks.payload.IntegrationCreatePayload;
+import org.discordbots.webhooks.payload.IntegrationDeletePayload;
+import org.discordbots.webhooks.payload.TestPayload;
+import org.discordbots.webhooks.payload.VoteCreatePayload;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class CustomWebhooks extends DBLWebhooks {
+ public CustomWebhooks() {
+ super(System.getenv("TOPGG_WEBHOOK_SECRET"));
+ }
+
+ @PostMapping("/webhook")
+ public ResponseEntity main(
+ @RequestBody final String body,
+ @RequestHeader("x-topgg-signature") final String signature,
+ @RequestHeader("x-topgg-trace") final String trace) {
+ return dispatch(body, signature, trace);
+ }
+
+ @Override
+ public ResponseEntity onIntegrationCreate(
+ final IntegrationCreatePayload payload, final String trace) {
+ return ResponseEntity.status(HttpStatus.OK).body("sb:integrationCreate," + trace);
+ }
+
+ @Override
+ public ResponseEntity onIntegrationDelete(
+ final IntegrationDeletePayload payload, final String trace) {
+ return ResponseEntity.status(HttpStatus.OK).body("sb:integrationDelete," + trace);
+ }
+
+ @Override
+ public ResponseEntity onTest(final TestPayload payload, final String trace) {
+ return ResponseEntity.status(HttpStatus.OK).body("sb:test," + trace);
+ }
+
+ @Override
+ public ResponseEntity onVoteCreate(final VoteCreatePayload payload, final String trace) {
+ return ResponseEntity.status(HttpStatus.OK).body("sb:voteCreate," + trace);
+ }
+}
diff --git a/src/test/java/org/discordbots/webhooks/springboot/DBLSpringBootWebhooksTest.java b/src/test/java/org/discordbots/webhooks/springboot/DBLSpringBootWebhooksTest.java
new file mode 100644
index 0000000..8405f4e
--- /dev/null
+++ b/src/test/java/org/discordbots/webhooks/springboot/DBLSpringBootWebhooksTest.java
@@ -0,0 +1,59 @@
+package org.discordbots.webhooks.springboot;
+
+import java.io.IOException;
+import org.discordbots.webhooks.Mocks;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+public class DBLSpringBootWebhooksTest {
+ private static final String SECRET = System.getenv("TOPGG_WEBHOOK_SECRET");
+ private static final String TRACE = "trace";
+ private static Mocks MOCKS;
+
+ @Autowired private MockMvc mvc;
+
+ @BeforeAll
+ public static void setup() throws IOException, NullPointerException {
+ MOCKS = new Mocks();
+ }
+
+ private void send(final String name, final String payload) throws IOException, Exception {
+ mvc.perform(
+ MockMvcRequestBuilders.post("/webhook")
+ .content(payload)
+ .header("x-topgg-signature", Mocks.signature(SECRET, payload))
+ .header("x-topgg-trace", TRACE)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(MockMvcResultMatchers.status().is(200))
+ .andExpect(MockMvcResultMatchers.content().string("sb:" + name + "," + TRACE));
+ }
+
+ @Test
+ public void integrationCreate() throws IOException, Exception {
+ send("integrationCreate", MOCKS.integrationCreatePayload);
+ }
+
+ @Test
+ public void integrationDelete() throws IOException, Exception {
+ send("integrationDelete", MOCKS.integrationDeletePayload);
+ }
+
+ @Test
+ public void test() throws IOException, Exception {
+ send("test", MOCKS.testPayload);
+ }
+
+ @Test
+ public void voteCreate() throws IOException, Exception {
+ send("voteCreate", MOCKS.voteCreatePayload);
+ }
+}
diff --git a/src/test/resources/GetSelfResponse.json b/src/test/resources/GetSelfResponse.json
new file mode 100644
index 0000000..e74997b
--- /dev/null
+++ b/src/test/resources/GetSelfResponse.json
@@ -0,0 +1,16 @@
+{
+ "id": "364806029876555776",
+ "name": "Top.gg Lib Dev API Access",
+ "type": "bot",
+ "platform": "discord",
+ "headline": "API access for Top.gg Library Developers",
+ "tags": [
+ "api",
+ "library",
+ "topgg"
+ ],
+ "votes": 4,
+ "votes_total": 34,
+ "review_score": 5,
+ "review_count": 2
+}
\ No newline at end of file
diff --git a/src/test/resources/GetVoteResponse.json b/src/test/resources/GetVoteResponse.json
new file mode 100644
index 0000000..c6a0227
--- /dev/null
+++ b/src/test/resources/GetVoteResponse.json
@@ -0,0 +1,5 @@
+{
+ "created_at": "2026-02-25T22:35:36.978392+00:00",
+ "expires_at": "2026-02-26T10:35:36.978392+00:00",
+ "weight": 1
+}
\ No newline at end of file
diff --git a/src/test/resources/GetVotesResponse.json b/src/test/resources/GetVotesResponse.json
new file mode 100644
index 0000000..5eab378
--- /dev/null
+++ b/src/test/resources/GetVotesResponse.json
@@ -0,0 +1,33 @@
+{
+ "cursor": "",
+ "data": [
+ {
+ "user_id": "800506814562787328",
+ "platform_id": "1461830808796139662",
+ "weight": 2,
+ "created_at": "2026-01-17T23:36:06.34732Z",
+ "expires_at": "2026-01-18T11:36:06.34732Z"
+ },
+ {
+ "user_id": "316026718115037184",
+ "platform_id": "481068576363773972",
+ "weight": 2,
+ "created_at": "2026-02-20T05:43:58.392411Z",
+ "expires_at": "2026-02-20T17:43:58.392411Z"
+ },
+ {
+ "user_id": "794153497215045632",
+ "platform_id": "1425259851600101457",
+ "weight": 2,
+ "created_at": "2026-02-21T18:59:20.660734Z",
+ "expires_at": "2026-02-22T06:59:20.660734Z"
+ },
+ {
+ "user_id": "8226924471638491136",
+ "platform_id": "661200758510977084",
+ "weight": 1,
+ "created_at": "2026-02-25T22:35:36.978392Z",
+ "expires_at": "2026-02-26T10:35:36.978392Z"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/test/resources/IntegrationCreatePayload.json b/src/test/resources/IntegrationCreatePayload.json
new file mode 100644
index 0000000..77df6b0
--- /dev/null
+++ b/src/test/resources/IntegrationCreatePayload.json
@@ -0,0 +1,19 @@
+{
+ "type": "integration.create",
+ "data": {
+ "connection_id": "112402021105124",
+ "webhook_secret": "whs_abcd",
+ "project": {
+ "id": "1230954036934033243",
+ "platform": "discord",
+ "platform_id": "3949456393249234923",
+ "type": "bot"
+ },
+ "user": {
+ "id": "3949456393249234923",
+ "platform_id": "3949456393249234923",
+ "name": "username",
+ "avatar_url": ""
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/IntegrationDeletePayload.json b/src/test/resources/IntegrationDeletePayload.json
new file mode 100644
index 0000000..cb44375
--- /dev/null
+++ b/src/test/resources/IntegrationDeletePayload.json
@@ -0,0 +1,6 @@
+{
+ "type": "integration.delete",
+ "data": {
+ "connection_id": "112402021105124"
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/LeadResponse.json b/src/test/resources/LeadResponse.json
new file mode 100644
index 0000000..52b54a3
--- /dev/null
+++ b/src/test/resources/LeadResponse.json
@@ -0,0 +1,3 @@
+{
+ "error": "Not Found"
+}
\ No newline at end of file
diff --git a/src/test/resources/PostCommands.json b/src/test/resources/PostCommands.json
new file mode 100644
index 0000000..071db3e
--- /dev/null
+++ b/src/test/resources/PostCommands.json
@@ -0,0 +1,11 @@
+[
+ {
+ "id": "1",
+ "type": 1,
+ "application_id": "1",
+ "name": "test",
+ "description": "command description",
+ "default_member_permissions": "",
+ "version": "1"
+ }
+]
\ No newline at end of file
diff --git a/src/test/resources/TestPayload.json b/src/test/resources/TestPayload.json
new file mode 100644
index 0000000..b7a7432
--- /dev/null
+++ b/src/test/resources/TestPayload.json
@@ -0,0 +1,17 @@
+{
+ "type": "webhook.test",
+ "data": {
+ "user": {
+ "id": "160105994217586689",
+ "platform_id": "160105994217586689",
+ "name": "username",
+ "avatar_url": ""
+ },
+ "project": {
+ "id": "803190510032756736",
+ "type": "bot",
+ "platform": "discord",
+ "platform_id": "160105994217586689"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/VoteCreatePayload.json b/src/test/resources/VoteCreatePayload.json
new file mode 100644
index 0000000..6850196
--- /dev/null
+++ b/src/test/resources/VoteCreatePayload.json
@@ -0,0 +1,21 @@
+{
+ "type": "vote.create",
+ "data": {
+ "id": "808499215864008704",
+ "weight": 1,
+ "created_at": "2026-02-09T00:47:14.2510149+00:00",
+ "expires_at": "2026-02-09T12:47:14.2510149+00:00",
+ "project": {
+ "id": "803190510032756736",
+ "type": "bot",
+ "platform": "discord",
+ "platform_id": "160105994217586689"
+ },
+ "user": {
+ "id": "160105994217586689",
+ "platform_id": "160105994217586689",
+ "name": "username",
+ "avatar_url": ""
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/dropwizard-test-config.yml b/src/test/resources/dropwizard-test-config.yml
new file mode 100644
index 0000000..c8aa4d6
--- /dev/null
+++ b/src/test/resources/dropwizard-test-config.yml
@@ -0,0 +1,7 @@
+server:
+ applicationConnectors:
+ - type: http
+ port: 0
+ adminConnectors:
+ - type: http
+ port: 0
\ No newline at end of file
diff --git a/src/webhooks/java/org/discordbots/webhooks/entity/PartialProject.java b/src/webhooks/java/org/discordbots/webhooks/entity/PartialProject.java
new file mode 100644
index 0000000..85fff1d
--- /dev/null
+++ b/src/webhooks/java/org/discordbots/webhooks/entity/PartialProject.java
@@ -0,0 +1,30 @@
+package org.discordbots.webhooks.entity;
+
+import com.google.gson.annotations.SerializedName;
+
+public class PartialProject {
+ private String id;
+
+ private ProjectType type;
+
+ private Platform platform;
+
+ @SerializedName("platform_id")
+ private String platformId;
+
+ public String getId() {
+ return id;
+ }
+
+ public ProjectType getType() {
+ return type;
+ }
+
+ public Platform getPlatform() {
+ return platform;
+ }
+
+ public String getPlatformId() {
+ return platformId;
+ }
+}
diff --git a/src/webhooks/java/org/discordbots/webhooks/entity/Platform.java b/src/webhooks/java/org/discordbots/webhooks/entity/Platform.java
new file mode 100644
index 0000000..8cb1ad2
--- /dev/null
+++ b/src/webhooks/java/org/discordbots/webhooks/entity/Platform.java
@@ -0,0 +1,8 @@
+package org.discordbots.webhooks.entity;
+
+import com.google.gson.annotations.SerializedName;
+
+public enum Platform {
+ @SerializedName("discord")
+ DISCORD
+}
diff --git a/src/webhooks/java/org/discordbots/webhooks/entity/ProjectType.java b/src/webhooks/java/org/discordbots/webhooks/entity/ProjectType.java
new file mode 100644
index 0000000..675f7df
--- /dev/null
+++ b/src/webhooks/java/org/discordbots/webhooks/entity/ProjectType.java
@@ -0,0 +1,11 @@
+package org.discordbots.webhooks.entity;
+
+import com.google.gson.annotations.SerializedName;
+
+public enum ProjectType {
+ @SerializedName("bot")
+ DISCORD_BOT,
+
+ @SerializedName("server")
+ DISCORD_SERVER
+}
diff --git a/src/webhooks/java/org/discordbots/webhooks/entity/User.java b/src/webhooks/java/org/discordbots/webhooks/entity/User.java
new file mode 100644
index 0000000..0be2afb
--- /dev/null
+++ b/src/webhooks/java/org/discordbots/webhooks/entity/User.java
@@ -0,0 +1,31 @@
+package org.discordbots.webhooks.entity;
+
+import com.google.gson.annotations.SerializedName;
+
+public class User {
+ private String id;
+
+ private String name;
+
+ @SerializedName("avatar_url")
+ private String avatar;
+
+ @SerializedName("platform_id")
+ private String platformId;
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getAvatar() {
+ return avatar;
+ }
+
+ public String getPlatformId() {
+ return platformId;
+ }
+}
diff --git a/src/webhooks/java/org/discordbots/webhooks/payload/IntegrationCreatePayload.java b/src/webhooks/java/org/discordbots/webhooks/payload/IntegrationCreatePayload.java
new file mode 100644
index 0000000..99303c1
--- /dev/null
+++ b/src/webhooks/java/org/discordbots/webhooks/payload/IntegrationCreatePayload.java
@@ -0,0 +1,33 @@
+package org.discordbots.webhooks.payload;
+
+import com.google.gson.annotations.SerializedName;
+import org.discordbots.webhooks.entity.PartialProject;
+import org.discordbots.webhooks.entity.User;
+
+public class IntegrationCreatePayload {
+ @SerializedName("connection_id")
+ private String connectionId;
+
+ @SerializedName("webhook_secret")
+ private String secret;
+
+ private PartialProject project;
+
+ private User user;
+
+ public String getConnectionId() {
+ return connectionId;
+ }
+
+ public String getSecret() {
+ return secret;
+ }
+
+ public PartialProject getProject() {
+ return project;
+ }
+
+ public User getUser() {
+ return user;
+ }
+}
diff --git a/src/webhooks/java/org/discordbots/webhooks/payload/IntegrationDeletePayload.java b/src/webhooks/java/org/discordbots/webhooks/payload/IntegrationDeletePayload.java
new file mode 100644
index 0000000..2653cda
--- /dev/null
+++ b/src/webhooks/java/org/discordbots/webhooks/payload/IntegrationDeletePayload.java
@@ -0,0 +1,12 @@
+package org.discordbots.webhooks.payload;
+
+import com.google.gson.annotations.SerializedName;
+
+public class IntegrationDeletePayload {
+ @SerializedName("connection_id")
+ private String connectionId;
+
+ public String getConnectionId() {
+ return connectionId;
+ }
+}
diff --git a/src/webhooks/java/org/discordbots/webhooks/payload/Payload.java b/src/webhooks/java/org/discordbots/webhooks/payload/Payload.java
new file mode 100644
index 0000000..424ea39
--- /dev/null
+++ b/src/webhooks/java/org/discordbots/webhooks/payload/Payload.java
@@ -0,0 +1,19 @@
+package org.discordbots.webhooks.payload;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSyntaxException;
+
+public class Payload {
+ private String type;
+
+ private JsonObject data;
+
+ public String getType() {
+ return type;
+ }
+
+ public T getData(final Gson gson, final Class cls) throws JsonSyntaxException {
+ return gson.fromJson(data, cls);
+ }
+}
diff --git a/src/webhooks/java/org/discordbots/webhooks/payload/TestPayload.java b/src/webhooks/java/org/discordbots/webhooks/payload/TestPayload.java
new file mode 100644
index 0000000..6a90483
--- /dev/null
+++ b/src/webhooks/java/org/discordbots/webhooks/payload/TestPayload.java
@@ -0,0 +1,18 @@
+package org.discordbots.webhooks.payload;
+
+import org.discordbots.webhooks.entity.PartialProject;
+import org.discordbots.webhooks.entity.User;
+
+public class TestPayload {
+ private PartialProject project;
+
+ private User user;
+
+ public PartialProject getProject() {
+ return project;
+ }
+
+ public User getUser() {
+ return user;
+ }
+}
diff --git a/src/webhooks/java/org/discordbots/webhooks/payload/VoteCreatePayload.java b/src/webhooks/java/org/discordbots/webhooks/payload/VoteCreatePayload.java
new file mode 100644
index 0000000..9f8e819
--- /dev/null
+++ b/src/webhooks/java/org/discordbots/webhooks/payload/VoteCreatePayload.java
@@ -0,0 +1,46 @@
+package org.discordbots.webhooks.payload;
+
+import com.google.gson.annotations.SerializedName;
+import java.time.OffsetDateTime;
+import org.discordbots.webhooks.entity.PartialProject;
+import org.discordbots.webhooks.entity.User;
+
+public class VoteCreatePayload {
+ private String id;
+
+ private int weight;
+
+ @SerializedName("created_at")
+ private OffsetDateTime votedAt;
+
+ @SerializedName("expires_at")
+ private OffsetDateTime expiresAt;
+
+ private PartialProject project;
+
+ private User user;
+
+ public String getId() {
+ return id;
+ }
+
+ public int getWeight() {
+ return weight;
+ }
+
+ public OffsetDateTime getVotedAt() {
+ return votedAt;
+ }
+
+ public OffsetDateTime getExpiredAt() {
+ return expiresAt;
+ }
+
+ public PartialProject getProject() {
+ return project;
+ }
+
+ public User getUser() {
+ return user;
+ }
+}