diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a69fe3b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* eol=lf +**/*.jar binary \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..192221b --- /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..31a374e 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 SDK 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 'gg.top:SDK:1.0.0' +``` + +### Maven + +Add the following line to the `dependencies` section of your `pom.xml`: + +```xml + + gg.top + SDK + 1.0.0 + +``` + +## Capabilities + +This SDK 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 gg.top.api.TopggAPI; + +final TopggAPI client = new TopggAPI(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 gg.top.api.entity.UserSource; + +client.getVote(UserSource.DISCORD, "661200758510977084").whenComplete((vote, error) -> { + if (error != null) { + System.err.println("Error: " + error.getMessage()); + } 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 gg.top.api.entity.UserSource; -api.setStats(serverCount); +client.getVote(UserSource.TOPGG, "8226924471638491136").whenComplete((vote, error) -> { + if (error != null) { + System.err.println("Error: " + error.getMessage()); + } 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 gg.top.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**: This 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**: This 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 -[![Release](https://jitpack.io/v/top-gg/java-sdk.svg)](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 gg.top.api.TopggWidget; +import gg.top.api.entity.Platform; +import gg.top.api.entity.ProjectType; -#### Maven +final String widgetUrl = TopggWidget.large(Platform.DISCORD, ProjectType.BOT, "1026525568344264724"); +``` -```xml - - - jitpack.io - https://jitpack.io - - +#### Votes + +```java +import gg.top.api.TopggWidget; +import gg.top.api.entity.Platform; +import gg.top.api.entity.ProjectType; + +final String widgetUrl = TopggWidget.votes(Platform.DISCORD, ProjectType.BOT, "1026525568344264724"); ``` -```xml - - - com.github.top-gg - java-sdk - VERSION - - -``` - -#### Gradle -```gradle -repositories { - maven { url 'https://jitpack.io' } + +#### Owner + +```java +import gg.top.api.TopggWidget; +import gg.top.api.entity.Platform; +import gg.top.api.entity.ProjectType; + +final String widgetUrl = TopggWidget.owner(Platform.DISCORD, ProjectType.BOT, "1026525568344264724"); +``` + +#### Social + +```java +import gg.top.api.TopggWidget; +import gg.top.api.entity.Platform; +import gg.top.api.entity.ProjectType; + +final String widgetUrl = TopggWidget.social(Platform.DISCORD, ProjectType.BOT, "1026525568344264724"); +``` + +### Webhooks + +#### Dropwizard + +> **NOTE**: This requires the `dropwizardWebhooks` capability. + +In your `Webhooks.java`: + +```java +import gg.top.webhooks.dropwizard.TopggWebhooks; +import gg.top.webhooks.payload.IntegrationCreatePayload; +import gg.top.webhooks.payload.IntegrationDeletePayload; +import gg.top.webhooks.payload.TestPayload; +import gg.top.webhooks.payload.VoteCreatePayload; + +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; + +// POST /webhook +@Path("/webhook") +public class Webhooks extends TopggWebhooks { + 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**: This requires the `eclipseJettyWebhooks` capability. + +In your `Webhooks.java`: + +```java +import gg.top.webhooks.eclipsejetty.TopggWebhooks; +import gg.top.webhooks.payload.IntegrationCreatePayload; +import gg.top.webhooks.payload.IntegrationDeletePayload; +import gg.top.webhooks.payload.TestPayload; +import gg.top.webhooks.payload.VoteCreatePayload; + +import jakarta.servlet.http.HttpServletResponse; + +public class Webhooks extends TopggWebhooks { + 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**: This requires the `springBootWebhooks` capability. + +In your `Webhooks.java`: + +```java +import gg.top.webhooks.springboot.TopggWebhooks; +import gg.top.webhooks.payload.IntegrationCreatePayload; +import gg.top.webhooks.payload.IntegrationDeletePayload; +import gg.top.webhooks.payload.TestPayload; +import gg.top.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 TopggWebhooks { + 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(); + } +} +``` diff --git a/build.gradle b/build.gradle index 023b55d..1bcdb00 100644 --- a/build.gradle +++ b/build.gradle @@ -1,19 +1,223 @@ -group 'org.discordbots' +plugins { + id 'java-library' + id 'maven-publish' +} + +def ver = '1.0.0' + +group = 'gg.top' +version = ver +description = 'The community-maintained Java SDK 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('gg.top', 'jdaWrapper', ver) + } + + registerFeature('discord4jWrapper') { + usingSourceSet sourceSets.discord4jWrapper + + capability('gg.top', 'discord4jWrapper', ver) + } + + registerFeature('webhooks') { + usingSourceSet sourceSets.webhooks + + capability('gg.top', 'webhooks', ver) + } + + registerFeature('dropwizardWebhooks') { + usingSourceSet sourceSets.webhooks + usingSourceSet sourceSets.dropwizardWebhooks + + capability('gg.top', 'dropwizardWebhooks', ver) + } + + registerFeature('eclipseJettyWebhooks') { + usingSourceSet sourceSets.webhooks + usingSourceSet sourceSets.eclipseJettyWebhooks + + capability('gg.top', 'eclipseJettyWebhooks', ver) + } + + registerFeature('springBootWebhooks') { + usingSourceSet sourceSets.webhooks + usingSourceSet sourceSets.springBootWebhooks + + capability('gg.top', 'springBootWebhooks', ver) + } +} + +publishing { + publications { + maven(MavenPublication) { + artifactId = 'SDK' + groupId = 'gg.top' + version = ver + + from components.java + + pom { + name = 'SDK' + description = 'The community-maintained Java SDK 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' + api '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' + jdaWrapperApi('net.dv8tion:JDA:6.3.1') { + exclude module: 'opus-java' + exclude module: 'tink' + } + + discord4jWrapperImplementation sourceSets.main.output + discord4jWrapperApi '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' + dropwizardWebhooksImplementation 'com.google.guava:guava:33.5.0-jre' + + eclipseJettyWebhooksImplementation sourceSets.webhooks.output + eclipseJettyWebhooksApi 'jakarta.servlet:jakarta.servlet-api:6.1.0' + eclipseJettyWebhooksImplementation 'com.google.guava:guava:33.5.0-jre' - //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..c4bdd3a 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,4 +1,22 @@ -@if "%DEBUG%" == "" @echo off +@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 @@ -9,25 +27,29 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +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= +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%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute -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. +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 @@ -35,48 +57,35 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute -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. +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 -: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% +"%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%"=="0" goto mainEnd +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! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +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 diff --git a/src/discord4jWrapper/java/gg/top/api/discord4j/Discord4JPostCommandsTransformer.java b/src/discord4jWrapper/java/gg/top/api/discord4j/Discord4JPostCommandsTransformer.java new file mode 100644 index 0000000..d520b48 --- /dev/null +++ b/src/discord4jWrapper/java/gg/top/api/discord4j/Discord4JPostCommandsTransformer.java @@ -0,0 +1,37 @@ +package gg.top.api.discord4j; + +import com.fasterxml.jackson.core.JsonProcessingException; +import discord4j.common.JacksonResources; +import discord4j.core.DiscordClient; +import java.util.concurrent.CompletionStage; +import gg.top.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/jdaWrapper/java/gg/top/api/jda/JDAPostCommandsTransformer.java b/src/jdaWrapper/java/gg/top/api/jda/JDAPostCommandsTransformer.java new file mode 100644 index 0000000..f1027b5 --- /dev/null +++ b/src/jdaWrapper/java/gg/top/api/jda/JDAPostCommandsTransformer.java @@ -0,0 +1,34 @@ +package gg.top.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 gg.top.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/gg/top/api/TopggAPI.java b/src/main/java/gg/top/api/TopggAPI.java new file mode 100644 index 0000000..0652015 --- /dev/null +++ b/src/main/java/gg/top/api/TopggAPI.java @@ -0,0 +1,192 @@ +package gg.top.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 gg.top.api.entity.PaginatedVotes; +import gg.top.api.entity.PartialVote; +import gg.top.api.entity.Project; +import gg.top.api.entity.UserSource; +import gg.top.api.io.DefaultResponseTransformer; +import gg.top.api.io.EmptyResponseTransformer; +import gg.top.api.io.PaginatedVotesConverter; +import gg.top.api.io.PostCommandsTransformer; +import gg.top.api.io.RawPostCommandsTransformer; +import gg.top.api.io.ResponseTransformer; +import gg.top.api.io.UnsuccessfulHttpException; + +public class TopggAPI { + 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 TopggAPI(final OkHttpClient httpClient) { + gson = + new GsonBuilder() + .registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeConverter()) + .registerTypeAdapter(PaginatedVotes.class, new PaginatedVotesConverter(this)) + .create(); + + this.httpClient = httpClient; + } + + public TopggAPI(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/gg/top/api/TopggWidget.java b/src/main/java/gg/top/api/TopggWidget.java new file mode 100644 index 0000000..cb462ae --- /dev/null +++ b/src/main/java/gg/top/api/TopggWidget.java @@ -0,0 +1,24 @@ +package gg.top.api; + +import gg.top.api.entity.Platform; +import gg.top.api.entity.ProjectType; + +public final class TopggWidget { + private static final String BASE_URL = "https://top.gg/api/v1/widgets"; + + public static String large(final Platform platform, final ProjectType projectType, final String id) { + return String.format("%s/large/%s/%s/%s", BASE_URL, platform.name().toLowerCase(), projectType.name().toLowerCase(), id); + } + + public static String votes(final Platform platform, final ProjectType projectType, final String id) { + return String.format("%s/small/votes/%s/%s/%s", BASE_URL, platform.name().toLowerCase(), projectType.name().toLowerCase(), id); + } + + public static String owner(final Platform platform, final ProjectType projectType, final String id) { + return String.format("%s/small/owner/%s/%s/%s", BASE_URL, platform.name().toLowerCase(), projectType.name().toLowerCase(), id); + } + + public static String social(final Platform platform, final ProjectType projectType, final String id) { + return String.format("%s/small/social/%s/%s/%s", BASE_URL, platform.name().toLowerCase(), projectType.name().toLowerCase(), id); + } +} diff --git a/src/main/java/gg/top/api/entity/PaginatedVotes.java b/src/main/java/gg/top/api/entity/PaginatedVotes.java new file mode 100644 index 0000000..87191fb --- /dev/null +++ b/src/main/java/gg/top/api/entity/PaginatedVotes.java @@ -0,0 +1,25 @@ +package gg.top.api.entity; + +import java.util.List; +import java.util.concurrent.CompletionStage; +import gg.top.api.TopggAPI; + +public class PaginatedVotes { + private final List votes; + private final String cursor; + private final TopggAPI client; + + public PaginatedVotes(final List votes, final String cursor, final TopggAPI 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/gg/top/api/entity/PartialVote.java b/src/main/java/gg/top/api/entity/PartialVote.java new file mode 100644 index 0000000..bff2371 --- /dev/null +++ b/src/main/java/gg/top/api/entity/PartialVote.java @@ -0,0 +1,26 @@ +package gg.top.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/gg/top/api/entity/Platform.java b/src/main/java/gg/top/api/entity/Platform.java new file mode 100644 index 0000000..efc6a9c --- /dev/null +++ b/src/main/java/gg/top/api/entity/Platform.java @@ -0,0 +1,8 @@ +package gg.top.api.entity; + +import com.google.gson.annotations.SerializedName; + +public enum Platform { + @SerializedName("discord") + DISCORD +} diff --git a/src/main/java/gg/top/api/entity/Project.java b/src/main/java/gg/top/api/entity/Project.java new file mode 100644 index 0000000..bf5f000 --- /dev/null +++ b/src/main/java/gg/top/api/entity/Project.java @@ -0,0 +1,65 @@ +package gg.top.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/gg/top/api/entity/ProjectType.java b/src/main/java/gg/top/api/entity/ProjectType.java new file mode 100644 index 0000000..5572a7d --- /dev/null +++ b/src/main/java/gg/top/api/entity/ProjectType.java @@ -0,0 +1,11 @@ +package gg.top.api.entity; + +import com.google.gson.annotations.SerializedName; + +public enum ProjectType { + @SerializedName("bot") + BOT, + + @SerializedName("server") + SERVER +} diff --git a/src/main/java/gg/top/api/entity/UserSource.java b/src/main/java/gg/top/api/entity/UserSource.java new file mode 100644 index 0000000..dee03fb --- /dev/null +++ b/src/main/java/gg/top/api/entity/UserSource.java @@ -0,0 +1,11 @@ +package gg.top.api.entity; + +import com.google.gson.annotations.SerializedName; + +public enum UserSource { + @SerializedName("discord") + DISCORD, + + @SerializedName("topgg") + TOPGG +} diff --git a/src/main/java/gg/top/api/entity/Vote.java b/src/main/java/gg/top/api/entity/Vote.java new file mode 100644 index 0000000..37b9d17 --- /dev/null +++ b/src/main/java/gg/top/api/entity/Vote.java @@ -0,0 +1,20 @@ +package gg.top.api.entity; + +import com.google.gson.annotations.SerializedName; +import java.time.OffsetDateTime; + +public class Vote extends PartialVote { + @SerializedName("user_id") + private String voterId; + + @SerializedName("platform_id") + private String platformId; + + public String getVoterId() { + return voterId; + } + + public String getPlatformId() { + return platformId; + } +} diff --git a/src/main/java/gg/top/api/io/DefaultResponseTransformer.java b/src/main/java/gg/top/api/io/DefaultResponseTransformer.java new file mode 100644 index 0000000..e2fb6a5 --- /dev/null +++ b/src/main/java/gg/top/api/io/DefaultResponseTransformer.java @@ -0,0 +1,20 @@ +package gg.top.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/gg/top/api/io/EmptyResponseTransformer.java b/src/main/java/gg/top/api/io/EmptyResponseTransformer.java new file mode 100644 index 0000000..602e225 --- /dev/null +++ b/src/main/java/gg/top/api/io/EmptyResponseTransformer.java @@ -0,0 +1,10 @@ +package gg.top.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/gg/top/api/io/PaginatedVotesConverter.java b/src/main/java/gg/top/api/io/PaginatedVotesConverter.java new file mode 100644 index 0000000..0d5a55e --- /dev/null +++ b/src/main/java/gg/top/api/io/PaginatedVotesConverter.java @@ -0,0 +1,35 @@ +package gg.top.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 gg.top.api.TopggAPI; +import gg.top.api.entity.PaginatedVotes; +import gg.top.api.entity.Vote; + +public class PaginatedVotesConverter implements JsonDeserializer { + private final TopggAPI client; + + public PaginatedVotesConverter(final TopggAPI 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/gg/top/api/io/PostCommandsTransformer.java b/src/main/java/gg/top/api/io/PostCommandsTransformer.java new file mode 100644 index 0000000..9e473ec --- /dev/null +++ b/src/main/java/gg/top/api/io/PostCommandsTransformer.java @@ -0,0 +1,7 @@ +package gg.top.api.io; + +import java.util.concurrent.CompletionStage; + +public interface PostCommandsTransformer { + CompletionStage toJsonString(); +} diff --git a/src/main/java/gg/top/api/io/RawPostCommandsTransformer.java b/src/main/java/gg/top/api/io/RawPostCommandsTransformer.java new file mode 100644 index 0000000..429715f --- /dev/null +++ b/src/main/java/gg/top/api/io/RawPostCommandsTransformer.java @@ -0,0 +1,18 @@ +package gg.top.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/gg/top/api/io/ResponseTransformer.java b/src/main/java/gg/top/api/io/ResponseTransformer.java new file mode 100644 index 0000000..2fcf690 --- /dev/null +++ b/src/main/java/gg/top/api/io/ResponseTransformer.java @@ -0,0 +1,7 @@ +package gg.top.api.io; + +import okhttp3.Response; + +public interface ResponseTransformer { + E transform(final Response response) throws Exception; +} diff --git a/src/main/java/gg/top/api/io/UnsuccessfulHttpException.java b/src/main/java/gg/top/api/io/UnsuccessfulHttpException.java new file mode 100644 index 0000000..fe13262 --- /dev/null +++ b/src/main/java/gg/top/api/io/UnsuccessfulHttpException.java @@ -0,0 +1,18 @@ +package gg.top.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/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); - } - -}