diff --git a/.gitignore b/.gitignore index 1fac4d5..f304b8a 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,178 @@ bin/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store + + +# Created by https://www.toptal.com/developers/gitignore/api/java,gradle,intellij +# Edit at https://www.toptal.com/developers/gitignore?templates=java,gradle,intellij + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +### Java ### +# 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 + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +### Gradle ### +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### Gradle Patch ### +# Java heap dump +*.hprof + +# End of https://www.toptal.com/developers/gitignore/api/java,gradle,intellij + +lib \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..f16dea7 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..09883b6 --- /dev/null +++ b/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'com.gradleup.shadow' version '8.3.5' + id 'java' +} + +group = 'com.leohabrom' +version = '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +tasks.withType(JavaExec) { + systemProperty "java.library.path", file("${projectDir}/lib").absolutePath +} +dependencies { + testImplementation platform('org.junit:junit-bom:5.10.0') + testImplementation 'org.junit.jupiter:junit-jupiter' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation files('lib/DiscordLib.jar') + implementation 'com.google.code.gson:gson:2.13.2' + implementation 'com.github.hypfvieh:dbus-java-core:5.2.0' + implementation 'com.github.hypfvieh:dbus-java-transport-native-unixsocket:5.2.0' +} + +test { + useJUnitPlatform() +} + +tasks.shadowJar { + manifest { + attributes["Main-Class"] = "com.leohabrom.discordrpc.Main" + } +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e379eb1 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Apr 04 01:38:12 CEST 2026 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 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/move.sh b/move.sh new file mode 100755 index 0000000..adde7ac --- /dev/null +++ b/move.sh @@ -0,0 +1,3 @@ +#!/bin/bash +mv build/libs/DiscordActivity-1.0-SNAPSHOT-all.jar ~/.local/share/applications/discordRpc.jar +discordRpc \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..c5f2bc1 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'DiscordActivity' \ No newline at end of file diff --git a/src/main/java/com/leohabrom/discordrpc/Activity.java b/src/main/java/com/leohabrom/discordrpc/Activity.java new file mode 100644 index 0000000..41d3f5a --- /dev/null +++ b/src/main/java/com/leohabrom/discordrpc/Activity.java @@ -0,0 +1,48 @@ +package com.leohabrom.discordrpc; + +import com.leohabrom.discord.DiscordActivity; + +public abstract class Activity { + protected DiscordActivity activity = new DiscordActivity(); + + public abstract boolean isActive(); + public abstract void update(); + public abstract boolean hasTimer(); + public abstract long getTimer(); + + public DiscordActivity getDiscordActivity() { + return activity; + } + + @Override + public String toString() { + return "DiscordActivity{" + + "name='" + activity.name + '\'' + + ", state='" + activity.state + '\'' + + ", stateUrl='" + activity.stateUrl + '\'' + + ", details='" + activity.details + '\'' + + ", detailsUrl='" + activity.detailsUrl + '\'' + + ", type=" + activity.type + + ", status=" + activity.status + + ", platform=" + activity.platform + + ", largeImage='" + activity.largeImage + '\'' + + ", largeText='" + activity.largeText + '\'' + + ", largeUrl='" + activity.largeUrl + '\'' + + ", smallImage='" + activity.smallImage + '\'' + + ", smallText='" + activity.smallText + '\'' + + ", smallUrl='" + activity.smallUrl + '\'' + + ", inviteCoverImage='" + activity.inviteCoverImage + '\'' + + ", partyId='" + activity.partyId + '\'' + + ", partyCurrentSize=" + activity.partyCurrentSize + + ", partyMaxSize=" + activity.partyMaxSize + + ", partyPrivacy=" + activity.partyPrivacy + + ", startTimestamp=" + activity.startTimestamp + + ", endTimestamp=" + activity.endTimestamp + + ", firstButtonLabel='" + activity.firstButtonLabel + '\'' + + ", firstButtonUrl='" + activity.firstButtonUrl + '\'' + + ", secondButtonLabel='" + activity.secondButtonLabel + '\'' + + ", secondButtonUrl='" + activity.secondButtonUrl + '\'' + + ", joinSecret='" + activity.joinSecret + '\'' + + '}'; + } +} diff --git a/src/main/java/com/leohabrom/discordrpc/Bridge.java b/src/main/java/com/leohabrom/discordrpc/Bridge.java new file mode 100644 index 0000000..a7c721c --- /dev/null +++ b/src/main/java/com/leohabrom/discordrpc/Bridge.java @@ -0,0 +1,88 @@ +package com.leohabrom.discordrpc; + +import com.leohabrom.discord.DiscordActivity; +import com.leohabrom.discord.DiscordBridge; +import com.leohabrom.discord.DiscordBridgeAdapter; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +public class Bridge implements DiscordBridgeAdapter { + private final DiscordBridge bridge; + private final BlockingQueue feedbackQueue = new ArrayBlockingQueue<>(1); + + // Scheduling tools + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + private final AtomicLong lastSendTime = new AtomicLong(0); + private final AtomicReference pendingActivity = new AtomicReference<>(); + private final AtomicReference> scheduledTask = new AtomicReference<>(); + + private DiscordActivity lastSentActivity = null; + + public Bridge(long appId) { + this.bridge = new DiscordBridge(); + this.bridge.init(appId); + this.bridge.setAdapter(this); + this.bridge.start(); + } + + /** + * Updates the activity. If called too fast, it buffers the latest + * request and sends it as soon as the rate limit allows. + */ + public void update(DiscordActivity activity) { + pendingActivity.set(activity); + + long now = System.currentTimeMillis(); + long timeSinceLastSend = now - lastSendTime.get(); + long delayNeeded = 1000 - timeSinceLastSend; + + if (delayNeeded <= 0) { + sendNow(); + } else { + // If a task is already scheduled, don't create another one. + // The scheduled task will naturally pick up the "latest" pendingActivity. + if (scheduledTask.get() == null || scheduledTask.get().isDone()) { + ScheduledFuture task = scheduler.schedule(this::sendNow, delayNeeded, TimeUnit.MILLISECONDS); + scheduledTask.set(task); + } + } + } + + private void sendNow() { + DiscordActivity activity = pendingActivity.getAndSet(null); + if (activity != null) { + if (activity.equals(lastSentActivity)) { + return; + } + lastSentActivity = activity; + lastSendTime.set(System.currentTimeMillis()); + bridge.update(activity); + } + } + + public boolean updateWithFeedback(DiscordActivity activity) { + feedbackQueue.clear(); + update(activity); + try { + // We wait a bit longer because the update might be delayed by the scheduler + Boolean result = feedbackQueue.poll(3, TimeUnit.SECONDS); + return result != null && result; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + + @Override + public void onMessage(String message) { + if (message == null) return; + String msg = message.toLowerCase(); + if (msg.contains("sent") || msg.contains("success")) { + feedbackQueue.offer(true); + } else if (msg.contains("fail") || msg.contains("error")) { + feedbackQueue.offer(false); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/leohabrom/discordrpc/Main.java b/src/main/java/com/leohabrom/discordrpc/Main.java new file mode 100644 index 0000000..7fb7ae6 --- /dev/null +++ b/src/main/java/com/leohabrom/discordrpc/Main.java @@ -0,0 +1,113 @@ +package com.leohabrom.discordrpc; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.stream.JsonReader; +import com.leohabrom.discordrpc.activities.*; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.util.ArrayList; +import java.util.List; + +public class Main { + private static final long ROTATION_INTERVAL_MS = 15_000; // 15 seconds + private static String OCTOPRINT_HOST = ""; + private static String OCTOPRINT_APIKEY = ""; + public static final File CONFIG_DIR = new File(System.getProperty("user.home"),".config/discordrpc"); + + public static void main(String[] args) throws FileNotFoundException { + Gson gson = new Gson(); + File keys = new File(CONFIG_DIR,"keys.json"); + JsonObject object = gson.fromJson(new JsonReader(new FileReader(keys)), JsonObject.class); + OCTOPRINT_APIKEY = object.get("octoprintKey").getAsString(); + OCTOPRINT_HOST = object.get("octoprintHost").getAsString(); + long appId = object.get("appId").getAsLong(); + + Bridge bridge = new Bridge(appId); + + List registry = new ArrayList<>(); + registry.add(new IdlingActivity()); + registry.add(new YouTubeMusic()); + registry.add(new PrinterActivity(OCTOPRINT_HOST,OCTOPRINT_APIKEY)); + registry.add(new GenericChromeActivity()); + registry.add(new SnaktActivity()); + registry.add(new MensaActivity()); + registry.add(new IntelliJActivity()); + registry.add(new VSCodeActivity()); + registry.add(new MinecraftActivity()); + registry.add(new MensazeitActivity()); + registry.add(new RiftboundActivity()); + + if (CONFIG_DIR.exists()) { + File customActivities = new File(CONFIG_DIR,"custom"); + if (customActivities.exists()) { + File[] files = customActivities.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) continue; + if (file.getName().toLowerCase().contains(".json")) registry.add(new CustomActivity(file)); + } + } + } + } + + int currentIndex = 0; + long lastSwitchTime = 0; + + System.out.println("Discord RPC Service Running..."); + + + + while (true) { + // 1. Get all activities that are currently active + List activeActivities = new ArrayList<>(); + for (Activity activity : registry) { + try { + if (activity.isActive()) { + activeActivities.add(activity); + } + } catch (Exception e) { + System.err.println("❌ Error checking activity: " + activity.getClass().getSimpleName()); + e.printStackTrace(); + } + } + + if (activeActivities.size() > 1) { + activeActivities.removeIf(a -> a instanceof IdlingActivity); + } + + if (!activeActivities.isEmpty()) { + // 2. Determine if it's time to switch to the next activity + long now = System.currentTimeMillis(); + if (now - lastSwitchTime > (currentIndex < activeActivities.size() && activeActivities.get(currentIndex).hasTimer() ? activeActivities.get(currentIndex).getTimer() : ROTATION_INTERVAL_MS)) { + if (currentIndex < activeActivities.size() && activeActivities.get(currentIndex) instanceof MensaActivity m) { + m.inc(); + } + currentIndex = (currentIndex + 1) % activeActivities.size(); + lastSwitchTime = now; + } + + // Ensure index stays in bounds if the list size changed + if (currentIndex >= activeActivities.size()) { + currentIndex = 0; + } + + // 3. Update and send the current activity in the rotation + Activity current = activeActivities.get(currentIndex); + current.update(); + bridge.update(current.getDiscordActivity()); + } + + try { + // Poll every 2 seconds to check for state changes, + // but only rotate every 15 seconds. + Thread.sleep(3000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/leohabrom/discordrpc/Test.java b/src/main/java/com/leohabrom/discordrpc/Test.java new file mode 100644 index 0000000..369704e --- /dev/null +++ b/src/main/java/com/leohabrom/discordrpc/Test.java @@ -0,0 +1,51 @@ +package com.leohabrom.discordrpc; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.leohabrom.discordrpc.activities.CustomActivity; +import com.leohabrom.discordrpc.interfaces.DBusInterface; +import com.leohabrom.discordrpc.interfaces.RiftbountClient; + +import java.io.File; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Random; + +import static com.leohabrom.discordrpc.Main.CONFIG_DIR; + + +public class Test { + public static void main(String[] args) { + new DBusInterface("ignored").test(); + Random random = new Random(); + System.out.println(URI.create("https://cover-lookup.leohabrom.com/" + URLEncoder.encode("Astral-acuna.png", StandardCharsets.UTF_8))); + RiftbountClient client = new RiftbountClient("https://cc-backend.leohabrom.com"); + JsonObject riftBoundData = client.getUserProfile("zxrotwo002"); + if (!riftBoundData.has("favorites")) return; + JsonArray favorites = riftBoundData.get("favorites").getAsJsonArray(); + if (favorites.isEmpty()) return; + JsonElement element = favorites.get(random.nextInt(favorites.size())); + String id = element.getAsString(); + JsonObject card = client.getCard(id); + String url = card.get("image").getAsString(); + url = url.startsWith("https://") ? url : "https://cc.leohabrom.com/" + url; + System.out.println(url); + + System.out.println(CONFIG_DIR.getAbsolutePath()); + if (CONFIG_DIR.exists()) { + File customActivities = new File(CONFIG_DIR,"custom"); + System.out.println(customActivities); + if (customActivities.exists()) { + File[] files = customActivities.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) continue; + if (file.getName().toLowerCase().contains(".json")) System.out.println(file.getAbsolutePath());; + } + } + } + } + } +} diff --git a/src/main/java/com/leohabrom/discordrpc/Util.java b/src/main/java/com/leohabrom/discordrpc/Util.java new file mode 100644 index 0000000..3a077ad --- /dev/null +++ b/src/main/java/com/leohabrom/discordrpc/Util.java @@ -0,0 +1,34 @@ +package com.leohabrom.discordrpc; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.List; + +public class Util { + public static String executeCommand(String command) { + StringBuilder output = new StringBuilder(); + try { + Process process = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", command}); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + } + } + process.waitFor(); + } catch (Exception e) { + return ""; + } + System.out.println(output); + return output.toString(); + } + + public static String listPrint(List list) { + StringBuilder sb = new StringBuilder(); + sb.append(list.removeFirst()); + for (String s : list) { + sb.append(", ").append(s); + } + return sb.toString(); + } +} diff --git a/src/main/java/com/leohabrom/discordrpc/WindowCalls.java b/src/main/java/com/leohabrom/discordrpc/WindowCalls.java new file mode 100644 index 0000000..a1c91de --- /dev/null +++ b/src/main/java/com/leohabrom/discordrpc/WindowCalls.java @@ -0,0 +1,12 @@ +package com.leohabrom.discordrpc; + +import org.freedesktop.dbus.interfaces.DBusInterface; +import org.freedesktop.dbus.annotations.DBusInterfaceName; + +@DBusInterfaceName("org.gnome.Shell.Extensions.Windows") +public interface WindowCalls extends DBusInterface { + /** * Returns a JSON string containing a list of all windows and their properties. + * Properties usually include: id, title, wm_class, pid, etc. + */ + String List(); +} \ No newline at end of file diff --git a/src/main/java/com/leohabrom/discordrpc/activities/CustomActivity.java b/src/main/java/com/leohabrom/discordrpc/activities/CustomActivity.java new file mode 100644 index 0000000..f13bb83 --- /dev/null +++ b/src/main/java/com/leohabrom/discordrpc/activities/CustomActivity.java @@ -0,0 +1,137 @@ +package com.leohabrom.discordrpc.activities; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.leohabrom.discord.DiscordActivity; +import com.leohabrom.discord.DiscordActivityBuilder; +import com.leohabrom.discordrpc.Activity; + +import java.io.*; +import java.net.URI; + +public class CustomActivity extends Activity { + private final File activityFile; + private final Gson gson = new Gson(); + + public CustomActivity(File customActivityFile) { + this.activityFile = customActivityFile; + } + + @Override + public boolean isActive() { + if (!exists()) return false; + JsonObject activity = getActivityObject(); + if (activity == null) return false; + return getActivityObject().get("active").getAsBoolean(); + } + + @Override + public void update() { + try { + JsonObject activity = getActivityObject(); + if (activity == null) { + this.activity = new DiscordActivity(); + return; + } + DiscordActivityBuilder builder = new DiscordActivityBuilder() + .name(activity.has("name") ? activity.get("name").getAsString() : null) + .details(activity.has("details") ? activity.get("details").getAsString() : null) + .state(activity.has("state") ? activity.get("state").getAsString() : null) + .smallImage(activity.has("smallImage") ? activity.get("smallImage").getAsString() : null) + .largeImage(activity.has("largeImage") ? activity.get("largeImage").getAsString() : null) + .time(activity.has("startTime") ? activity.get("startTime").getAsLong() : -1) + .endTime(activity.has("endTime") ? activity.get("endTime").getAsLong() : -1); + if (activity.has("details") && activity.has("detailsUrl")) { + builder.details(activity.get("details").getAsString(),URI.create(activity.get("detailsUrl").getAsString())); + } + if (activity.has("state") && activity.has("stateUrl")) { + builder.state(activity.get("state").getAsString(),URI.create(activity.get("stateUrl").getAsString())); + } + if (activity.has("statusDisplay")) { + switch (activity.get("statusDisplay").getAsString()) { + case "name" -> builder.statusDisplay(DiscordActivityBuilder.Status.NAME); + case "state" -> builder.statusDisplay(DiscordActivityBuilder.Status.STATE); + case "detail" -> builder.statusDisplay(DiscordActivityBuilder.Status.DETAIL); + } + } + if (activity.has("type")) { + switch (activity.get("type").getAsString()) { + case "watching" -> builder.type(DiscordActivityBuilder.Type.WATCHING); + case "listening" -> builder.type(DiscordActivityBuilder.Type.LISTENING); + case "playing" -> builder.type(DiscordActivityBuilder.Type.PLAYING); + case "competing" -> builder.type(DiscordActivityBuilder.Type.COMPETING); + } + } + if (activity.has("largeImage") && activity.has("largeImageUrl")) { + builder.largeImage(activity.get("largeImage").getAsString(),URI.create(activity.get("largeImageUrl").getAsString())); + } + if (activity.has("largeImage") && activity.has("largeImageText")) { + builder.largeImage(activity.get("largeImage").getAsString(),activity.get("largeImageText").getAsString()); + } + if (activity.has("largeImage") && activity.has("largeImageUrl") && activity.has("largeImageText")) { + builder.largeImage(activity.get("largeImage").getAsString(),activity.get("largeImageText").getAsString(),URI.create(activity.get("largeImageUrl").getAsString())); + } + if (activity.has("smallImage") && activity.has("smallImageUrl")) { + builder.smallImage(activity.get("smallImage").getAsString(),URI.create(activity.get("smallImageUrl").getAsString())); + } + if (activity.has("smallImage") && activity.has("smallImageText")) { + builder.smallImage(activity.get("smallImage").getAsString(),activity.get("smallImageText").getAsString()); + } + if (activity.has("smallImage") && activity.has("smallImageUrl") && activity.has("smallImageText")) { + builder.smallImage(activity.get("smallImage").getAsString(),activity.get("smallImageText").getAsString(),URI.create(activity.get("smallImageUrl").getAsString())); + } + this.activity = builder.build(); + } catch (Exception e) { + System.err.println(e); + this.activity = new DiscordActivity(); + } + } + + @Override + public boolean hasTimer() { + return true; + } + + @Override + public long getTimer() { + long defaultTimer = 15_000; + if (!exists()) return defaultTimer; + JsonObject activity = getActivityObject(); + if (activity == null) return defaultTimer; + if (!activity.has("timer")) return defaultTimer; + return getActivityObject().get("timer").getAsLong(); + } + + private JsonElement getActivity() { + if (!activityFile.exists()) return null; + try { + BufferedReader reader = new BufferedReader(new FileReader(activityFile)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append(System.lineSeparator()); + } + return gson.fromJson(sb.toString(), JsonElement.class); + } catch (Exception e) { + System.err.println(e); + return null; + } + + } + + private JsonObject getActivityObject() { + JsonElement element = getActivity(); + if (element == null) return null; + if (!(element.isJsonObject())) return null; + return element.getAsJsonObject(); + } + + private boolean exists() { + if (!activityFile.exists()) return false; + JsonElement element = getActivity(); + if (element == null) return false; + if (!(element.isJsonObject())) return false; + return element.getAsJsonObject().has("active"); + } +} diff --git a/src/main/java/com/leohabrom/discordrpc/activities/GenericChromeActivity.java b/src/main/java/com/leohabrom/discordrpc/activities/GenericChromeActivity.java new file mode 100644 index 0000000..0a9231a --- /dev/null +++ b/src/main/java/com/leohabrom/discordrpc/activities/GenericChromeActivity.java @@ -0,0 +1,83 @@ +package com.leohabrom.discordrpc.activities; + +import com.leohabrom.discord.DiscordActivity; +import com.leohabrom.discord.DiscordActivityBuilder; +import com.leohabrom.discordrpc.Activity; +import com.leohabrom.discordrpc.interfaces.DBusInterface; + +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +public class GenericChromeActivity extends Activity { + private final DBusInterface dbus = new DBusInterface("chromium"); + + @Override + public boolean isActive() { + try { + boolean isPlaying = "Playing".equalsIgnoreCase(dbus.getPlaybackStatus()); + + Map metadata = dbus.getMetadata(); + String mprisTitle = String.valueOf(metadata.getOrDefault("xesam:title", "Propably Not In A Youtube Music Title")); + + boolean isYTM = dbus.isMediaTitleInWindow(mprisTitle, "chrome-"); + + // Only trigger if something is playing but it's NOT the YouTube Music app + return isPlaying && !isYTM; + } catch (Exception e) { + return false; + } + } + + @Override + public void update() { + try { + Map metadata = dbus.getMetadata(); + long posMilli = dbus.getPosition() / 1000; + long now = System.currentTimeMillis(); + + // Safely parse the title + String title = String.valueOf(metadata.getOrDefault("xesam:title", "Browser Media")); + + // Safely parse the artist (Handling the ArrayList) + String artist = ""; + Object artistObj = metadata.get("xesam:artist"); + if (artistObj instanceof List list) { + artist = String.join(", ", list.stream().map(Object::toString).toList()); + } else if (artistObj != null) { + artist = artistObj.toString(); + } + DiscordActivityBuilder builder = new DiscordActivityBuilder() + .name("Google-Chrome-Unstable") + .type(DiscordActivityBuilder.Type.WATCHING) + .details(title, URI.create("https://www.google.com/search?q=" + URLEncoder.encode(title + " " + artist, StandardCharsets.UTF_8))) + .state(artist.isEmpty() ? null : artist) + .largeImage("logo512") + .smallImage("https://www.youtube.com/s/desktop/1afc1cab/img/favicon_144x144.png") + .statusDisplay(DiscordActivityBuilder.Status.DETAIL); + + + if (metadata.containsKey("mpris:length")) { + long lengthMilli = Long.parseLong(String.valueOf(metadata.get("mpris:length"))) / 1000; + builder.time(now - posMilli, now + (lengthMilli - posMilli)); + } + + this.activity = builder.build(); + } catch (Exception e) { + System.err.println(e); + this.activity = new DiscordActivity(); + } + } + + @Override + public boolean hasTimer() { + return false; + } + + @Override + public long getTimer() { + return 0; + } +} \ No newline at end of file diff --git a/src/main/java/com/leohabrom/discordrpc/activities/IdlingActivity.java b/src/main/java/com/leohabrom/discordrpc/activities/IdlingActivity.java new file mode 100644 index 0000000..ed6c190 --- /dev/null +++ b/src/main/java/com/leohabrom/discordrpc/activities/IdlingActivity.java @@ -0,0 +1,43 @@ +package com.leohabrom.discordrpc.activities; + +import com.leohabrom.discord.DiscordActivity; +import com.leohabrom.discord.DiscordActivityBuilder; +import com.leohabrom.discordrpc.Activity; + +import java.net.URI; + +public class IdlingActivity extends Activity { + private final long startTime = System.currentTimeMillis(); + + @Override + public boolean isActive() { + return true; // Always available as a fallback + } + + @Override + public void update() { + try { + this.activity = new DiscordActivityBuilder() + .name("Jinx") + .details("Idling...") + .type(DiscordActivityBuilder.Type.WATCHING) + .statusDisplay(DiscordActivityBuilder.Status.DETAIL) + .largeImage("mixxdlabel", URI.create("https://cc.leohabrom.com")) + .time(startTime) + .build(); + } catch (Exception e) { + System.err.println(e); + this.activity = new DiscordActivity(); + } + } + + @Override + public boolean hasTimer() { + return false; + } + + @Override + public long getTimer() { + return 0; + } +} \ No newline at end of file diff --git a/src/main/java/com/leohabrom/discordrpc/activities/IntelliJActivity.java b/src/main/java/com/leohabrom/discordrpc/activities/IntelliJActivity.java new file mode 100644 index 0000000..e29fb8d --- /dev/null +++ b/src/main/java/com/leohabrom/discordrpc/activities/IntelliJActivity.java @@ -0,0 +1,57 @@ +package com.leohabrom.discordrpc.activities; + +import com.google.gson.JsonObject; +import com.leohabrom.discord.DiscordActivity; +import com.leohabrom.discord.DiscordActivityBuilder; +import com.leohabrom.discordrpc.Activity; +import com.leohabrom.discordrpc.interfaces.DBusInterface; + +import java.net.URI; +import java.util.Arrays; + +public class IntelliJActivity extends Activity { + + private final DBusInterface dBusInterface = new DBusInterface("ignored"); + private long startTime = -1; + + @Override + public boolean isActive() { + JsonObject object = dBusInterface.getClass("jetbrains-idea"); + if (object == null) { + startTime = -1; + return false; + } + if (startTime == -1) startTime = System.currentTimeMillis(); + return object.get("focus").getAsBoolean(); + } + + @Override + public void update() { + try { + String title = dBusInterface.getClass("jetbrains-idea").get("title").getAsString(); + String[] titles = title.split("\\s+[\\-\u2013\u2014]\\s+"); + this.activity = new DiscordActivityBuilder() + .name("IntelliJ Idea Ultimate") + .details("Coding: " + (titles.length > 1 ? titles[1] : title)) + .statusDisplay(DiscordActivityBuilder.Status.DETAIL) + .type(DiscordActivityBuilder.Type.PLAYING) + .time(startTime) + .state(titles.length > 1 ? "in " + titles[0] : null) + .largeImage("https://resources.jetbrains.com/storage/products/company/brand/logos/IntelliJ_IDEA_icon.png", "Copyright © 2026 JetBrains s.r.o. IntelliJ and the IntelliJ logo are trademarks of JetBrains s.r.o.", URI.create("https://www.jetbrains.com/idea/")) + .build(); + } catch (Exception e) { + System.err.println(e); + this.activity = new DiscordActivity(); + } + } + + @Override + public boolean hasTimer() { + return false; + } + + @Override + public long getTimer() { + return 0; + } +} diff --git a/src/main/java/com/leohabrom/discordrpc/activities/MensaActivity.java b/src/main/java/com/leohabrom/discordrpc/activities/MensaActivity.java new file mode 100644 index 0000000..4f1e364 --- /dev/null +++ b/src/main/java/com/leohabrom/discordrpc/activities/MensaActivity.java @@ -0,0 +1,106 @@ +package com.leohabrom.discordrpc.activities; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.leohabrom.discord.DiscordActivity; +import com.leohabrom.discord.DiscordActivityBuilder; +import com.leohabrom.discordrpc.Activity; +import com.leohabrom.discordrpc.interfaces.MensaClient; + +import java.net.URI; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class MensaActivity extends Activity { + + private final Gson gson = new Gson(); + private final MensaClient client = new MensaClient("http://api.mensaplan.leohabrom.com"); + private int counter = 0; + + @Override + public boolean isActive() { + int days = LocalDateTime.now().isBefore(LocalDateTime.now().withHour(13).withMinute(45)) ? 0 : 1; + return client.hasMeals(LocalDateTime.now().plusDays(days).format(DateTimeFormatter.ISO_DATE)); + } + + public void inc() { + counter++; + if (counter > 6) counter = 0; + } + + @Override + public void update() { + try { + String day = LocalDateTime.now().isBefore(LocalDateTime.now().withHour(13).withMinute(45)) ? "heute" : "morgen"; + int days = LocalDateTime.now().isBefore(LocalDateTime.now().withHour(13).withMinute(45)) ? 0 : 1; + String date = LocalDateTime.now().plusDays(days).format(DateTimeFormatter.ISO_DATE); + String category; + switch (counter) { + case 0: + category = "vegan"; + if (client.hasMeal(date, category)) break; + counter++; + case 1: + category = "vegetarisch"; + if (client.hasMeal(date, category)) break; + counter++; + case 2: + category = "fleisch + fisch"; + if (client.hasMeal(date, category)) break; + counter++; + case 3: + category = "pizza i"; + if (client.hasMeal(date, category)) break; + counter++; + case 4: + category = "pizza ii"; + if (client.hasMeal(date, category)) break; + counter++; + case 5: + category = "pizza iii"; + if (client.hasMeal(date, category)) break; + counter++; + case 6: + category = "dessert"; + if (client.hasMeal(date, category)) break; + default: + counter = 0; + return; + } + JsonObject meal = client.getMeal(date, category); + if (meal.get("exists").getAsBoolean()) { + this.activity = new DiscordActivityBuilder() + .name("\uD83E\uDD57 Mensaplan " + day + " - " + category + " \uD83E\uDD57") + .details(meal.get("name").getAsString(), URI.create("https://mensaplan.leohabrom.com")) + .state("Student: " + String.format("%.2f", meal.get("prices").getAsJsonArray().get(0).getAsDouble()) + "€", URI.create("https://mensaplan.leohabrom.com")) + .statusDisplay(DiscordActivityBuilder.Status.DETAIL) + .largeImage("https://studierendenwerk-ulm.de/wp-content/themes/studentenwerk/assets/favicon/android-icon-192x192.png", "View Mensaplan", URI.create("https://mensaplan.leohabrom.com")) + .type(DiscordActivityBuilder.Type.WATCHING) + .smallImage("logo512") + .build(); + } else { + this.activity = new DiscordActivityBuilder() + .name("\uD83E\uDD57 Mensaplan - " + day + " \uD83E\uDD57") + .details("Heute geschlossen") + .state("Today closed") + .largeImage("https://studierendenwerk-ulm.de/wp-content/themes/studentenwerk/assets/favicon/android-icon-192x192.png", "View Mensaplan", URI.create("https://mensaplan.leohabrom.com")) + .type(DiscordActivityBuilder.Type.WATCHING) + .smallImage("logo512") + .build(); + } + } catch (Exception e) { + System.err.println(e); + this.activity = new DiscordActivity(); + } + } + + @Override + public boolean hasTimer() { + return true; + } + + @Override + public long getTimer() { + return 12_000; + } +} diff --git a/src/main/java/com/leohabrom/discordrpc/activities/MensazeitActivity.java b/src/main/java/com/leohabrom/discordrpc/activities/MensazeitActivity.java new file mode 100644 index 0000000..d5ae698 --- /dev/null +++ b/src/main/java/com/leohabrom/discordrpc/activities/MensazeitActivity.java @@ -0,0 +1,48 @@ +package com.leohabrom.discordrpc.activities; + +import com.leohabrom.discord.DiscordActivity; +import com.leohabrom.discord.DiscordActivityBuilder; +import com.leohabrom.discordrpc.Activity; + +import java.net.URI; +import java.time.LocalDateTime; +import java.time.ZoneId; + +public class MensazeitActivity extends Activity { + @Override + public boolean isActive() { + return LocalDateTime.now().isBefore(LocalDateTime.now().withHour(13).withMinute(45)) && LocalDateTime.now().isAfter(LocalDateTime.now().withHour(11).withMinute(30)); + } + + @Override + public void update() { + try { + ZoneId zone = ZoneId.of("Europe/Berlin"); + long startTime = LocalDateTime.now().withHour(11).withMinute(30).withSecond(0).toEpochSecond(zone.getRules().getOffset(LocalDateTime.now())) * 1000 - 11 * 60 * 60 * 1000 - 30 * 60 * 1000; + long endTime = LocalDateTime.now().withHour(13).withMinute(45).withSecond(0).toEpochSecond(zone.getRules().getOffset(LocalDateTime.now())) * 1000; + this.activity = new DiscordActivityBuilder() + .name("Mensazeit") + .details("Es ist zeit zu mensieren") + .state("Foood?") + .largeImage("https://cdn.discordapp.com/avatars/1294751726582235178/c38228bdb5c01aba883a2f88ada96a13.png", "Mmmh lecker Mensa", URI.create("https://mensaplan.leohabrom.com")) + .smallImage("logo512", "Jinx :D") + .type(DiscordActivityBuilder.Type.WATCHING) + .statusDisplay(DiscordActivityBuilder.Status.STATE) + .time(startTime, endTime) + .build(); + } catch (Exception e) { + System.err.println(e); + this.activity = new DiscordActivity(); + } + } + + @Override + public boolean hasTimer() { + return false; + } + + @Override + public long getTimer() { + return 0; + } +} diff --git a/src/main/java/com/leohabrom/discordrpc/activities/MinecraftActivity.java b/src/main/java/com/leohabrom/discordrpc/activities/MinecraftActivity.java new file mode 100644 index 0000000..64f944a --- /dev/null +++ b/src/main/java/com/leohabrom/discordrpc/activities/MinecraftActivity.java @@ -0,0 +1,60 @@ +package com.leohabrom.discordrpc.activities; + +import com.google.gson.JsonObject; +import com.leohabrom.discord.DiscordActivity; +import com.leohabrom.discord.DiscordActivityBuilder; +import com.leohabrom.discordrpc.Activity; +import com.leohabrom.discordrpc.interfaces.DBusInterface; + +import java.net.URI; +import java.util.Arrays; + +public class MinecraftActivity extends Activity { + + private final DBusInterface dBusInterface = new DBusInterface("ignored"); + private long startTime = -1; + + @Override + public boolean isActive() { + JsonObject object = dBusInterface.getMinecraft(); + if (object == null) { + startTime = -1; + return false; + } + if (startTime == -1) startTime = System.currentTimeMillis(); + return true; + } + + @Override + public void update() { + try { + JsonObject object = dBusInterface.getMinecraft(); + String title = object.get("title").getAsString(); + String[] titles = title.split("\\s+[\\-\u2013\u2014]\\s+"); + this.activity = new DiscordActivityBuilder() + .name("Minecraft") + .state((titles.length > 1 ? titles[1] : title)) + .statusDisplay(DiscordActivityBuilder.Status.NAME) + .type(DiscordActivityBuilder.Type.PLAYING) + .time(startTime) + .details(titles.length > 1 ? titles[0] : null) + .largeImage("https://minecraft.wiki/images/Grass_Block_JE7_BE6.png", "Minecraft", URI.create("https://www.minecraft.net/en-us")) + .smallImage("logo512", "My Minecraft server", URI.create("https://jinxhideout.com")) + .party("minecraft-party", 1, 1, DiscordActivityBuilder.Privacy.PRIVATE) + .build(); + } catch (Exception e) { + System.err.println(e); + this.activity = new DiscordActivity(); + } + } + + @Override + public boolean hasTimer() { + return false; + } + + @Override + public long getTimer() { + return 0; + } +} diff --git a/src/main/java/com/leohabrom/discordrpc/activities/PrinterActivity.java b/src/main/java/com/leohabrom/discordrpc/activities/PrinterActivity.java new file mode 100644 index 0000000..42f1716 --- /dev/null +++ b/src/main/java/com/leohabrom/discordrpc/activities/PrinterActivity.java @@ -0,0 +1,90 @@ +package com.leohabrom.discordrpc.activities; + +import com.google.gson.JsonObject; +import com.leohabrom.discord.DiscordActivity; +import com.leohabrom.discord.DiscordActivityBuilder; +import com.leohabrom.discordrpc.Activity; +import com.leohabrom.discordrpc.interfaces.OctoPrintClient; + +import java.net.URI; +import java.net.URL; + +public class PrinterActivity extends Activity { + private final OctoPrintClient printerClient; + + public PrinterActivity(String host, String apiKey) { + this.printerClient = new OctoPrintClient(host, apiKey); + } + + @Override + public boolean isActive() { + try { + JsonObject state = printerClient.getPrinterState(); + return state.get("state").getAsJsonObject() + .get("flags").getAsJsonObject() + .get("printing").getAsBoolean(); + } catch (Exception e) { + return false; + } + } + + @Override + public void update() { + try { + JsonObject printer = printerClient.getPrinterState(); + JsonObject job = printerClient.getJobState(); + + // Modular Extraction + PrinterData data = new PrinterData(printer, job); + + this.activity = new DiscordActivityBuilder() + .name("3D Printer") + .type(DiscordActivityBuilder.Type.WATCHING) + .details("Printing: " + data.fileName) + .state(data.getTempString()) + .largeImage("https://3d-cam.leohabrom.com/frame.jpeg?t=" + (System.currentTimeMillis() / 300000), "Watch Stream", new URI("https://3d-cam.leohabrom.com/stream")) + .time(data.startTime, data.endTime) + .statusDisplay(DiscordActivityBuilder.Status.DETAIL) + .build(); + } catch (Exception e) { + System.err.println(e); + this.activity = new DiscordActivity(); + } + } + + @Override + public boolean hasTimer() { + return true; + } + + @Override + public long getTimer() { + return 10_000; + } + + /** + * Inner helper class to keep the mapping logic clean + */ + private static class PrinterData { + final String fileName; + final int toolTemp, bedTemp; + final long startTime, endTime; + + PrinterData(JsonObject printer, JsonObject job) { + this.fileName = job.getAsJsonObject("job").getAsJsonObject("file").get("name").getAsString(); + this.toolTemp = (int) printer.getAsJsonObject("temperature").getAsJsonObject("tool0").get("actual").getAsDouble(); + this.bedTemp = (int) printer.getAsJsonObject("temperature").getAsJsonObject("bed").get("actual").getAsDouble(); + + long now = System.currentTimeMillis(); + long elapsed = job.getAsJsonObject("progress").get("printTime").getAsLong(); + long left = job.getAsJsonObject("progress").get("printTimeLeft").getAsLong(); + + this.startTime = ((long) ((now - (elapsed * 1000)) / 1000.0)) * 1000; + this.endTime = ((long) ((now + (left * 1000)) / 1000.0)) * 1000; + } + + String getTempString() { + return String.format("Ext: %d°C | Bed: %d°C", toolTemp, bedTemp); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/leohabrom/discordrpc/activities/RiftboundActivity.java b/src/main/java/com/leohabrom/discordrpc/activities/RiftboundActivity.java new file mode 100644 index 0000000..ffcf4f8 --- /dev/null +++ b/src/main/java/com/leohabrom/discordrpc/activities/RiftboundActivity.java @@ -0,0 +1,75 @@ +package com.leohabrom.discordrpc.activities; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.leohabrom.discord.DiscordActivity; +import com.leohabrom.discord.DiscordActivityBuilder; +import com.leohabrom.discordrpc.Activity; +import com.leohabrom.discordrpc.interfaces.MensaClient; +import com.leohabrom.discordrpc.interfaces.RiftbountClient; + +import java.net.URI; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Random; + +public class RiftboundActivity extends Activity { + + private final Gson gson = new Gson(); + private final Random random = new Random(); + private final RiftbountClient client = new RiftbountClient("https://cc-backend.leohabrom.com"); + private int counter = 0; + + @Override + public boolean isActive() { + if (!((LocalDateTime.now().isAfter(LocalDateTime.now().withHour(14).withMinute(0)) && LocalDateTime.now().isBefore(LocalDateTime.now().withHour(17).withMinute(0))))) return false; + return client.getUserProfile("zxrotwo002").has("favorites"); + } + + @Override + public void update() { + if (counter == 1 && !this.activity.equals(new DiscordActivity())) { + counter = 0; + return; + } + counter = 1; + try { + JsonObject riftBoundData = client.getUserProfile("zxrotwo002"); + if (!riftBoundData.has("favorites")) return; + if (!riftBoundData.has("collection")) return; + JsonArray favorites = riftBoundData.get("favorites").getAsJsonArray(); + JsonObject colleciton = riftBoundData.get("collection").getAsJsonObject(); + if (favorites.isEmpty()) return; + JsonElement element = favorites.get(random.nextInt(favorites.size())); + String id = element.getAsString(); + JsonObject card = client.getCard(id); + String url = card.get("image").getAsString(); + url = url.startsWith("https://") ? url : "https://cc.leohabrom.com/" + url; + String name = card.get("name").getAsString(); + String tags = card.has("tags") ? card.get("tags").getAsString() : card.has("set") ? card.get("set").getAsString() : ""; + this.activity = new DiscordActivityBuilder() + .name("Riftbound Favorites") + .type(DiscordActivityBuilder.Type.WATCHING) + .details(name + " - " + tags,URI.create("https://cc.leohabrom.com")) + .state("ID: " + id + ", Owned: " + (colleciton.has(id) ? colleciton.get(id).getAsInt() : 0)) + .largeImage(url,name,URI.create(url)) + .smallImage("riftbound","Riftbound",URI.create("https://riftbound.leagueoflegends.com/en-us/card-gallery/")) + .build(); + } catch (Exception e) { + System.err.println(e); + this.activity = new DiscordActivity(); + } + } + + @Override + public boolean hasTimer() { + return true; + } + + @Override + public long getTimer() { + return 30_000; + } +} diff --git a/src/main/java/com/leohabrom/discordrpc/activities/SnaktActivity.java b/src/main/java/com/leohabrom/discordrpc/activities/SnaktActivity.java new file mode 100644 index 0000000..27d1d55 --- /dev/null +++ b/src/main/java/com/leohabrom/discordrpc/activities/SnaktActivity.java @@ -0,0 +1,42 @@ +package com.leohabrom.discordrpc.activities; + +import com.leohabrom.discord.DiscordActivity; +import com.leohabrom.discord.DiscordActivityBuilder; +import com.leohabrom.discordrpc.Activity; + +import java.net.URI; +import java.time.LocalDateTime; + +public class SnaktActivity extends Activity { + @Override + public boolean isActive() { + return (LocalDateTime.now().isAfter(LocalDateTime.now().withHour(18).withMinute(0)) && LocalDateTime.now().isBefore(LocalDateTime.now().withHour(19).withMinute(0))); + } + + @Override + public void update() { + try { + this.activity = new DiscordActivityBuilder() + .type(DiscordActivityBuilder.Type.WATCHING) + .name("Snakt Martin") + .details("Ich besnakte dich \uD83D\uDE14") + .statusDisplay(DiscordActivityBuilder.Status.DETAIL) + .time(100000, System.currentTimeMillis() + System.currentTimeMillis()) + .largeImage("https://snakt.wirkaufendeinhintern.de/snakt_martin.png", URI.create("https://snakt.wirkaufendeinhintern.de")) + .build(); + } catch (Exception e) { + System.err.println(e); + this.activity = new DiscordActivity(); + } + } + + @Override + public boolean hasTimer() { + return false; + } + + @Override + public long getTimer() { + return 0; + } +} diff --git a/src/main/java/com/leohabrom/discordrpc/activities/VSCodeActivity.java b/src/main/java/com/leohabrom/discordrpc/activities/VSCodeActivity.java new file mode 100644 index 0000000..4ccde16 --- /dev/null +++ b/src/main/java/com/leohabrom/discordrpc/activities/VSCodeActivity.java @@ -0,0 +1,57 @@ +package com.leohabrom.discordrpc.activities; + +import com.google.gson.JsonObject; +import com.leohabrom.discord.DiscordActivity; +import com.leohabrom.discord.DiscordActivityBuilder; +import com.leohabrom.discordrpc.Activity; +import com.leohabrom.discordrpc.interfaces.DBusInterface; + +import java.net.URI; +import java.util.Arrays; + +public class VSCodeActivity extends Activity { + + private final DBusInterface dBusInterface = new DBusInterface("ignored"); + private long startTime = -1; + + @Override + public boolean isActive() { + JsonObject object = dBusInterface.getClass("code"); + if (object == null) { + startTime = -1; + return false; + } + if (startTime == -1) startTime = System.currentTimeMillis(); + return object.get("focus").getAsBoolean(); + } + + @Override + public void update() { + try { + String title = dBusInterface.getClass("code").get("title").getAsString(); + String[] titles = title.split("\\s+[\\-\u2013\u2014]\\s+"); + this.activity = new DiscordActivityBuilder() + .name("Visual Studio Code") + .details("Coding: " + (titles.length > 1 ? titles[0] : title)) + .statusDisplay(DiscordActivityBuilder.Status.DETAIL) + .type(DiscordActivityBuilder.Type.PLAYING) + .time(startTime) + .state(titles.length > 1 ? "in " + titles[1] : null) + .largeImage("https://code.visualstudio.com/assets/branding/code-stable.png", "Visual Studio Code, VS Code, and the Visual Studio Code icon are trademarks of Microsoft Corporation. All rights reserved.", URI.create("https://code.visualstudio.com/")) + .build(); + } catch (Exception e) { + System.err.println(e); + this.activity = new DiscordActivity(); + } + } + + @Override + public boolean hasTimer() { + return false; + } + + @Override + public long getTimer() { + return 0; + } +} diff --git a/src/main/java/com/leohabrom/discordrpc/activities/YouTubeMusic.java b/src/main/java/com/leohabrom/discordrpc/activities/YouTubeMusic.java new file mode 100644 index 0000000..85f5ec5 --- /dev/null +++ b/src/main/java/com/leohabrom/discordrpc/activities/YouTubeMusic.java @@ -0,0 +1,88 @@ +package com.leohabrom.discordrpc.activities; + +import com.leohabrom.discord.DiscordActivity; +import com.leohabrom.discord.DiscordActivityBuilder; +import com.leohabrom.discordrpc.Activity; +import com.leohabrom.discordrpc.interfaces.DBusInterface; + +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +public class YouTubeMusic extends Activity { + private final DBusInterface dbus = new DBusInterface("chromium"); + + @Override + public boolean isActive() { + try { + // 1. Must be "Playing" + if (!"Playing".equalsIgnoreCase(dbus.getPlaybackStatus())) { + return false; + } + + // 2. Get the actual title from MPRIS + Map metadata = dbus.getMetadata(); + String mprisTitle = String.valueOf(metadata.getOrDefault("xesam:title", "Propably Not In A Youtube Music Title")); + + // 3. Verify that this specific title is visible in a Window + return dbus.isMediaTitleInWindow(mprisTitle, "chrome-"); + + } catch (Exception e) { + return false; + } + } + + @Override + public void update() { + try { + Map metadata = dbus.getMetadata(); + long posMilli = dbus.getPosition() / 1000; + long now = System.currentTimeMillis(); + + // Safely parse the title + String title = String.valueOf(metadata.getOrDefault("xesam:title", "Unknown")); + + // Safely parse the artist (Handling the ArrayList) + String artist = "Unknown Artist"; + Object artistObj = metadata.get("xesam:artist"); + if (artistObj instanceof List list) { + artist = String.join(", ", list.stream().map(Object::toString).toList()); + } else if (artistObj != null) { + artist = artistObj.toString(); + } + + String coverUrl = URI.create("https://cover-lookup.leohabrom.com/" + URLEncoder.encode(title + "-" + artist + ".png", StandardCharsets.UTF_8)).toString(); + + DiscordActivityBuilder builder = new DiscordActivityBuilder() + .name("YouTube Music") + .type(DiscordActivityBuilder.Type.LISTENING) + .statusDisplay(DiscordActivityBuilder.Status.DETAIL) + .details(title, URI.create("https://music.youtube.com/search?q=" + URLEncoder.encode(title, StandardCharsets.UTF_8))) + .state(artist, URI.create("https://music.youtube.com/search?q=" + URLEncoder.encode(artist, StandardCharsets.UTF_8))) + .largeImage(coverUrl, title + " - " + artist, URI.create("https://music.youtube.com/search?q=" + URLEncoder.encode(title + " " + artist, StandardCharsets.UTF_8))) + .smallImage("https://music.youtube.com/img/favicon_144.png"); + + if (metadata.containsKey("mpris:length")) { + long lengthMilli = Long.parseLong(String.valueOf(metadata.get("mpris:length"))) / 1000; + builder.time(now - posMilli, now + (lengthMilli - posMilli)); + } + + this.activity = builder.build(); + } catch (Exception e) { + System.err.println(e); + this.activity = new DiscordActivity(); + } + } + + @Override + public boolean hasTimer() { + return true; + } + + @Override + public long getTimer() { + return 30_000; + } +} \ No newline at end of file diff --git a/src/main/java/com/leohabrom/discordrpc/interfaces/DBusInterface.java b/src/main/java/com/leohabrom/discordrpc/interfaces/DBusInterface.java new file mode 100644 index 0000000..c9ea8ef --- /dev/null +++ b/src/main/java/com/leohabrom/discordrpc/interfaces/DBusInterface.java @@ -0,0 +1,135 @@ +package com.leohabrom.discordrpc.interfaces; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.leohabrom.discordrpc.WindowCalls; +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; +import org.freedesktop.dbus.exceptions.DBusException; +import org.freedesktop.dbus.interfaces.DBus; +import org.freedesktop.dbus.interfaces.Properties; + +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; + +public class DBusInterface { + private final Gson gson = new Gson(); + private final String MPRIS_PREFIX = "org.mpris.MediaPlayer2."; + + private final String application; + + private static DBusConnection connection; + + static { + try { + connection = DBusConnectionBuilder.forSessionBus().build(); + } catch (DBusException e) { + throw new RuntimeException(e); + } + } + + + public DBusInterface(String application) { + this.application = application; + } + + + public Map getMetadata() { + return getProperties(application).Get(MPRIS_PREFIX + "Player","Metadata"); + } + + public long getPosition() { + return getProperties(application).Get(MPRIS_PREFIX + "Player","Position"); + } + + public String getPlaybackStatus() { + return getProperties(application).Get(MPRIS_PREFIX + "Player","PlaybackStatus"); + } + + public void test() { + System.out.println(getWindowCalls().List()); + } + + private WindowCalls getWindowCalls() { + try { + WindowCalls windowCalls = connection.getRemoteObject("org.gnome.Shell","/org/gnome/Shell/Extensions/Windows", WindowCalls.class); + return windowCalls; + } catch (DBusException e) { + throw new RuntimeException(e); + } + } + + + private Properties getProperties(String application) { + try { + Properties properties = connection.getRemoteObject(getMediaPlayerBusName(application),"/org/mpris/MediaPlayer2",Properties.class); + return properties; + } catch (DBusException e) { + throw new RuntimeException(e); + } + } + + + private String getMediaPlayerBusName(String application) { + Optional chromiumBusName; + try { + chromiumBusName = Arrays.stream(connection.getRemoteObject("org.freedesktop.DBus", "/org/freedesktop/DBus", DBus.class).ListNames()) + .filter(name -> name.startsWith(MPRIS_PREFIX + application)) + .findFirst(); + } catch (DBusException e) { + throw new RuntimeException(e); + } + return chromiumBusName.orElse(null); + } + + + public boolean isMediaTitleInWindow(String mprisTitle, String startsWith) { + if (mprisTitle == null || mprisTitle.isEmpty()) return false; + + try { + String jsonOutput = getWindowCalls().List(); + JsonArray windows = gson.fromJson(jsonOutput, JsonArray.class); + if (windows == null) return false; + + for (JsonElement element : windows) { + JsonObject window = element.getAsJsonObject(); + if (window.has("title") && window.has("wm_class")) { + String windowTitle = window.get("title").getAsString(); + String wmClass = window.get("wm_class").getAsString(); + // YouTube Music titles usually look like "Song Name - Artist - YouTube Music" + // So we check if the window title contains the xesam:title + if (windowTitle.toLowerCase().contains(mprisTitle.toLowerCase()) && wmClass.toLowerCase().trim().startsWith(startsWith.toLowerCase().trim())) { + return true; + } + } + } + } catch (Exception e) { + return false; + } + return false; + } + + public JsonObject getClass(String wmClassStartsWith) { + JsonArray jsonArray = gson.fromJson(getWindowCalls().List(), JsonArray.class); + if (jsonArray == null) return null; + for (JsonElement element : jsonArray) { + if (element.getAsJsonObject().has("wm_class") && element.getAsJsonObject().get("wm_class").getAsString().toLowerCase().trim().startsWith(wmClassStartsWith.toLowerCase().trim())) return element.getAsJsonObject(); + } + return null; + } + + public JsonObject getMinecraft() { + JsonArray jsonArray = gson.fromJson(getWindowCalls().List(), JsonArray.class); + if (jsonArray == null) return null; + for (JsonElement element : jsonArray) { + if (!element.getAsJsonObject().has("wm_class_instance") && !element.getAsJsonObject().has("wm_class")) continue; + String wmClassInstance = element.getAsJsonObject().get("wm_class_instance").getAsString(); + String wmClass = element.getAsJsonObject().get("wm_class").getAsString(); + if (wmClass.toLowerCase().trim().startsWith("minecraft") && !wmClassInstance.equalsIgnoreCase("minecraft-launcher")) return element.getAsJsonObject(); + } + return null; + } +} diff --git a/src/main/java/com/leohabrom/discordrpc/interfaces/MensaClient.java b/src/main/java/com/leohabrom/discordrpc/interfaces/MensaClient.java new file mode 100644 index 0000000..5c71f5d --- /dev/null +++ b/src/main/java/com/leohabrom/discordrpc/interfaces/MensaClient.java @@ -0,0 +1,74 @@ +package com.leohabrom.discordrpc.interfaces; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +public class MensaClient { + private final String url; + private final HttpClient client; + private final Gson gson = new Gson(); + + + + public MensaClient(String url) { + this.url = url+"/"; + this.client = HttpClient.newBuilder().build(); + } + + public JsonObject getMeal(String day, String category) { + try { + JsonArray array = fetch(day).get("meals").getAsJsonArray(); + for (JsonElement element : array) { + if (element.getAsJsonObject().get("category").getAsString().equalsIgnoreCase(category)) { + JsonObject object = element.getAsJsonObject(); + object.addProperty("exists",true); + return object; + } + } + JsonObject object = new JsonObject(); + object.addProperty("exists",false); + return object; + } catch (Exception e) { + JsonObject object = new JsonObject(); + object.addProperty("exists",false); + return object; + } + } + + public boolean hasMeal(String day, String category) { + try { + JsonArray array = fetch(day).get("meals").getAsJsonArray(); + for (JsonElement element : array) { + if (element.getAsJsonObject().get("category").getAsString().equalsIgnoreCase(category)) { + return true; + } + } + return false; + } catch (Exception e) { + return false; + } + } + + public boolean hasMeals(String day) { + try { + return fetch(day).has("meals"); + } catch (Exception e) { + return false; + } + } + + private JsonObject fetch(String endpoint) throws Exception { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url + endpoint)) + .GET().build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + return gson.fromJson(response.body(), JsonObject.class); + } +} diff --git a/src/main/java/com/leohabrom/discordrpc/interfaces/OctoPrintClient.java b/src/main/java/com/leohabrom/discordrpc/interfaces/OctoPrintClient.java new file mode 100644 index 0000000..bb315ee --- /dev/null +++ b/src/main/java/com/leohabrom/discordrpc/interfaces/OctoPrintClient.java @@ -0,0 +1,37 @@ +package com.leohabrom.discordrpc.interfaces; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import java.net.URI; +import java.net.http.*; + +public class OctoPrintClient { + private final String baseUrl; + private final String apiKey; + private final HttpClient client; + private final Gson gson = new Gson(); + + public OctoPrintClient(String host, String apiKey) { + this.baseUrl = host + "/api/"; + this.apiKey = apiKey; + this.client = HttpClient.newHttpClient(); + } + + public JsonObject getPrinterState() throws Exception { + return fetch("printer"); + } + + public JsonObject getJobState() throws Exception { + return fetch("job"); + } + + private JsonObject fetch(String endpoint) throws Exception { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + endpoint)) + .header("X-Api-Key", apiKey) + .GET().build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + return gson.fromJson(response.body(), JsonObject.class); + } +} \ No newline at end of file diff --git a/src/main/java/com/leohabrom/discordrpc/interfaces/RiftbountClient.java b/src/main/java/com/leohabrom/discordrpc/interfaces/RiftbountClient.java new file mode 100644 index 0000000..c71ec65 --- /dev/null +++ b/src/main/java/com/leohabrom/discordrpc/interfaces/RiftbountClient.java @@ -0,0 +1,58 @@ +package com.leohabrom.discordrpc.interfaces; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +public class RiftbountClient { + private final String url; + private final HttpClient client; + private final Gson gson = new Gson(); + + + + public RiftbountClient(String url) { + this.url = url+"/api/"; + this.client = HttpClient.newBuilder().build(); + } + + public JsonObject getUserProfile(String username) { + try { + return fetch(url,"user-profile/"+username).getAsJsonObject(); + } catch (Exception e) { + return null; + } + } + + public JsonObject getCard(String id) { + try { + JsonArray sets = fetch("https://cc.leohabrom.com/","cards.json").getAsJsonArray(); + for (JsonElement element : sets) { + JsonObject currentSet = element.getAsJsonObject(); + JsonArray cards = currentSet.get("cards").getAsJsonArray(); + for (JsonElement card : cards) { + JsonObject cardObject = card.getAsJsonObject(); + if (cardObject.get("id").getAsString().equalsIgnoreCase(id)) return cardObject; + } + } + return null; + } catch (Exception e) { + System.out.println(e); + return null; + } + } + + private JsonElement fetch(String url, String endpoint) throws Exception { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url + endpoint)) + .GET().build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + return gson.fromJson(response.body(), JsonElement.class); + } +}