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);
+ }
+}