From 7e4707c1703cef296ca975eb51a54ccf42c1aa28 Mon Sep 17 00:00:00 2001 From: Toastie Date: Sun, 24 Mar 2024 22:01:32 +1300 Subject: [PATCH] AleaAPI v2.2.1 --- .gitignore | 31 ++ build.gradle | 145 +++++++++ gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 234 +++++++++++++++ gradlew.bat | 89 ++++++ settings.gradle | 2 + .../com/toastiet0ast/aleaapi/APIInfo.java | 6 + .../com/toastiet0ast/aleaapi/AleaAPI.java | 279 ++++++++++++++++++ .../aleaapi/entities/AnimeData.java | 19 ++ .../aleaapi/entities/PokemonData.java | 37 +++ .../aleaapi/patreon/PatreonPledge.java | 41 +++ .../aleaapi/patreon/PatreonReceiver.java | 154 ++++++++++ .../aleaapi/patreon/PatreonReward.java | 32 ++ .../aleaapi/patreon/PledgeLoader.java | 84 ++++++ .../toastiet0ast/aleaapi/utils/Config.java | 85 ++++++ .../com/toastiet0ast/aleaapi/utils/Utils.java | 93 ++++++ src/main/resources/logback.xml | 26 ++ 17 files changed, 1364 insertions(+) create mode 100644 .gitignore create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/com/toastiet0ast/aleaapi/APIInfo.java create mode 100644 src/main/java/com/toastiet0ast/aleaapi/AleaAPI.java create mode 100644 src/main/java/com/toastiet0ast/aleaapi/entities/AnimeData.java create mode 100644 src/main/java/com/toastiet0ast/aleaapi/entities/PokemonData.java create mode 100644 src/main/java/com/toastiet0ast/aleaapi/patreon/PatreonPledge.java create mode 100644 src/main/java/com/toastiet0ast/aleaapi/patreon/PatreonReceiver.java create mode 100644 src/main/java/com/toastiet0ast/aleaapi/patreon/PatreonReward.java create mode 100644 src/main/java/com/toastiet0ast/aleaapi/patreon/PledgeLoader.java create mode 100644 src/main/java/com/toastiet0ast/aleaapi/utils/Config.java create mode 100644 src/main/java/com/toastiet0ast/aleaapi/utils/Utils.java create mode 100644 src/main/resources/logback.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..932a08a --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# Config files +*.json + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +/src/main/java/resources/* + +.idea/* +.gradle/* \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..9ab2957 --- /dev/null +++ b/build.gradle @@ -0,0 +1,145 @@ +import org.apache.tools.ant.filters.ReplaceTokens + +//Plugins +plugins { + //Compiles Java + id 'java' + //Adds an Executable Manifest + id 'application' + //Creates FatJars + id 'com.github.johnrengelman.shadow' version '8.1.1' +} + +archivesBaseName = 'AleaAPI' +group "toastiet0ast" +def ver = new Version(major: 2, minor: 2, revision: 1) +version ver.toString() +sourceCompatibility = 17 +targetCompatibility = 17 +mainClassName = "com.toastiet0ast.aleaapi.AleaAPI" + +def lint = [ + "auxiliaryclass", + "cast", + "classfile", + "deprecation", + "dep-ann", + "divzero", + "empty", + "exports", + "fallthrough", + "finally", + "module", + "opens", + "options", + "overloads", + "overrides", + "path", + //removed because of "No processor claimed any of these annotations: ..." + //"processing", + "rawtypes", + "removal", + "requires-automatic", + "requires-transitive-automatic", + "serial", + "static", + "try", + "unchecked", + "varargs", + "preview" +] + +repositories { + mavenCentral() + maven { url 'https://jitpack.io' } +} + +tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:deprecation" + options.encoding = "UTF-8" +} + +dependencies { + implementation 'com.sparkjava:spark-core:2.9.4' + implementation 'ch.qos.logback:logback-classic:1.2.11' + implementation ('com.github.adamint:patreon-java:417322b') { exclude group: 'org.slf4j' } + implementation group: 'org.json', name: 'json', version: '20220320' + implementation group: 'commons-io', name: 'commons-io', version: '2.11.0' + implementation group: 'commons-codec', name: 'commons-codec', version: '1.15' + implementation group: 'com.google.code.gson', name: 'gson', version: '2.9.1' + implementation group: 'redis.clients', name: 'jedis', version: '4.2.3' + implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.10.0' + testImplementation group: 'junit', name: 'junit', version: '4.13.2' +} + +build.dependsOn shadowJar + +artifacts { + archives shadowJar +} + +def gitRevision() { + def gitVersion = new ByteArrayOutputStream() + exec { + commandLine("git", "rev-parse", "--short", "HEAD") + standardOutput = gitVersion + } + + return gitVersion.toString().trim() +} + + +task sourcesForRelease(type: Copy) { + from ('src/main/java') { + include '**/APIInfo.java' + filter(ReplaceTokens, tokens: [ + version: ver.toString(), + revision: gitRevision().toString() + ]) + } + into 'build/filteredSrc' + + includeEmptyDirs = false +} + +task generateJavaSources(type: SourceTask) { + def javaSources = sourceSets.main.allJava.filter { + it.name != 'APIInfo.java' + } + source = javaSources + sourcesForRelease.destinationDir + + dependsOn sourcesForRelease +} + +compileJava { + source = generateJavaSources.source + classpath = sourceSets.main.compileClasspath + options.compilerArgs += ["-Xlint:${lint.join(",")}"] + + dependsOn generateJavaSources +} + + +task cleanDistTar(type: Delete) { delete files(distTar) } +distTar { archiveClassifier.set("trash") } +distTar.finalizedBy cleanDistTar + +task cleanDistZip(type: Delete) { delete files(distZip) } +distZip { archiveClassifier.set("trash") } +distZip.finalizedBy cleanDistZip + +task cleanUnshadedJar(type: Delete) { delete files(jar) } +jar { archiveClassifier.set("trash") } +jar.finalizedBy cleanUnshadedJar + +shadowJar { + archiveClassifier.set(null) +} + +class Version { + String major, minor, revision + + String toString() { + "${major}.${minor}.${revision}" + } +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..f80c569 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +#Sun Mar 24 19:00:14 NZDT 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists \ No newline at end of file diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 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. +# + +############################################################################## +# +# 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/master/subprojects/plugins/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 +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 + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${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"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +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 ;; #( + 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 + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +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. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +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" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + 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 + # 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 +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# 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 new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@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 + +@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 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%" == "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. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +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. + +goto fail + +: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 %* + +: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 diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..1795299 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = "alea-api" + diff --git a/src/main/java/com/toastiet0ast/aleaapi/APIInfo.java b/src/main/java/com/toastiet0ast/aleaapi/APIInfo.java new file mode 100644 index 0000000..8eda060 --- /dev/null +++ b/src/main/java/com/toastiet0ast/aleaapi/APIInfo.java @@ -0,0 +1,6 @@ +package com.toastiet0ast.aleaapi; + +public class APIInfo { + public static final String VERSION = "@version@"; + public static final String GIT_REVISION = "@revision@"; +} \ No newline at end of file diff --git a/src/main/java/com/toastiet0ast/aleaapi/AleaAPI.java b/src/main/java/com/toastiet0ast/aleaapi/AleaAPI.java new file mode 100644 index 0000000..139122b --- /dev/null +++ b/src/main/java/com/toastiet0ast/aleaapi/AleaAPI.java @@ -0,0 +1,279 @@ +package com.toastiet0ast.aleaapi; + +import com.toastiet0ast.aleaapi.entities.AnimeData; +import com.toastiet0ast.aleaapi.entities.PokemonData; +import com.toastiet0ast.aleaapi.patreon.PatreonPledge; +import com.toastiet0ast.aleaapi.patreon.PatreonReceiver; +import com.toastiet0ast.aleaapi.patreon.PatreonReward; +import com.toastiet0ast.aleaapi.patreon.PledgeLoader; +import com.toastiet0ast.aleaapi.utils.Config; +import com.toastiet0ast.aleaapi.utils.Utils; +import org.apache.commons.io.IOUtils; +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spark.Spark; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static spark.Spark.*; + +public class AleaAPI { + private final Logger logger = LoggerFactory.getLogger(AleaAPI.class); + private final List pokemon = new ArrayList<>(); + private final List characters = new ArrayList<>(); + private final List splashes = new ArrayList<>(); + + private final Random r = new Random(); + private JSONObject hush; //hush there, I know you're looking .w. + private Config config; + private int servedRequests; + + public static void main(String[] args) { + try { + new AleaAPI(); + } catch (Exception e) { + e.printStackTrace(); + System.exit(-1); + } + } + + private AleaAPI() throws Exception { + logger.info("\n" + + ":: Alea API {} ({}):: Made by Toastie ::\n", APIInfo.VERSION, APIInfo.GIT_REVISION); + try { + config = Utils.loadConfig(); + } catch (IOException e) { + logger.error("An error occurred while loading the configuration file!", e); + System.exit(100); + } + + //Load current pledges, if necessary. + Executors.newSingleThreadExecutor().submit(() -> PledgeLoader.checkPledges(logger, config, false)); + + //Check pledges every x days, if enabled. + if(config.isConstantCheck()) { + Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> + PledgeLoader.checkPledges(logger, config, true), config.getConstantCheckDelay(), config.getConstantCheckDelay(), TimeUnit.DAYS + ); + } + + //Read text/json files containing information related to what the API serves. + readFiles(); + + //Spark initialization. + ipAddress(config.getBindAddress()); + port(config.getPort()); + Spark.init(); + + //Receive webhooks from Patreon. + new PatreonReceiver(logger, config); + + get("/aleaapi/ping", (req, res) -> + new JSONObject().put("status", "ok") + .put("version", APIInfo.VERSION) + .put("rev", APIInfo.GIT_REVISION) + .put("requests_served", servedRequests) + .toString() + ); + + path("/aleaapi/bot", () -> { + //Spark why does this work like this but not without an argument, I'M LITERALLY GIVING YOU AN EMPTY STRING + before("", (request, response) -> { + handleAuthentication(request.headers("Authorization")); + servedRequests++; + }); + before("/*", (request, response) -> { + handleAuthentication(request.headers("Authorization")); + servedRequests++; + }); + + get("/pokemon", (req, res) -> { + try { + var pokemonData = pokemon.get(r.nextInt(pokemon.size())); + var image = pokemonData.getUrl(); + var name = pokemonData.getName(); + String[] names = pokemonData.getNames(); + + return new JSONObject() + .put("name", name) + .put("names", names) + .put("image", image) + .toString(); + } catch (Exception e) { + res.status(500); + return new JSONObject().put("error", e.getMessage()).toString(); + } + }); + + get("/character", (req, res) -> { + try { + var animeData = characters.get(r.nextInt(characters.size())); + var name = animeData.getName(); + var image = animeData.getUrl(); + + return new JSONObject() + .put("name", name) + .put("image", image) + .toString(); + } catch (Exception e) { + res.status(500); + return new JSONObject().put("error", e.getMessage()).toString(); + } + }); + + get("/pokemon/info", (req, res) -> new JSONObject() + .put("available", pokemon.size()) + .toString()); + + get("/splashes/info", (req, res) -> new JSONObject() + .put("available", splashes.size()) + .toString()); + + get("/character/info", (req, res) -> new JSONObject() + .put("available", characters.size()) + .toString()); + + get("/splashes/random", (req, res) -> new JSONObject().put("splash", splashes.get(r.nextInt(splashes.size()))).toString()); + + get("/patreon/refresh", (req, res) -> { + Executors.newSingleThreadExecutor().submit(() -> PledgeLoader.checkPledges(logger, config, true)); + return "{\"status\":\"ok\"}"; + }); + + // This is the old way. Maybe so I can test it on MP before pushing to Alea + // because if something goes wrong here, everything goes wrong. + // This basically just returns the amount. The active field is pretty useless, ngl. + post("/patreon/check", (req, res) -> { + var obj = new JSONObject(req.body()); + var id = obj.getString("id"); + var placeholder = new JSONObject().put("active", false).put("amount", "0").toString(); + + return Utils.accessRedis(jedis -> { + try { + if (!jedis.hexists("donators", id)) + return placeholder; + + // Using two different JSON libraries to accomplish this is surely peak bullshit. + var json = jedis.hget("donators", id); + var pledgeJSON = new JSONObject(json); + var active = pledgeJSON.getBoolean("active"); + var amount = Double.toString(pledgeJSON.getDouble("amount")); + + return new JSONObject().put("active", active).put("amount", amount); + } catch (Exception e) { + e.printStackTrace(); + halt(500); + return placeholder; + } + }); + }); + + // This returns the entire object, so we can analyze it properly. + // Mostly this is here so we can get the tier instead of just the amount, because patreon is + // very silly and doesn't give me the amount in USD anymore for international pledges. + post("/patreon/checknew", (req, res) -> { + var obj = new JSONObject(req.body()); + var id = obj.getString("id"); + var placeholder = new PatreonPledge(0, false, PatreonReward.NONE); + + return Utils.accessRedis(jedis -> { + try { + if(!jedis.hexists("donators", id)) + return new JSONObject(placeholder).toString(); + + // We can just return the whole object. + return new JSONObject(jedis.hget("donators", id)); + } catch (Exception e) { + e.printStackTrace(); + halt(500); + return placeholder; + } + }); + }); + + post("/hush", (req, res) -> { + var obj = new JSONObject(req.body()); + var name = obj.getString("name"); + var type = obj.getString("type").toLowerCase(); + + String answer; + try { + answer = hush.getJSONObject(type).getString(name.replace(" ", "_")); + } catch (JSONException e) { + res.status(500); + answer = "NONE"; + } + + return new JSONObject().put("hush", answer); + }); + }); + } + + //bootleg af honestly + private void handleAuthentication(String auth) { + if(!config.getAuth().equals(auth)) + halt(403); + } + + public void readFiles() throws IOException { + logger.info("Reading pokemon data << pokemon_data.txt"); + var stream = getClass().getClassLoader().getResourceAsStream("pokemon_data.txt"); + if (stream != null) { + var pokemonLines = IOUtils.readLines(stream, StandardCharsets.UTF_8); + for (var s : pokemonLines) { + var data = s.replace("\r", "").split("`"); + var names = Arrays.copyOfRange(data, 1, data.length); + var image = data[0]; + + pokemon.add(new PokemonData(names[0], image, names)); + } + } else { + logger.error("Error loading Pokemon data!"); + } + + logger.info("Reading anime data << anime_data.txt"); + var animeStream = getClass().getClassLoader().getResourceAsStream("anime_data.txt"); + if (animeStream != null) { + var animeLines = IOUtils.readLines(animeStream, StandardCharsets.UTF_8); + for (var s : animeLines) { + var data = s.replace("\r", "").split(";"); + var name = data[0]; + var image = data[1]; + characters.add(new AnimeData(name, image)); + } + } else { + logger.error("Error loading anime data!"); + } + + logger.info("Reading hush data << hush.json"); + var hushStream = getClass().getClassLoader().getResourceAsStream("hush.json"); + if (hushStream != null) { + var hushLines = IOUtils.readLines(hushStream, StandardCharsets.UTF_8); + hush = new JSONObject(String.join("", hushLines)); + } else { + logger.error("Error loading hush badges!"); + } + + logger.info("Reading splashes data << splashes.txt"); + var splashesStream = getClass().getClassLoader().getResourceAsStream("splashes.txt"); + if (splashesStream != null) { + var splashesLines = IOUtils.readLines(splashesStream, StandardCharsets.UTF_8); + for (var s : splashesLines) { + splashes.add(s.replace("\r", "")); + } + + splashes.removeIf(s -> s == null || s.isEmpty()); + } else { + logger.error("Error loading splashes!"); + } + } +} diff --git a/src/main/java/com/toastiet0ast/aleaapi/entities/AnimeData.java b/src/main/java/com/toastiet0ast/aleaapi/entities/AnimeData.java new file mode 100644 index 0000000..4d33b30 --- /dev/null +++ b/src/main/java/com/toastiet0ast/aleaapi/entities/AnimeData.java @@ -0,0 +1,19 @@ +package com.toastiet0ast.aleaapi.entities; + +public class AnimeData { + private final String name; + private final String url; + + public AnimeData(String name, String url) { + this.name = name; + this.url = url; + } + + public String getName() { + return name; + } + + public String getUrl() { + return url; + } +} diff --git a/src/main/java/com/toastiet0ast/aleaapi/entities/PokemonData.java b/src/main/java/com/toastiet0ast/aleaapi/entities/PokemonData.java new file mode 100644 index 0000000..3659d80 --- /dev/null +++ b/src/main/java/com/toastiet0ast/aleaapi/entities/PokemonData.java @@ -0,0 +1,37 @@ +package com.toastiet0ast.aleaapi.entities; + +public class PokemonData { + private String name; + private String url; + private String[] names; + + public PokemonData(String name, String url, String[] names) { + this.name = name; + this.url = url; + this.names = names; + } + + public String getName() { + return name; + } + + public String getUrl() { + return url; + } + + public String[] getNames() { + return names; + } + + public void setName(String name) { + this.name = name; + } + + public void setUrl(String url) { + this.url = url; + } + + public void setNames(String[] names) { + this.names = names; + } +} \ No newline at end of file diff --git a/src/main/java/com/toastiet0ast/aleaapi/patreon/PatreonPledge.java b/src/main/java/com/toastiet0ast/aleaapi/patreon/PatreonPledge.java new file mode 100644 index 0000000..2f0cd25 --- /dev/null +++ b/src/main/java/com/toastiet0ast/aleaapi/patreon/PatreonPledge.java @@ -0,0 +1,41 @@ +package com.toastiet0ast.aleaapi.patreon; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.beans.ConstructorProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class PatreonPledge { + private double amount; + private PatreonReward reward; + private final boolean active; + + @ConstructorProperties({"amount", "active", "reward"}) + @JsonCreator + public PatreonPledge(double amount, boolean active, PatreonReward reward) { + this.amount = amount; + this.reward = reward; + this.active = active; + } + + public boolean isActive() { + return active; + } + + public double getAmount() { + return amount; + } + + public void setAmount(double amount) { + this.amount = amount; + } + + public PatreonReward getReward() { + return reward; + } + + public void setReward(PatreonReward reward) { + this.reward = reward; + } +} diff --git a/src/main/java/com/toastiet0ast/aleaapi/patreon/PatreonReceiver.java b/src/main/java/com/toastiet0ast/aleaapi/patreon/PatreonReceiver.java new file mode 100644 index 0000000..9673a09 --- /dev/null +++ b/src/main/java/com/toastiet0ast/aleaapi/patreon/PatreonReceiver.java @@ -0,0 +1,154 @@ +package com.toastiet0ast.aleaapi.patreon; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.toastiet0ast.aleaapi.utils.Config; +import com.toastiet0ast.aleaapi.utils.Utils; +import org.apache.commons.codec.digest.HmacAlgorithms; +import org.apache.commons.codec.digest.HmacUtils; +import org.json.JSONObject; +import org.slf4j.Logger; + +import java.security.MessageDigest; + +import static spark.Spark.halt; +import static spark.Spark.post; + +// This is painful.. +public class PatreonReceiver { + private static final String OK_RESPONSE = "{\"status\":\"ok\"}"; + + public PatreonReceiver(Logger logger, Config config) { + //Handle patreon webhooks. + post("/aleaapi/patreon", (req, res) -> { + final var body = req.body(); + final var signature = req.headers("X-Patreon-Signature"); + + if(signature == null) { + logger.warn("Patreon webhook had no signature! Probably fake or invalid request."); + logger.warn("Patreon webhook data: {}", req.body()); + halt(401); + return ""; + } + + final var hmac = new HmacUtils(HmacAlgorithms.HMAC_MD5, config.getPatreonSecret()).hmacHex(body); + if(!MessageDigest.isEqual(hmac.getBytes(), signature.getBytes())) { + logger.warn("Patreon webhook signature was invalid! Probably fake or invalid request."); + halt(401); + return ""; + } else { + logger.info("Accepted Patreon signed data."); + logger.debug("Accepted Patreon signed data <- {}", body); + } + + // Events are pledges:{create,update,delete} + final var patreonEvent = req.headers("X-Patreon-Event"); + final var json = JsonParser.parseString(body).getAsJsonObject(); + + try { + //what the fuck + final var patronId = json + .get("data").getAsJsonObject() + .get("relationships").getAsJsonObject() + .get("patron").getAsJsonObject() + .get("data").getAsJsonObject() + .get("id").getAsString(); + + final var included = json + .get("included").getAsJsonArray() + .iterator(); + + JsonObject patronObject = null; + while(included.hasNext()) { + final var next = included.next(); + final var includedObject = next.getAsJsonObject(); + if(includedObject.get("id").getAsString().equals(patronId)) { + patronObject = includedObject; + break; + } + } + + if(patronObject != null) { + final var pledgeAmountCents = json + .get("data").getAsJsonObject() + .get("attributes").getAsJsonObject() + .get("amount_cents").getAsLong(); + + final var socialConnections = patronObject + .get("attributes").getAsJsonObject() + .get("social_connections").getAsJsonObject(); + + // This might be discord: null if there's nothing. We can't get the JSONObject until we know it's not null + // else we get an exception thrown here. + final var discordConnection = socialConnections.get("discord"); + if (discordConnection == null) { + logger.info("Received Patreon event {}, but without a Discord ID. Cannot process.", patreonEvent); + return OK_RESPONSE; + } + + final var socialConnection = discordConnection.getAsJsonObject(); + final var discordUserId = socialConnection.get("user_id").getAsString(); + final double pledgeAmountDollars = pledgeAmountCents / 100D; + logger.info("Received Patreon event '{}' for Discord ID '{}' with amount ${}", patreonEvent, + discordUserId, String.format("%.2f", pledgeAmountDollars) + ); + + switch(patreonEvent) { + case "pledges:create": + case "pledges:update": + //what the fuck part 2 + final String pledgeReward = json + .get("data").getAsJsonObject() + .get("relationships").getAsJsonObject() + .get("reward").getAsJsonObject() + .get("data").getAsJsonObject() + .get("id").getAsString(); + + // pledge updated / created, we need to set it on both cases. + if (pledgeReward == null) { + logger.error("Unknown reward. Can't find it?"); + return OK_RESPONSE; + } + + long tier = Long.parseLong(pledgeReward); + // Why does this exist? + if (tier == 0 || tier == -1) { + logger.error("Unknown tier reward for {} (tier == 0 | tier == -1)", discordUserId); + return OK_RESPONSE; + } + + var patreonReward = PatreonReward.fromId(tier); + if (patreonReward == null) { + logger.error("Unknown reward? Can't convert to enum! {}", tier); + return OK_RESPONSE; + } + + var pledgeObject = new PatreonPledge(pledgeAmountDollars, true, patreonReward); + Utils.accessRedis(jedis -> + jedis.hset("donators", discordUserId, new JSONObject(pledgeObject).toString()) + ); + + logger.info("Added pledge data: Discord ID: {}, Pledge tier: {}", discordUserId, patreonReward); + break; + case "pledges:delete": + Utils.accessRedis(jedis -> + jedis.hdel("donators", discordUserId) + ); + + break; + default: + logger.info("Got unknown patreon event {} for Discord ID: {}", patreonEvent, discordUserId); + break; + } + } else { + logger.info("Null patron object?"); + } + } catch(final Exception e) { + logger.error("(!!!) Failed to process data, dumping <- {}", body); + e.printStackTrace(); + } + + return OK_RESPONSE; + }); + } +} diff --git a/src/main/java/com/toastiet0ast/aleaapi/patreon/PatreonReward.java b/src/main/java/com/toastiet0ast/aleaapi/patreon/PatreonReward.java new file mode 100644 index 0000000..74386ac --- /dev/null +++ b/src/main/java/com/toastiet0ast/aleaapi/patreon/PatreonReward.java @@ -0,0 +1,32 @@ +package com.toastiet0ast.aleaapi.patreon; + +public enum PatreonReward { + NONE(-100), + SUPPORTER(1487355), + FRIEND(1487342), + PATREON_BOT(1700160), + MILESTONER(1487346), + SERVER_SUPPORTER(1487350), + AWOOSOME(1670005), + FUNDER(1670076), + BUT_WHY(1669990); + + private final long id; + PatreonReward(long id) { + this.id = id; + } + + public long getId() { + return id; + } + + public static PatreonReward fromId(long id) { + for (PatreonReward pr : values()) { + if (pr.getId() == id) { + return pr; + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/toastiet0ast/aleaapi/patreon/PledgeLoader.java b/src/main/java/com/toastiet0ast/aleaapi/patreon/PledgeLoader.java new file mode 100644 index 0000000..19fe97c --- /dev/null +++ b/src/main/java/com/toastiet0ast/aleaapi/patreon/PledgeLoader.java @@ -0,0 +1,84 @@ +package com.toastiet0ast.aleaapi.patreon; + +import com.patreon.PatreonAPI; +import com.toastiet0ast.aleaapi.utils.Config; +import com.toastiet0ast.aleaapi.utils.Utils; +import org.json.JSONObject; +import org.slf4j.Logger; + +import java.util.concurrent.atomic.AtomicInteger; + +public class PledgeLoader { + public static void checkPledges(Logger logger, Config config, boolean force) { + if (config.checkOldPatrons() || force) { + try { + logger.info("Checking pledges..."); + var patreonAPI = new PatreonAPI(config.getPatreonToken()); + + // This is for debugging reasons. Patreon's API is as painful as it gets. + var rewards = patreonAPI.fetchCampaigns().get().get(0).getRewards(); + logger.info("Printing current known rewards:"); + for (var r : rewards) { + System.out.println(r.getId() + " " + r.getTitle()); + } + + var pledges = patreonAPI.fetchAllPledges("328369"); + var active = new AtomicInteger(); + for (var pledge : pledges) { + var declinedSince = pledge.getDeclinedSince(); + if (declinedSince == null) { + var discordId = pledge.getPatron().getDiscordId(); + + //come on guys, use integrations + if (discordId != null) { + double amount = pledge.getAmountCents() / 100D; + var reward = pledge.getReward(); + if (reward == null) { + logger.error("(!!) Unknown tier reward for {}", discordId); + return; + } + + var tier = Long.parseLong(reward.getId()); + if (tier == 0 || tier == -1) { + logger.error("(!!) Unknown tier reward for {} (tier == 0 | tier == -1)", discordId); + return; + } + + var tierName = reward.getTitle(); + var patreonReward = PatreonReward.fromId(tier); + logger.info("!! Processed pledge for {} for ${} -- Tier: {} ({} / {})", discordId, amount, tier, patreonReward, tierName); + Utils.accessRedis(jedis -> { + if (jedis.hget("donators", discordId) == null) { + logger.info("(!!) Processed new: Pledge email {}, discordId {}", pledge.getPatron().getEmail(), discordId); + } + + if (patreonReward == null) { + logger.error("Unknown reward? Can't convert to enum! {}", tier); + return null; + } + + var pledgeObject = new PatreonPledge(amount, true, patreonReward); + active.getAndIncrement(); + return jedis.hset("donators", discordId, new JSONObject(pledgeObject).toString()); + }); + } + } else { + Utils.accessRedis(jedis -> { + String discordId = pledge.getPatron().getDiscordId(); + + if(discordId != null && jedis.hexists("donators", discordId)) { + return jedis.hdel("donators", discordId); + } + + return null; + }); + } + } + + logger.info("(!!!) Updated all pledges! Total active: {}", active.get()); + } catch (Exception e) { + e.printStackTrace(); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/toastiet0ast/aleaapi/utils/Config.java b/src/main/java/com/toastiet0ast/aleaapi/utils/Config.java new file mode 100644 index 0000000..df7de6e --- /dev/null +++ b/src/main/java/com/toastiet0ast/aleaapi/utils/Config.java @@ -0,0 +1,85 @@ +package com.toastiet0ast.aleaapi.utils; + +public class Config { + private String patreonSecret; + private int port; + private String patreonToken; + private boolean checkOldPatrons; + private String auth; + private String userAgent; + private boolean constantCheck; + private int constantCheckDelay; + private String bindAddress; + + public String getUserAgent() { + return userAgent; + } + + public void setUserAgent(String userAgent) { + this.userAgent = userAgent; + } + + public String getPatreonSecret() { + return patreonSecret; + } + + public void setPatreonSecret(String patreonSecret) { + this.patreonSecret = patreonSecret; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getPatreonToken() { + return patreonToken; + } + + public void setPatreonToken(String patreonToken) { + this.patreonToken = patreonToken; + } + + public boolean checkOldPatrons() { + return checkOldPatrons; + } + + public void setCheckOldPatrons(boolean checkOldPatrons) { + this.checkOldPatrons = checkOldPatrons; + } + + public String getAuth() { + return auth; + } + + public void setAuth(String auth) { + this.auth = auth; + } + + public boolean isConstantCheck() { + return constantCheck; + } + + public void setConstantCheck(boolean constantCheck) { + this.constantCheck = constantCheck; + } + + public int getConstantCheckDelay() { + return constantCheckDelay; + } + + public void setConstantCheckDelay(int constantCheckDelay) { + this.constantCheckDelay = constantCheckDelay; + } + + public String getBindAddress() { + return bindAddress; + } + + public void setBindAddress(String bindAddress) { + this.bindAddress = bindAddress; + } +} \ No newline at end of file diff --git a/src/main/java/com/toastiet0ast/aleaapi/utils/Utils.java b/src/main/java/com/toastiet0ast/aleaapi/utils/Utils.java new file mode 100644 index 0000000..e5e8da9 --- /dev/null +++ b/src/main/java/com/toastiet0ast/aleaapi/utils/Utils.java @@ -0,0 +1,93 @@ +package com.toastiet0ast.aleaapi.utils; + +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +import java.io.*; +import java.nio.charset.Charset; +import java.time.Duration; +import java.util.function.Function; + +public class Utils { + private static final Logger logger = LoggerFactory.getLogger(Utils.class); + private static final JedisPoolConfig poolConfig = buildPoolConfig(); + private static final JedisPool jedisPool = new JedisPool(poolConfig, "redis://localhost:6379"); + private static JedisPoolConfig buildPoolConfig() { + final JedisPoolConfig poolConfig = new JedisPoolConfig(); + poolConfig.setMaxTotal(128); + poolConfig.setMaxIdle(128); + poolConfig.setMinIdle(16); + poolConfig.setTestOnBorrow(true); + poolConfig.setTestOnReturn(true); + poolConfig.setTestWhileIdle(true); + poolConfig.setMinEvictableIdleTime(Duration.ofSeconds(60)); + poolConfig.setTimeBetweenEvictionRuns(Duration.ofSeconds(30)); + poolConfig.setNumTestsPerEvictionRun(3); + poolConfig.setBlockWhenExhausted(true); + return poolConfig; + } + + //Load the config from file. + public static Config loadConfig() throws IOException { + Config cfg = new Config(); + logger.info("Loading configuration file << api.json"); + File config = new File("api.json"); + if(!config.exists()) { + JSONObject obj = new JSONObject(); + obj.put("patreon_secret", "secret"); + obj.put("patreon_token", "token"); + obj.put("port", 5874); + obj.put("check", true); + obj.put("auth", "uuid"); + obj.put("user_agent", "agent"); + obj.put("constant_check", false); + obj.put("constant_check_delay_days", 1); + + FileOutputStream fos = new FileOutputStream(config); + ByteArrayInputStream bais = new ByteArrayInputStream(obj.toString(4).getBytes(Charset.defaultCharset())); + byte[] buffer = new byte[1024]; + int read; + while((read = bais.read(buffer)) != -1) + fos.write(buffer, 0, read); + fos.close(); + logger.error("Could not find config file at " + config.getAbsolutePath() + ", creating a new one..."); + logger.error("Generated new config file at " + config.getAbsolutePath() + "."); + logger.error("Please, fill the file with valid properties."); + System.exit(-1); + } + + JSONObject obj; { + try (FileInputStream fis = new FileInputStream(config)) { + byte[] buffer = new byte[1024]; + int read; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + while((read = fis.read(buffer)) != -1) + baos.write(buffer, 0, read); + obj = new JSONObject(baos.toString(Charset.defaultCharset())); + } + } + + cfg.setPatreonSecret(obj.getString("patreon_secret")); + cfg.setPort(obj.getInt("port")); + cfg.setPatreonToken(obj.getString("patreon_token")); + cfg.setCheckOldPatrons(obj.getBoolean("check")); + cfg.setConstantCheck(obj.getBoolean("constant_check")); + cfg.setConstantCheckDelay(obj.getInt("constant_check_delay_days")); + cfg.setAuth(obj.getString("auth")); + cfg.setUserAgent(obj.getString("user_agent")); + cfg.setBindAddress(obj.getString("bind_address")); + + return cfg; + } + + public static T accessRedis(Function consumer) { + try(Jedis jedis = jedisPool.getResource()) { + logger.debug("Accessing redis instance"); + return consumer.apply(jedis); + } + } +} \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..b1dbea1 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,26 @@ + + + + INFO + + logs/api.log + + logs/alea.%d{yyyyMMdd}.log + 7 + + + [%d{HH:mm:ss}] [%t/%level] [%logger{0}] %X{jda.shard}: %msg%n + + + + + + [%d{HH:mm:ss}] [%t] [%level] [%logger{0}]: %msg%n + + + + + + + + \ No newline at end of file