feat: first commit

This commit is contained in:
Leo
2026-04-12 00:58:20 +02:00
parent b79ff7ebf4
commit 60f1c0c303
32 changed files with 2094 additions and 1 deletions

174
.gitignore vendored
View File

@@ -41,3 +41,177 @@ bin/
### Mac OS ###
.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

10
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

34
build.gradle Normal file
View File

@@ -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"
}
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -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

234
gradlew vendored Executable file
View File

@@ -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" "$@"

89
gradlew.bat vendored Normal file
View File

@@ -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

3
move.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
mv build/libs/DiscordActivity-1.0-SNAPSHOT-all.jar ~/.local/share/applications/discordRpc.jar
discordRpc

1
settings.gradle Normal file
View File

@@ -0,0 +1 @@
rootProject.name = 'DiscordActivity'

View File

@@ -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 + '\'' +
'}';
}
}

View File

@@ -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<Boolean> feedbackQueue = new ArrayBlockingQueue<>(1);
// Scheduling tools
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final AtomicLong lastSendTime = new AtomicLong(0);
private final AtomicReference<DiscordActivity> pendingActivity = new AtomicReference<>();
private final AtomicReference<ScheduledFuture<?>> 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);
}
}
}

View File

@@ -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<Activity> 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<Activity> 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;
}
}
}
}

View File

@@ -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());;
}
}
}
}
}
}

View File

@@ -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<String> list) {
StringBuilder sb = new StringBuilder();
sb.append(list.removeFirst());
for (String s : list) {
sb.append(", ").append(s);
}
return sb.toString();
}
}

View File

@@ -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();
}

View File

@@ -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");
}
}

View File

@@ -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<String, Object> 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<String, Object> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

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

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<String, Object> 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<String, Object> 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;
}
}

View File

@@ -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<String,Object> 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<String> 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;
}
}

View File

@@ -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<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
return gson.fromJson(response.body(), JsonObject.class);
}
}

View File

@@ -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<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
return gson.fromJson(response.body(), JsonObject.class);
}
}

View File

@@ -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<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
return gson.fromJson(response.body(), JsonElement.class);
}
}