- * + * Rich Presence data. + *
* The ID provided should be the client ID of the particular * application providing Rich Presence, which can be found - * here.
- * - * When initially created using {@link #IPCClient(long)} the client will + * here. + *
+ * When initially created using {@link #IPCClient(long, boolean, String)} the client will
* be inactive awaiting a call to {@link #connect(DiscordBuild...)}.
* After the call, this client can send and receive Rich Presence data
* to and from discord via {@link #sendRichPresence(RichPresence)} and
- * {@link #setListener(IPCListener)} respectively.
- * + * {@link #setListener(IPCListener)} respectively. + *
* Please be mindful that the client created is initially unconnected,
* and calling any methods that exchange data between this client and
* Discord before a call to {@link #connect(DiscordBuild...)} will cause
@@ -53,47 +55,300 @@
*
* @author John Grosh (john.a.grosh@gmail.com)
*/
-public final class IPCClient implements Closeable
-{
+public final class IPCClient implements Closeable {
private static final Logger LOGGER = LoggerFactory.getLogger(IPCClient.class);
+ private final Backoff RECONNECT_TIME_MS = new Backoff(500, 60 * 1000);
private final long clientId;
- private final HashMap
+ * Constructs a new IPCClient using the provided {@code clientId}.
+ * @param clientId The Rich Presence application's client ID, which can be found
+ * here
+ * @param debugMode Whether Debug Logging should be shown for this client
+ * @param verboseLogging Whether excess/deeper-rooted logging should be shown
+ * @param autoRegister Whether to register as an application with discord
+ * @param applicationId The application id to register with, usually the client id in string form
+ */
+ public IPCClient(long clientId, boolean debugMode, boolean verboseLogging, boolean autoRegister, String applicationId) {
+ this(clientId, debugMode, verboseLogging, autoRegister, applicationId, null);
+ }
+
+ /**
+ * Constructs a new IPCClient using the provided {@code clientId}.
+ * A single IPCClient can only have one of these set at any given time.
* This can be set safely before a call to {@link #connect(DiscordBuild...)}
* is made.
*
* @param listener The {@link IPCListener} to set for this IPCClient.
- *
* @see IPCListener
*/
- public void setListener(IPCListener listener)
- {
+ public void setListener(IPCListener listener) {
this.listener = listener;
if (pipe != null)
pipe.setListener(listener);
}
-
+
+ /**
+ * Gets the application id associated with this IPCClient
+ *
+ * This must be set upon initialization and is a required variable
+ *
+ * @return applicationId
+ */
+ public String getApplicationId() {
+ return applicationId;
+ }
+
+ /**
+ * Gets the steam id associated with this IPCClient, if any
+ *
+ * This must be set upon initialization and is an optional variable
+ * Default: UTF-8
+ *
+ * @return encoding
+ */
+ public String getEncoding() {
+ return this.encoding;
+ }
+
+ /**
+ * Sets the encoding to send packets in.
+ *
+ * This can be set safely before a call to {@link #connect(DiscordBuild...)}
+ * is made.
+ *
+ * Default: UTF-8
+ *
+ * @param encoding for this IPCClient.
+ */
+ public void setEncoding(final String encoding) {
+ this.encoding = encoding;
+ }
+
+ /**
+ * Gets the client ID associated with this IPCClient
+ *
+ * @return the client id
+ */
+ public long getClientID() {
+ return this.clientId;
+ }
+
+ /**
+ * Gets whether this IPCClient is in Debug Mode
+ * Default: False
+ *
+ * @return The Debug Mode Status
+ */
+ public boolean isDebugMode() {
+ return debugMode;
+ }
+
+ /**
+ * Sets whether this IPCClient is in Debug Mode
+ *
+ * @param debugMode The Debug Mode Status
+ */
+ public void setDebugMode(boolean debugMode) {
+ this.debugMode = debugMode;
+ }
+
+ /**
+ * Gets whether this IPCClient will show verbose logging
+ * Default: False
+ *
+ * @return The Verbose Logging Status
+ */
+ public boolean isVerboseLogging() {
+ return verboseLogging;
+ }
+
+ /**
+ * Sets whether this IPCClient will show verbose logging
+ *
+ * @param verboseLogging The Verbose Mode Status
+ */
+ public void setVerboseLogging(boolean verboseLogging) {
+ this.verboseLogging = verboseLogging;
+ }
+
/**
* Opens the connection between the IPCClient and Discord.
*
@@ -101,75 +356,131 @@ public void setListener(IPCListener listener)
* IPCClient and Discord.
*
* @param preferredOrder the priority order of client builds to connect to
- *
- * @throws IllegalStateException
- * There is an open connection on this IPCClient.
- * @throws NoDiscordClientException
- * No client of the provided {@link DiscordBuild build type}(s) was found.
+ * @throws IllegalStateException There is an open connection on this IPCClient.
+ * @throws NoDiscordClientException No client of the provided {@link DiscordBuild build type}(s) was found.
*/
- public void connect(DiscordBuild... preferredOrder) throws NoDiscordClientException
- {
+ @SuppressWarnings("BusyWait")
+ public void connect(DiscordBuild... preferredOrder) throws NoDiscordClientException {
checkConnected(false);
+ long timeToConnect;
+ while ((timeToConnect = nextDelay - System.currentTimeMillis()) > 0) {
+ if (debugMode) {
+ getCurrentLogger(LOGGER).info("[DEBUG] Attempting connection in: " + timeToConnect + "ms");
+ }
+ try {
+ Thread.sleep(timeToConnect);
+ } catch (InterruptedException ignored) {
+ }
+ }
callbacks.clear();
pipe = null;
- pipe = Pipe.openPipe(this, clientId, callbacks, preferredOrder);
+ try {
+ pipe = Pipe.openPipe(this, clientId, callbacks, preferredOrder);
+ } catch (Exception ex) {
+ updateReconnectTime();
+ throw ex;
+ }
- LOGGER.debug("Client is now connected and ready!");
- if(listener != null)
+ if (isAutoRegister()) {
+ try {
+ if (optionalSteamId != null && !optionalSteamId.isEmpty())
+ this.registerSteamGame(getApplicationId(), optionalSteamId);
+ else
+ this.registerApp(getApplicationId(), null);
+ } catch (Throwable ex) {
+ if (debugMode) {
+ getCurrentLogger(LOGGER).error("Unable to register application", ex);
+ } else {
+ getCurrentLogger(LOGGER).error("Unable to register application, enable debug mode for trace...");
+ }
+ }
+ }
+
+ if (debugMode) {
+ getCurrentLogger(LOGGER).info("[DEBUG] Client is now connected and ready!");
+ }
+
+ if (listener != null) {
listener.onReady(this);
+ pipe.setListener(listener);
+ }
startReading();
}
-
+
/**
- * Sends a {@link RichPresence} to the Discord client.
- *
+ * Sends a {@link RichPresence} to the Discord client.
+ *
* This is where the IPCClient will officially display
- * a Rich Presence in the Discord client.
- *
+ * a Rich Presence in the Discord client.
+ *
* Sending this again will overwrite the last provided
* {@link RichPresence}.
*
* @param presence The {@link RichPresence} to send.
- *
- * @throws IllegalStateException
- * If a connection was not made prior to invoking
- * this method.
- *
+ * @throws IllegalStateException If a connection was not made prior to invoking
+ * this method.
* @see RichPresence
*/
- public void sendRichPresence(RichPresence presence)
- {
+ public void sendRichPresence(RichPresence presence) {
sendRichPresence(presence, null);
}
-
+
/**
- * Sends a {@link RichPresence} to the Discord client.
- *
+ * Sends a {@link RichPresence} to the Discord client.
+ *
* This is where the IPCClient will officially display
- * a Rich Presence in the Discord client.
- *
+ * a Rich Presence in the Discord client.
+ *
* Sending this again will overwrite the last provided
* {@link RichPresence}.
*
* @param presence The {@link RichPresence} to send.
* @param callback A {@link Callback} to handle success or error
- *
- * @throws IllegalStateException
- * If a connection was not made prior to invoking
- * this method.
- *
+ * @throws IllegalStateException If a connection was not made prior to invoking
+ * this method.
* @see RichPresence
*/
- public void sendRichPresence(RichPresence presence, Callback callback)
- {
+ public void sendRichPresence(RichPresence presence, Callback callback) {
checkConnected(true);
- LOGGER.debug("Sending RichPresence to discord: "+(presence == null ? null : presence.toJson().toString()));
- pipe.send(OpCode.FRAME, new JSONObject()
- .put("cmd","SET_ACTIVITY")
- .put("args", new JSONObject()
- .put("pid",getPID())
- .put("activity",presence == null ? null : presence.toJson())), callback);
+
+ if (debugMode) {
+ getCurrentLogger(LOGGER).info("[DEBUG] Sending RichPresence to discord: " + presence.toDecodedJson(encoding));
+ }
+
+ // Setup and Send JsonObject Data Representing an RPC Update
+ JsonObject finalObject = new JsonObject(),
+ args = new JsonObject();
+
+ finalObject.addProperty("cmd", "SET_ACTIVITY");
+
+ args.addProperty("pid", getPID());
+ args.add("activity", presence.toJson());
+
+ finalObject.add("args", args);
+ pipe.send(OpCode.FRAME, finalObject, callback);
+ }
+
+ /**
+ * Manually register a steam game
+ *
+ * @param applicationId Application ID
+ * @param optionalSteamId Application Steam ID
+ */
+ public void registerSteamGame(String applicationId, String optionalSteamId) {
+ if (this.pipe != null)
+ this.pipe.registerSteamGame(applicationId, optionalSteamId);
+ }
+
+ /**
+ * Manually register an application
+ *
+ * @param applicationId Application ID
+ * @param command Command to run the application
+ */
+ public void registerApp(String applicationId, String command) {
+ if (this.pipe != null)
+ this.pipe.registerApp(applicationId, command);
}
/**
@@ -181,16 +492,13 @@ public void sendRichPresence(RichPresence presence, Callback callback)
* and creating a new one.
*
* @param sub The event {@link Event} to add.
- *
- * @throws IllegalStateException
- * If a connection was not made prior to invoking
- * this method.
+ * @throws IllegalStateException If a connection was not made prior to invoking
+ * this method.
*/
- public void subscribe(Event sub)
- {
+ public void subscribe(Event sub) {
subscribe(sub, null);
}
-
+
/**
* Adds an event {@link Event} to this IPCClient.
- *
+ * Gets the IPCClient's {@link DiscordBuild}.
+ *
* This is always the first specified DiscordBuild when
* making a call to {@link #connect(DiscordBuild...)},
* or the first one found if none or {@link DiscordBuild#ANY}
- * is specified.
- *
+ * is specified.
+ *
* Note that specifying ANY doesn't mean that this will return
* ANY. In fact this method should never return the
* value ANY.
*
* @return The {@link DiscordBuild} of this IPCClient, or null if not connected.
*/
- public DiscordBuild getDiscordBuild()
- {
+ public DiscordBuild getDiscordBuild() {
if (pipe == null) return null;
return pipe.getDiscordBuild();
}
/**
- * Constants representing events that can be subscribed to
- * using {@link #subscribe(Event)}.
+ * Gets the IPCClient's current {@link User} attached to the target {@link DiscordBuild}.
+ *
+ * This is always the User Data attached to the DiscordBuild found when
+ * making a call to {@link #connect(DiscordBuild...)}
+ *
+ * Note that this value should NOT return null under any circumstances.
*
- * Each event corresponds to a different function as a
- * component of the Rich Presence.
+ * Each event corresponds to a different function as a
+ * component of the Rich Presence.
- *
+ * An implementable listener used to handle events caught by an {@link IPCClient}.
+ *
* Can be attached to an IPCClient using {@link IPCClient#setListener(IPCListener)}.
*
* @author John Grosh (john.a.grosh@gmail.com)
*/
-public interface IPCListener
-{
+public interface IPCListener {
/**
* Fired whenever an {@link IPCClient} sends a {@link Packet} to Discord.
*
* @param client The IPCClient sending the Packet.
* @param packet The Packet being sent.
*/
- default void onPacketSent(IPCClient client, Packet packet) {}
+ void onPacketSent(IPCClient client, Packet packet);
/**
* Fired whenever an {@link IPCClient} receives a {@link Packet} to Discord.
@@ -42,7 +42,7 @@ default void onPacketSent(IPCClient client, Packet packet) {}
* @param client The IPCClient receiving the Packet.
* @param packet The Packet being received.
*/
- default void onPacketReceived(IPCClient client, Packet packet) {}
+ void onPacketReceived(IPCClient client, Packet packet);
/**
* Fired whenever a RichPresence activity informs us that
@@ -51,7 +51,7 @@ default void onPacketReceived(IPCClient client, Packet packet) {}
* @param client The IPCClient receiving the event.
* @param secret The secret of the event, determined by the implementation and specified by the user.
*/
- default void onActivityJoin(IPCClient client, String secret) {}
+ void onActivityJoin(IPCClient client, String secret);
/**
* Fired whenever a RichPresence activity informs us that
@@ -60,42 +60,42 @@ default void onActivityJoin(IPCClient client, String secret) {}
* @param client The IPCClient receiving the event.
* @param secret The secret of the event, determined by the implementation and specified by the user.
*/
- default void onActivitySpectate(IPCClient client, String secret) {}
+ void onActivitySpectate(IPCClient client, String secret);
/**
* Fired whenever a RichPresence activity informs us that
- * a user has clicked a "ask to join" button.
- *
+ * a user has clicked an "ask to join" button.
+ *
* As opposed to {@link #onActivityJoin(IPCClient, String)},
* this also provides packaged {@link User} data.
*
* @param client The IPCClient receiving the event.
* @param secret The secret of the event, determined by the implementation and specified by the user.
- * @param user The user who clicked the clicked the event, containing data on the account.
+ * @param user The user who clicked the event, containing data on the account.
*/
- default void onActivityJoinRequest(IPCClient client, String secret, User user) {}
+ void onActivityJoinRequest(IPCClient client, String secret, User user);
/**
* Fired whenever an {@link IPCClient} is ready and connected to Discord.
*
* @param client The now ready IPCClient.
*/
- default void onReady(IPCClient client) {}
+ void onReady(IPCClient client);
/**
* Fired whenever an {@link IPCClient} has closed.
*
* @param client The now closed IPCClient.
- * @param json A {@link JSONObject} with close data.
+ * @param json A {@link JsonObject} with close data.
*/
- default void onClose(IPCClient client, JSONObject json) {}
+ void onClose(IPCClient client, JsonObject json);
/**
* Fired whenever an {@link IPCClient} has disconnected,
* either due to bad data or an exception.
*
* @param client The now closed IPCClient.
- * @param t A {@link Throwable} responsible for the disconnection.
+ * @param t A {@link Throwable} responsible for the disconnection.
*/
- default void onDisconnect(IPCClient client, Throwable t) {}
+ void onDisconnect(IPCClient client, Throwable t);
}
diff --git a/src/main/java/com/jagrosh/discordipc/entities/ActivityType.java b/src/main/java/com/jagrosh/discordipc/entities/ActivityType.java
new file mode 100644
index 0000000..d8ed770
--- /dev/null
+++ b/src/main/java/com/jagrosh/discordipc/entities/ActivityType.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2017 John Grosh (john.a.grosh@gmail.com).
+ *
+ * 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
+ *
+ * http://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.
+ */
+
+package com.jagrosh.discordipc.entities;
+
+/**
+ * Constants representing various Discord client activity types,
+ * such as Playing or Listening
+ */
+public enum ActivityType {
+ /**
+ * Constant for the "Playing" Discord RPC Activity type.
+ */
+ Playing,
+
+ /**
+ * Constant for the "Streaming" Discord RPC Activity type.
+ */
+ Streaming,
+
+ /**
+ * Constant for the "Listening" Discord RPC Activity type.
+ */
+ Listening,
+
+ /**
+ * Constant for the "Watching" Discord RPC Activity type.
+ */
+ Watching,
+
+ /**
+ * Constant for the "Custom" Discord RPC Activity type.
+ */
+ Custom,
+
+ /**
+ * Constant for the "Competing" Discord RPC Activity type.
+ */
+ Competing;
+
+ /**
+ * Gets a {@link ActivityType} matching the specified index.
+ *
+ * This is only internally implemented.
+ *
+ * @param index The index to get from.
+ * @return The {@link ActivityType} corresponding to the parameters, or
+ * {@link ActivityType#Playing} if none match.
+ */
+ public static ActivityType from(int index) {
+ for (ActivityType value : values()) {
+ if (value.ordinal() == index) {
+ return value;
+ }
+ }
+ return Playing;
+ }
+}
diff --git a/src/main/java/com/jagrosh/discordipc/entities/Callback.java b/src/main/java/com/jagrosh/discordipc/entities/Callback.java
index 128fcdf..2694299 100644
--- a/src/main/java/com/jagrosh/discordipc/entities/Callback.java
+++ b/src/main/java/com/jagrosh/discordipc/entities/Callback.java
@@ -13,29 +13,30 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package com.jagrosh.discordipc.entities;
+import com.jagrosh.discordipc.IPCClient;
+
import java.util.function.Consumer;
/**
* A callback for asynchronous logic when dealing processes that
- * would normally block the calling thread.
- *
- * This is most visibly implemented in {@link com.jagrosh.discordipc.IPCClient IPCClient}.
+ * would normally block the calling thread.
+ *
+ * This is most visibly implemented in {@link IPCClient IPCClient}.
*
* @author John Grosh (john.a.grosh@gmail.com)
*/
-public class Callback
-{
+public class Callback {
private final Consumer
- *
+ * that the first valid build will be used.
+ *
* Other than this exact function, there is no use for this value.
*/
ANY;
private final String endpoint;
- DiscordBuild(String endpoint)
- {
+ DiscordBuild(String endpoint) {
this.endpoint = endpoint;
}
- DiscordBuild()
- {
+ DiscordBuild() {
this(null);
}
/**
- * Gets a {@link DiscordBuild} matching the specified endpoint.
+ * Gets a {@link DiscordBuild} matching the specified index.
+ *
+ * This is only internally implemented.
*
+ * @param index The index to get from.
+ * @return The {@link DiscordBuild} corresponding to the parameters, or
+ * {@link DiscordBuild#ANY} if none match.
+ */
+ public static DiscordBuild from(int index) {
+ for (DiscordBuild value : values()) {
+ if (value.ordinal() == index) {
+ return value;
+ }
+ }
+ return ANY;
+ }
+
+ /**
+ * Gets a {@link DiscordBuild} matching the specified endpoint.
+ *
* This is only internally implemented.
*
* @param endpoint The endpoint to get from.
- *
* @return The DiscordBuild corresponding to the endpoint, or
- * {@link DiscordBuild#ANY} if none match.
+ * {@link DiscordBuild#ANY} if none match.
*/
- public static DiscordBuild from(String endpoint)
- {
- for(DiscordBuild value : values())
- {
- if(value.endpoint != null && value.endpoint.equals(endpoint))
- {
+ public static DiscordBuild from(String endpoint) {
+ for (DiscordBuild value : values()) {
+ if (value.endpoint != null && value.endpoint.equals(endpoint)) {
return value;
}
}
diff --git a/src/main/java/com/jagrosh/discordipc/entities/Packet.java b/src/main/java/com/jagrosh/discordipc/entities/Packet.java
index 26d218d..36f789c 100644
--- a/src/main/java/com/jagrosh/discordipc/entities/Packet.java
+++ b/src/main/java/com/jagrosh/discordipc/entities/Packet.java
@@ -13,33 +13,49 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package com.jagrosh.discordipc.entities;
+import com.google.gson.JsonObject;
+import com.jagrosh.discordipc.IPCClient;
+import com.jagrosh.discordipc.IPCListener;
+
+import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
-import org.json.JSONObject;
/**
- * A data-packet received from Discord via an {@link com.jagrosh.discordipc.IPCClient IPCClient}.
+ * This is only internally implemented.
+ *
+ * @param index The index to get from.
+ * @return The {@link PartyPrivacy} corresponding to the parameters, or
+ * {@link PartyPrivacy#Public} if none match.
+ */
+ public static PartyPrivacy from(int index) {
+ for (PartyPrivacy value : values()) {
+ if (value.ordinal() == index) {
+ return value;
+ }
+ }
+ return Public;
+ }
+}
diff --git a/src/main/java/com/jagrosh/discordipc/entities/RichPresence.java b/src/main/java/com/jagrosh/discordipc/entities/RichPresence.java
index 78774c5..77f4ecf 100644
--- a/src/main/java/com/jagrosh/discordipc/entities/RichPresence.java
+++ b/src/main/java/com/jagrosh/discordipc/entities/RichPresence.java
@@ -13,11 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package com.jagrosh.discordipc.entities;
-import java.time.OffsetDateTime;
-import org.json.JSONArray;
-import org.json.JSONObject;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+
+import java.util.Objects;
/**
* An encapsulation of all data needed to properly construct a JSON RichPresence payload.
@@ -26,100 +29,272 @@
*
* @author John Grosh (john.a.grosh@gmail.com)
*/
-public class RichPresence
-{
+public class RichPresence {
+ private static final int MIN_ALLOWED_BUTTONS = 1;
+ private static final int MAX_ALLOWED_BUTTONS = 2;
+ private final ActivityType activityType;
+ private final StatusDisplayType statusDisplayType;
private final String state;
+ private final String stateUrl;
private final String details;
- private final OffsetDateTime startTimestamp;
- private final OffsetDateTime endTimestamp;
+ private final String detailsUrl;
+ private final String name;
+ private final long startTimestamp;
+ private final long endTimestamp;
private final String largeImageKey;
private final String largeImageText;
+ private final String largeImageUrl;
private final String smallImageKey;
private final String smallImageText;
+ private final String smallImageUrl;
private final String partyId;
private final int partySize;
private final int partyMax;
+ private final PartyPrivacy partyPrivacy;
private final String matchSecret;
private final String joinSecret;
private final String spectateSecret;
+ private final JsonArray buttons;
private final boolean instance;
-
- public RichPresence(String state, String details, OffsetDateTime startTimestamp, OffsetDateTime endTimestamp,
- String largeImageKey, String largeImageText, String smallImageKey, String smallImageText,
- String partyId, int partySize, int partyMax, String matchSecret, String joinSecret,
- String spectateSecret, boolean instance)
- {
+
+ public RichPresence(ActivityType activityType, StatusDisplayType statusDisplayType,
+ String state, String stateUrl,
+ String details, String detailsUrl,
+ String name, long startTimestamp, long endTimestamp,
+ String largeImageKey, String largeImageText, String largeImageUrl,
+ String smallImageKey, String smallImageText, String smallImageUrl,
+ String partyId, int partySize, int partyMax, PartyPrivacy partyPrivacy,
+ String matchSecret, String joinSecret, String spectateSecret,
+ JsonArray buttons, boolean instance) {
+ this.activityType = activityType;
+ this.statusDisplayType = statusDisplayType;
this.state = state;
+ this.stateUrl = stateUrl;
this.details = details;
+ this.detailsUrl = detailsUrl;
+ this.name = name;
this.startTimestamp = startTimestamp;
this.endTimestamp = endTimestamp;
this.largeImageKey = largeImageKey;
this.largeImageText = largeImageText;
+ this.largeImageUrl = largeImageUrl;
this.smallImageKey = smallImageKey;
this.smallImageText = smallImageText;
+ this.smallImageUrl = smallImageUrl;
this.partyId = partyId;
this.partySize = partySize;
this.partyMax = partyMax;
+ this.partyPrivacy = partyPrivacy;
this.matchSecret = matchSecret;
this.joinSecret = joinSecret;
this.spectateSecret = spectateSecret;
+ this.buttons = buttons;
this.instance = instance;
}
/**
- * Constructs a {@link JSONObject} representing a payload to send to discord
+ * Constructs a {@link JsonObject} representing a payload to send to discord
* to update a user's Rich Presence.
*
- * This is purely internal, and should not ever need to be called outside of
+ * This is purely internal, and should not ever need to be called outside
* the library.
*
* @return A JSONObject payload for updating a user's Rich Presence.
*/
- public JSONObject toJson()
- {
- return new JSONObject()
- .put("state", state)
- .put("details", details)
- .put("timestamps", new JSONObject()
- .put("start", startTimestamp==null ? null : startTimestamp.toEpochSecond())
- .put("end", endTimestamp==null ? null : endTimestamp.toEpochSecond()))
- .put("assets", new JSONObject()
- .put("large_image", largeImageKey)
- .put("large_text", largeImageText)
- .put("small_image", smallImageKey)
- .put("small_text", smallImageText))
- .put("party", partyId==null ? null : new JSONObject()
- .put("id", partyId)
- .put("size", new JSONArray().put(partySize).put(partyMax)))
- .put("secrets", new JSONObject()
- .put("join", joinSecret)
- .put("spectate", spectateSecret)
- .put("match", matchSecret))
- .put("instance", instance);
+ public JsonObject toJson() {
+ JsonObject timestamps = new JsonObject(),
+ assets = new JsonObject(),
+ party = new JsonObject(),
+ secrets = new JsonObject(),
+ finalObject = new JsonObject();
+
+ if (startTimestamp > 0) {
+ timestamps.addProperty("start", startTimestamp);
+
+ if (endTimestamp > startTimestamp) {
+ timestamps.addProperty("end", endTimestamp);
+ }
+ }
+
+ if (largeImageKey != null && !largeImageKey.isEmpty()) {
+ assets.addProperty("large_image", largeImageKey);
+
+ if (largeImageText != null && !largeImageText.isEmpty()) {
+ assets.addProperty("large_text", largeImageText);
+ }
+ if (largeImageUrl != null && !largeImageUrl.isEmpty()) {
+ assets.addProperty("large_url", largeImageUrl);
+ }
+ }
+
+ if (smallImageKey != null && !smallImageKey.isEmpty()) {
+ assets.addProperty("small_image", smallImageKey);
+
+ if (smallImageText != null && !smallImageText.isEmpty()) {
+ assets.addProperty("small_text", smallImageText);
+ }
+ if (smallImageUrl != null && !smallImageUrl.isEmpty()) {
+ assets.addProperty("small_url", smallImageUrl);
+ }
+ }
+
+ if ((partyId != null && !partyId.isEmpty()) ||
+ (partySize > 0 && partyMax > 0)) {
+ if (partyId != null && !partyId.isEmpty()) {
+ party.addProperty("id", partyId);
+ }
+
+ JsonArray partyData = new JsonArray();
+
+ if (partySize > 0) {
+ partyData.add(new JsonPrimitive(partySize));
+
+ if (partyMax >= partySize) {
+ partyData.add(new JsonPrimitive(partyMax));
+ }
+ }
+ party.add("size", partyData);
+ party.add("privacy", new JsonPrimitive(partyPrivacy.ordinal()));
+ }
+
+ if (joinSecret != null && !joinSecret.isEmpty()) {
+ secrets.addProperty("join", joinSecret);
+ }
+ if (spectateSecret != null && !spectateSecret.isEmpty()) {
+ secrets.addProperty("spectate", spectateSecret);
+ }
+ if (matchSecret != null && !matchSecret.isEmpty()) {
+ secrets.addProperty("match", matchSecret);
+ }
+
+ finalObject.addProperty("type", activityType.ordinal());
+ finalObject.addProperty("status_display_type", statusDisplayType.ordinal());
+
+ if (state != null && !state.isEmpty()) {
+ finalObject.addProperty("state", state);
+
+ if (stateUrl != null && !stateUrl.isEmpty()) {
+ finalObject.addProperty("state_url", stateUrl);
+ }
+ }
+ if (details != null && !details.isEmpty()) {
+ finalObject.addProperty("details", details);
+
+ if (detailsUrl != null && !detailsUrl.isEmpty()) {
+ finalObject.addProperty("details_url", detailsUrl);
+ }
+ }
+
+ if (name != null && !name.isEmpty()) {
+ finalObject.addProperty("name", name);
+ }
+
+ if (timestamps.has("start")) {
+ finalObject.add("timestamps", timestamps);
+ }
+ if (assets.has("large_image")) {
+ finalObject.add("assets", assets);
+ }
+ if (party.has("id")) {
+ finalObject.add("party", party);
+ }
+ if (secrets.has("join") || secrets.has("spectate") || secrets.has("match")) {
+ finalObject.add("secrets", secrets);
+ }
+ if (buttons != null && !buttons.isJsonNull() && buttons.size() >= MIN_ALLOWED_BUTTONS && buttons.size() <= MAX_ALLOWED_BUTTONS) {
+ finalObject.add("buttons", buttons);
+ }
+ finalObject.addProperty("instance", instance);
+
+ return finalObject;
+ }
+
+ public String toDecodedJson(String encoding) {
+ try {
+ return new String(toJson().toString().getBytes(encoding));
+ } catch (Exception ex) {
+ return toJson().toString();
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof RichPresence))
+ return false;
+ RichPresence oPresence = (RichPresence) o;
+ return this == oPresence || (
+ Objects.equals(activityType, oPresence.activityType) &&
+ Objects.equals(statusDisplayType, oPresence.statusDisplayType) &&
+ Objects.equals(state, oPresence.state) &&
+ Objects.equals(stateUrl, oPresence.stateUrl) &&
+ Objects.equals(details, oPresence.details) &&
+ Objects.equals(detailsUrl, oPresence.detailsUrl) &&
+ Objects.equals(name, oPresence.name) &&
+ Objects.equals(startTimestamp, oPresence.startTimestamp) &&
+ Objects.equals(endTimestamp, oPresence.endTimestamp) &&
+ Objects.equals(largeImageKey, oPresence.largeImageKey) &&
+ Objects.equals(largeImageText, oPresence.largeImageText) &&
+ Objects.equals(largeImageUrl, oPresence.largeImageUrl) &&
+ Objects.equals(smallImageKey, oPresence.smallImageKey) &&
+ Objects.equals(smallImageText, oPresence.smallImageText) &&
+ Objects.equals(smallImageUrl, oPresence.smallImageUrl) &&
+ Objects.equals(partyId, oPresence.partyId) &&
+ Objects.equals(partySize, oPresence.partySize) &&
+ Objects.equals(partyMax, oPresence.partyMax) &&
+ Objects.equals(partyPrivacy, oPresence.partyPrivacy) &&
+ Objects.equals(matchSecret, oPresence.matchSecret) &&
+ Objects.equals(joinSecret, oPresence.joinSecret) &&
+ Objects.equals(spectateSecret, oPresence.spectateSecret) &&
+ Objects.equals(buttons, oPresence.buttons) &&
+ Objects.equals(instance, oPresence.instance)
+ );
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ activityType, statusDisplayType,
+ state, stateUrl,
+ details, detailsUrl,
+ name, startTimestamp, endTimestamp,
+ largeImageKey, largeImageText, largeImageUrl,
+ smallImageKey, smallImageText, smallImageUrl,
+ partyId, partySize, partyMax, partyPrivacy,
+ matchSecret, joinSecret, spectateSecret,
+ buttons, instance
+ );
}
/**
* A chain builder for a {@link RichPresence} object.
*
* An accurate description of each field and it's functions can be found
- * here
+ * here
*/
- public static class Builder
- {
+ public static class Builder {
+ private ActivityType activityType;
+ private StatusDisplayType statusDisplayType;
private String state;
+ private String stateUrl;
private String details;
- private OffsetDateTime startTimestamp;
- private OffsetDateTime endTimestamp;
+ private String detailsUrl;
+ private String name;
+ private long startTimestamp;
+ private long endTimestamp;
private String largeImageKey;
private String largeImageText;
+ private String largeImageUrl;
private String smallImageKey;
private String smallImageText;
+ private String smallImageUrl;
private String partyId;
private int partySize;
private int partyMax;
+ private PartyPrivacy partyPrivacy;
private String matchSecret;
private String joinSecret;
private String spectateSecret;
+ private JsonArray buttons;
private boolean instance;
/**
@@ -127,49 +302,102 @@ public static class Builder
*
* @return The RichPresence built.
*/
- public RichPresence build()
- {
- return new RichPresence(state, details, startTimestamp, endTimestamp,
- largeImageKey, largeImageText, smallImageKey, smallImageText,
- partyId, partySize, partyMax, matchSecret, joinSecret,
- spectateSecret, instance);
+ public RichPresence build() {
+ return new RichPresence(activityType, statusDisplayType,
+ state, stateUrl,
+ details, detailsUrl,
+ name, startTimestamp, endTimestamp,
+ largeImageKey, largeImageText, largeImageUrl,
+ smallImageKey, smallImageText, smallImageUrl,
+ partyId, partySize, partyMax, partyPrivacy,
+ matchSecret, joinSecret, spectateSecret,
+ buttons, instance);
+ }
+
+ /**
+ * Sets the activity type for the player's current activity
+ *
+ * @param activityType The new activity type
+ * @return This Builder.
+ */
+ public Builder setActivityType(ActivityType activityType) {
+ this.activityType = activityType;
+ return this;
+ }
+
+ /**
+ * Sets the status display type for the player's current activity
+ *
+ * @param statusDisplayType The new status display type
+ * @return This Builder.
+ */
+ public Builder setStatusDisplayType(StatusDisplayType statusDisplayType) {
+ this.statusDisplayType = statusDisplayType;
+ return this;
}
/**
* Sets the state of the user's current party.
*
* @param state The state of the user's current party.
- *
* @return This Builder.
*/
- public Builder setState(String state)
- {
+ public Builder setState(String state) {
this.state = state;
return this;
}
+ /**
+ * Sets the state url of the user's current party
+ *
+ * @param stateUrl The state url of the user's current party.
+ * @return This Builder.
+ */
+ public Builder setStateUrl(String stateUrl) {
+ this.stateUrl = stateUrl;
+ return this;
+ }
+
/**
* Sets details of what the player is currently doing.
*
* @param details The details of what the player is currently doing.
- *
* @return This Builder.
*/
- public Builder setDetails(String details)
- {
+ public Builder setDetails(String details) {
this.details = details;
return this;
}
+ /**
+ * Sets the details url of what the player is currently doing.
+ *
+ * @param detailsUrl The details url of what the player is currently doing
+ * @return This Builder.
+ */
+ public Builder setDetailsUrl(String detailsUrl) {
+ this.detailsUrl = detailsUrl;
+ return this;
+ }
+
+ /**
+ * Sets the player activity name.
+ *
+ * @param name The player activity name.
+ * @return This Builder.
+ */
+ public Builder setName(String name) {
+ this.name = name;
+ return this;
+ }
+
/**
* Sets the time that the player started a match or activity.
*
* @param startTimestamp The time the player started a match or activity.
- *
* @return This Builder.
*/
- public Builder setStartTimestamp(OffsetDateTime startTimestamp)
- {
+ public Builder setStartTimestamp(long startTimestamp) {
this.startTimestamp = startTimestamp;
return this;
}
@@ -178,81 +406,135 @@ public Builder setStartTimestamp(OffsetDateTime startTimestamp)
* Sets the time that the player's current activity will end.
*
* @param endTimestamp The time the player's activity will end.
- *
* @return This Builder.
*/
- public Builder setEndTimestamp(OffsetDateTime endTimestamp)
- {
+ public Builder setEndTimestamp(long endTimestamp) {
this.endTimestamp = endTimestamp;
return this;
}
/**
* Sets the key of the uploaded image for the large profile artwork, as well as
- * the text tooltip shown when a cursor hovers over it.
+ * the text tooltip shown when a cursor hovers over it, and the url when clicked.
*
- * These can be configured in the applications
+ * These can be configured in the applications
* page on the discord website.
*
- * @param largeImageKey A key to an image to display.
+ * @param largeImageKey A key to an image to display.
* @param largeImageText Text displayed when a cursor hovers over the large image.
- *
+ * @param largeImageUrl The Url to navigate to when clicking the large image.
* @return This Builder.
*/
- public Builder setLargeImage(String largeImageKey, String largeImageText)
- {
+ public Builder setLargeImage(String largeImageKey, String largeImageText, String largeImageUrl) {
this.largeImageKey = largeImageKey;
this.largeImageText = largeImageText;
+ this.largeImageUrl = largeImageUrl;
return this;
}
+ /**
+ * Sets the key of the uploaded image for the large profile artwork, as well as
+ * the text tooltip shown when a cursor hovers over it.
+ *
+ * These can be configured in the applications
+ * page on the discord website.
+ *
+ * @param largeImageKey A key to an image to display.
+ * @param largeImageText Text displayed when a cursor hovers over the large image.
+ * @return This Builder.
+ */
+ public Builder setLargeImageWithTooltip(String largeImageKey, String largeImageText) {
+ return setLargeImage(largeImageKey, largeImageText, null);
+ }
+
+ /**
+ * Sets the key of the uploaded image for the large profile artwork, as well as
+ * the url to navigate to when clicked.
+ *
+ * These can be configured in the applications
+ * page on the discord website.
+ *
+ * @param largeImageKey A key to an image to display.
+ * @param largeImageUrl The Url to navigate to when clicking the large image.
+ * @return This Builder.
+ */
+ public Builder setLargeImageWithUrl(String largeImageKey, String largeImageUrl) {
+ return setLargeImage(largeImageKey, null, largeImageUrl);
+ }
+
/**
* Sets the key of the uploaded image for the large profile artwork.
*
- * These can be configured in the applications
+ * These can be configured in the applications
* page on the discord website.
*
* @param largeImageKey A key to an image to display.
- *
* @return This Builder.
*/
- public Builder setLargeImage(String largeImageKey)
- {
- return setLargeImage(largeImageKey, null);
+ public Builder setLargeImage(String largeImageKey) {
+ return setLargeImage(largeImageKey, null, null);
}
/**
* Sets the key of the uploaded image for the small profile artwork, as well as
- * the text tooltip shown when a cursor hovers over it.
+ * the text tooltip shown when a cursor hovers over it, and the url when clicked.
*
- * These can be configured in the applications
+ * These can be configured in the applications
* page on the discord website.
*
- * @param smallImageKey A key to an image to display.
+ * @param smallImageKey A key to an image to display.
* @param smallImageText Text displayed when a cursor hovers over the small image.
- *
+ * @param smallImageUrl The Url to navigate to when clicking the small image.
* @return This Builder.
*/
- public Builder setSmallImage(String smallImageKey, String smallImageText)
- {
+ public Builder setSmallImage(String smallImageKey, String smallImageText, String smallImageUrl) {
this.smallImageKey = smallImageKey;
this.smallImageText = smallImageText;
+ this.smallImageUrl = smallImageUrl;
return this;
}
+ /**
+ * Sets the key of the uploaded image for the small profile artwork, as well as
+ * the text tooltip shown when a cursor hovers over it.
+ *
+ * These can be configured in the applications
+ * page on the discord website.
+ *
+ * @param smallImageKey A key to an image to display.
+ * @param smallImageText Text displayed when a cursor hovers over the small image.
+ * @return This Builder.
+ */
+ public Builder setSmallImageWithTooltip(String smallImageKey, String smallImageText) {
+ return setSmallImage(smallImageKey, smallImageText, null);
+ }
+
+ /**
+ * Sets the key of the uploaded image for the small profile artwork, as well as
+ * the url to navigate to when clicked.
+ *
+ * These can be configured in the applications
+ * page on the discord website.
+ *
+ * @param smallImageKey A key to an image to display.
+ * @param smallImageUrl The Url to navigate to when clicking the small image.
+ * @return This Builder.
+ */
+ public Builder setSmallImageWithUrl(String smallImageKey, String smallImageUrl) {
+ return setSmallImage(smallImageKey, null, smallImageUrl);
+ }
+
/**
* Sets the key of the uploaded image for the small profile artwork.
*
- * These can be configured in the applications
+ * These can be configured in the applications
* page on the discord website.
*
* @param smallImageKey A key to an image to display.
- *
* @return This Builder.
*/
- public Builder setSmallImage(String smallImageKey)
- {
- return setSmallImage(smallImageKey, null);
+ public Builder setSmallImage(String smallImageKey) {
+ return setSmallImage(smallImageKey, null, null);
}
/**
@@ -262,17 +544,17 @@ public Builder setSmallImage(String smallImageKey)
* Must be a format of {'label': "...", 'url': "..."} with a max length of 2
+ * This is only internally implemented.
+ *
+ * @param index The index to get from.
+ * @return The {@link StatusDisplayType} corresponding to the parameters, or
+ * {@link StatusDisplayType#Name} if none match.
+ */
+ public static StatusDisplayType from(int index) {
+ for (StatusDisplayType value : values()) {
+ if (value.ordinal() == index) {
+ return value;
+ }
+ }
+ return Name;
+ }
+}
diff --git a/src/main/java/com/jagrosh/discordipc/entities/User.java b/src/main/java/com/jagrosh/discordipc/entities/User.java
index 80ea17d..e6985d8 100644
--- a/src/main/java/com/jagrosh/discordipc/entities/User.java
+++ b/src/main/java/com/jagrosh/discordipc/entities/User.java
@@ -13,19 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package com.jagrosh.discordipc.entities;
+import com.jagrosh.discordipc.IPCClient;
+import com.jagrosh.discordipc.IPCListener;
+
/**
- * A encapsulation of a Discord User's data provided when a
- * {@link com.jagrosh.discordipc.IPCListener IPCListener} fires
- * {@link com.jagrosh.discordipc.IPCListener#onActivityJoinRequest(com.jagrosh.discordipc.IPCClient, String, User)
+ * An encapsulation of a Discord User's data provided when a
+ * {@link IPCListener IPCListener} fires
+ * {@link IPCListener#onActivityJoinRequest(IPCClient, String, User)
* onActivityJoinRequest}.
*
* @author John Grosh (john.a.grosh@gmail.com)
*/
-public class User
-{
- private final String name;
+public class User {
+ private final String username;
+ private final String nickname;
private final String discriminator;
private final long id;
private final String avatar;
@@ -33,14 +37,16 @@ public class User
/**
* Constructs a new {@link User}.
- *
+ * Gets whether this User is a bot.
+ *
* While, at the time of writing this documentation, bots cannot
* use Rich Presence features, there may be a time in the future
* where they have such an ability.
*
* @return False
*/
- public boolean isBot()
- {
+ public boolean isBot() {
return false; //bots cannot use RPC
}
/**
- * Gets the User as a discord formatted mention.
- *
+ * Gets the User as a discord formatted mention.
+ *
* {@code <@SNOWFLAKE_ID> }
*
* @return A discord formatted mention of this User.
*/
- public String getAsMention()
- {
+ public String getAsMention() {
return "<@" + id + '>';
}
-
+
@Override
- public boolean equals(Object o)
- {
+ public boolean equals(Object o) {
if (!(o instanceof User))
return false;
User oUser = (User) o;
return this == oUser || this.id == oUser.id;
}
-
+
@Override
- public int hashCode()
- {
+ public int hashCode() {
return Long.hashCode(id);
}
@Override
- public String toString()
- {
+ public String toString() {
return "U:" + getName() + '(' + id + ')';
}
@@ -189,24 +206,22 @@ public String toString()
* Constants representing one of five different
* default avatars a {@link User} can have.
*/
- public enum DefaultAvatar
- {
+ public enum DefaultAvatar {
BLURPLE("6debd47ed13483642cf09e832ed0bc1b"),
GREY("322c936a8c8be1b803cd94861bdfa868"),
GREEN("dd4dbc0016779df1378e7812eabaa04d"),
ORANGE("0e291f67c9274a1abdddeb3fd919cbaa"),
- RED("1cbd08c76f8af6dddce02c5138971129");
+ RED("1cbd08c76f8af6dddce02c5138971129"),
+ PINK("1b3106e166c99cc64682");
private final String text;
- DefaultAvatar(String text)
- {
+ DefaultAvatar(String text) {
this.text = text;
}
@Override
- public String toString()
- {
+ public String toString() {
return text;
}
}
diff --git a/src/main/java/com/jagrosh/discordipc/entities/pipe/MacPipe.java b/src/main/java/com/jagrosh/discordipc/entities/pipe/MacPipe.java
new file mode 100644
index 0000000..91cf346
--- /dev/null
+++ b/src/main/java/com/jagrosh/discordipc/entities/pipe/MacPipe.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2017 John Grosh (john.a.grosh@gmail.com).
+ *
+ * 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
+ *
+ * http://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.
+ */
+
+package com.jagrosh.discordipc.entities.pipe;
+
+import com.jagrosh.discordipc.IPCClient;
+import com.jagrosh.discordipc.entities.Callback;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.HashMap;
+
+public class MacPipe extends UnixPipe {
+
+ MacPipe(IPCClient ipcClient, HashMap
- *
+ * Status for when the IPCClient when no attempt to connect has been made.
+ *
* All IPCClients are created starting with this status,
* and it never returns for the lifespan of the client.
*/
UNINITIALIZED,
/**
- * Status for when the Pipe is attempting to connect.
- *
+ * Status for when the Pipe is attempting to connect.
+ *
* This will become set whenever the #connect() method is called.
*/
CONNECTING,
/**
- * Status for when the Pipe is connected with Discord.
- *
+ * Status for when the Pipe is connected with Discord.
+ *
* This is only present when the connection is healthy, stable,
* and reading good data without exception.
- *
+ * Status for when the pipe status is beginning to close.
+ *
+ * The status that immediately follows is always {@link PipeStatus#CLOSED}
+ */
+ CLOSING,
+
+ /**
+ * Status for when the Pipe has received an {@link Packet.OpCode#CLOSE}.
+ *
* This signifies that the reading thread has safely and normally shut
* and the client is now inactive.
*/
@@ -62,12 +68,12 @@ public enum PipeStatus
/**
* Status for when the Pipe has unexpectedly disconnected, either because
- * of an exception, and/or due to bad data.
- *
- * When the status of an Pipe becomes this, a call to
+ * of an exception, and/or due to bad data.
+ *
+ * When the status of a Pipe becomes this, a call to
* {@link IPCListener#onDisconnect(IPCClient, Throwable)} will be made if one
- * has been provided to the IPCClient.
- *
+ * has been provided to the IPCClient.
+ *
* Note that the IPCClient will be inactive with this status, after which a
* call to {@link IPCClient#connect(DiscordBuild...)} can be made to "reconnect" the
* IPCClient.
diff --git a/src/main/java/com/jagrosh/discordipc/entities/pipe/UnixPipe.java b/src/main/java/com/jagrosh/discordipc/entities/pipe/UnixPipe.java
index 22de47e..0eed109 100644
--- a/src/main/java/com/jagrosh/discordipc/entities/pipe/UnixPipe.java
+++ b/src/main/java/com/jagrosh/discordipc/entities/pipe/UnixPipe.java
@@ -16,89 +16,161 @@
package com.jagrosh.discordipc.entities.pipe;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
import com.jagrosh.discordipc.IPCClient;
import com.jagrosh.discordipc.entities.Callback;
import com.jagrosh.discordipc.entities.Packet;
-import org.json.JSONException;
-import org.json.JSONObject;
import org.newsclub.net.unix.AFUNIXSocket;
import org.newsclub.net.unix.AFUNIXSocketAddress;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.File;
+import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
+import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
-public class UnixPipe extends Pipe
-{
-
+public class UnixPipe extends Pipe {
private static final Logger LOGGER = LoggerFactory.getLogger(UnixPipe.class);
private final AFUNIXSocket socket;
- UnixPipe(IPCClient ipcClient, HashMap
- *
+ * An exception thrown when the {@link IPCClient IPCClient}
+ * cannot find the proper application to use for RichPresence when
+ * attempting to {@link IPCClient#connect(DiscordBuild...) connect}.
+ *
* This purely and always means the IPCClient in question (specifically the client ID)
* is invalid and features using this library cannot be accessed using the instance.
*
* @author John Grosh (john.a.grosh@gmail.com)
*/
-public class NoDiscordClientException extends Exception
-{
-
+public class NoDiscordClientException extends Exception {
+ /**
+ * The serialized unique version identifier
+ */
+ private static final long serialVersionUID = 1L;
+
+ public NoDiscordClientException() {
+ super("No Valid Discord Client was found for this Instance");
+ }
}
diff --git a/src/main/java/com/jagrosh/discordipc/impl/Backoff.java b/src/main/java/com/jagrosh/discordipc/impl/Backoff.java
new file mode 100644
index 0000000..70472f4
--- /dev/null
+++ b/src/main/java/com/jagrosh/discordipc/impl/Backoff.java
@@ -0,0 +1,35 @@
+package com.jagrosh.discordipc.impl;
+
+import java.util.Random;
+
+public class Backoff {
+ private final long minAmount;
+ private final long maxAmount;
+ private final Random randGenerator;
+ private long current;
+ private int fails;
+
+ public Backoff(long min, long max) {
+ this.minAmount = min;
+ this.maxAmount = max;
+ this.current = min;
+ this.fails = 0;
+ this.randGenerator = new Random();
+ }
+
+ public void reset() {
+ fails = 0;
+ current = minAmount;
+ }
+
+ public long nextDelay() {
+ fails++;
+ double delay = current * 2.0 * rand01();
+ current = Math.min(current + (long) delay, maxAmount);
+ return current;
+ }
+
+ private double rand01() {
+ return randGenerator.nextDouble();
+ }
+}
diff --git a/src/main/java/com/jagrosh/discordipc/impl/WinRegistry.java b/src/main/java/com/jagrosh/discordipc/impl/WinRegistry.java
new file mode 100644
index 0000000..ab9667e
--- /dev/null
+++ b/src/main/java/com/jagrosh/discordipc/impl/WinRegistry.java
@@ -0,0 +1,484 @@
+/*
+ * Copyright 2017 John Grosh (john.a.grosh@gmail.com).
+ *
+ * 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
+ *
+ * http://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.
+ */
+
+package com.jagrosh.discordipc.impl;
+
+import net.lenni0451.reflect.Methods;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.prefs.Preferences;
+
+@SuppressWarnings("NegativeIntConstantInLongContext")
+public class WinRegistry {
+ public static final int HKEY_CURRENT_USER = 0x80000001;
+ public static final int HKEY_LOCAL_MACHINE = 0x80000002;
+ public static final int REG_SUCCESS = 0;
+
+ private static final int KEY_ALL_ACCESS = 0xf003f;
+ private static final int KEY_READ = 0x20019;
+ private static final Preferences userRoot = Preferences.userRoot();
+ private static final Preferences systemRoot = Preferences.systemRoot();
+ private static final Class extends Preferences> userClass = userRoot.getClass();
+ private static final Method regOpenKey;
+ private static final Method regCloseKey;
+ private static final Method regQueryValueEx;
+ private static final Method regEnumValue;
+ private static final Method regQueryInfoKey;
+ private static final Method regEnumKeyEx;
+ private static final Method regCreateKeyEx;
+ private static final Method regSetValueEx;
+ private static final Method regDeleteKey;
+ private static final Method regDeleteValue;
+
+ private static final float javaSpec;
+
+ static {
+ try {
+ javaSpec = Float.parseFloat(System.getProperty("java.specification.version"));
+
+ regOpenKey = Methods.getDeclaredMethod(userClass, "WindowsRegOpenKey",
+ (javaSpec >= 11 ? long.class : int.class), byte[].class, int.class);
+ regCloseKey = Methods.getDeclaredMethod(userClass, "WindowsRegCloseKey",
+ (javaSpec >= 11 ? long.class : int.class));
+ regQueryValueEx = Methods.getDeclaredMethod(userClass, "WindowsRegQueryValueEx",
+ (javaSpec >= 11 ? long.class : int.class), byte[].class);
+ regEnumValue = Methods.getDeclaredMethod(userClass, "WindowsRegEnumValue",
+ (javaSpec >= 11 ? long.class : int.class), int.class, int.class);
+ regQueryInfoKey = Methods.getDeclaredMethod(userClass, "WindowsRegQueryInfoKey1",
+ (javaSpec >= 11 ? long.class : int.class));
+ regEnumKeyEx = Methods.getDeclaredMethod(userClass,
+ "WindowsRegEnumKeyEx", (javaSpec >= 11 ? long.class : int.class), int.class,
+ int.class);
+ regCreateKeyEx = Methods.getDeclaredMethod(userClass,
+ "WindowsRegCreateKeyEx", (javaSpec >= 11 ? long.class : int.class),
+ byte[].class);
+ regSetValueEx = Methods.getDeclaredMethod(userClass,
+ "WindowsRegSetValueEx", (javaSpec >= 11 ? long.class : int.class),
+ byte[].class, byte[].class);
+ regDeleteValue = Methods.getDeclaredMethod(userClass,
+ "WindowsRegDeleteValue", (javaSpec >= 11 ? long.class : int.class),
+ byte[].class);
+ regDeleteKey = Methods.getDeclaredMethod(userClass,
+ "WindowsRegDeleteKey", (javaSpec >= 11 ? long.class : int.class),
+ byte[].class);
+ } catch (Exception e) {
+ throw new RuntimeException("Unable to setup registry data", e);
+ }
+ }
+
+ private WinRegistry() {
+ }
+
+ /**
+ * Read a value from key and value name
+ *
+ * @param hkey HKEY_CURRENT_USER/HKEY_LOCAL_MACHINE
+ * @param key The target key
+ * @param valueName The target value name
+ * @return the value
+ * @throws IllegalArgumentException if hkey is invalid
+ * @throws IllegalAccessException if permissions insufficient
+ * @throws InvocationTargetException if underlying method(s) throw(s) an exception
+ */
+ public static String readString(int hkey, String key, String valueName)
+ throws IllegalArgumentException, IllegalAccessException,
+ InvocationTargetException {
+ if (hkey == HKEY_LOCAL_MACHINE) {
+ return readString(systemRoot, hkey, key, valueName);
+ } else if (hkey == HKEY_CURRENT_USER) {
+ return readString(userRoot, hkey, key, valueName);
+ } else {
+ throw new IllegalArgumentException("hkey=" + hkey);
+ }
+ }
+
+ /**
+ * Read a value from key and value name
+ *
+ * @param hkey HKEY_CURRENT_USER/HKEY_LOCAL_MACHINE
+ * @param key The target key
+ * @param valueName The target value name
+ * @return the value
+ * @throws IllegalArgumentException if hkey is invalid
+ * @throws IllegalAccessException if permissions insufficient
+ * @throws InvocationTargetException if underlying method(s) throw(s) an exception
+ */
+ public static String readString(long hkey, String key, String valueName)
+ throws IllegalArgumentException, IllegalAccessException,
+ InvocationTargetException {
+ if (hkey == HKEY_LOCAL_MACHINE) {
+ return readString(systemRoot, hkey, key, valueName);
+ } else if (hkey == HKEY_CURRENT_USER) {
+ return readString(userRoot, hkey, key, valueName);
+ } else {
+ throw new IllegalArgumentException("hkey=" + hkey);
+ }
+ }
+
+ /**
+ * Read value(s) and value name(s) form given key
+ *
+ * @param hkey HKEY_CURRENT_USER/HKEY_LOCAL_MACHINE
+ * @param key The target key
+ * @return the value name(s) plus the value(s)
+ * @throws IllegalArgumentException if hkey is invalid
+ * @throws IllegalAccessException if permissions insufficient
+ * @throws InvocationTargetException if underlying method(s) throw(s) an exception
+ */
+ public static Map
* This is initially unconnected to Discord.
*
- * @param clientId The Rich Presence application's client ID, which can be found
- * here
+ * @param clientId The Rich Presence application's client ID, which can be found
+ * here
+ * @param debugMode Whether Debug Logging should be shown for this client
+ * @param verboseLogging Whether excess/deeper-rooted logging should be shown
+ * @param autoRegister Whether to register as an application with discord
+ * @param applicationId The application id to register with, usually the client id in string form
+ * @param optionalSteamId The steam id to register with, registers as a steam game if present
*/
- public IPCClient(long clientId)
- {
+ public IPCClient(long clientId, boolean debugMode, boolean verboseLogging, boolean autoRegister, String applicationId, String optionalSteamId) {
this.clientId = clientId;
+ this.debugMode = debugMode;
+ this.verboseLogging = verboseLogging;
+ this.applicationId = applicationId;
+ this.autoRegister = autoRegister;
+ this.optionalSteamId = optionalSteamId;
}
-
+
/**
- * Sets this IPCClient's {@link IPCListener} to handle received events.
+ * This is initially unconnected to Discord.
*
- * A single IPCClient can only have one of these set at any given time.
- * Setting this {@code null} will remove the currently active one.
+ * This is initially unconnected to Discord.
+ *
+ * @param clientId The Rich Presence application's client ID, which can be found
+ * here
+ * @param debugMode Whether Debug Logging should be shown for this client
+ * @param verboseLogging Whether excess/deeper-rooted logging should be shown
+ */
+ public IPCClient(long clientId, boolean debugMode, boolean verboseLogging) {
+ this(clientId, debugMode, verboseLogging, false, null);
+ }
+
+ /**
+ * Constructs a new IPCClient using the provided {@code clientId}.
+ * This is initially unconnected to Discord.
+ *
+ * @param clientId The Rich Presence application's client ID, which can be found
+ * here
+ * @param debugMode Whether Debug Logging should be shown for this client
+ * @param autoRegister Whether to register as an application with discord
+ * @param applicationId The application id to register with, usually the client id in string form
+ * @param optionalSteamId The steam id to register with, registers as a steam game if present
+ */
+ public IPCClient(long clientId, boolean debugMode, boolean autoRegister, String applicationId, String optionalSteamId) {
+ this(clientId, debugMode, false, autoRegister, applicationId, optionalSteamId);
+ }
+
+ /**
+ * Constructs a new IPCClient using the provided {@code clientId}.
+ * This is initially unconnected to Discord.
+ *
+ * @param clientId The Rich Presence application's client ID, which can be found
+ * here
+ * @param debugMode Whether Debug Logging should be shown for this client
+ * @param autoRegister Whether to register as an application with discord
+ * @param applicationId The application id to register with, usually the client id in string form
+ */
+ public IPCClient(long clientId, boolean debugMode, boolean autoRegister, String applicationId) {
+ this(clientId, debugMode, autoRegister, applicationId, null);
+ }
+
+ /**
+ * Constructs a new IPCClient using the provided {@code clientId}.
+ * This is initially unconnected to Discord.
+ *
+ * @param clientId The Rich Presence application's client ID, which can be found
+ * here
+ * @param debugMode Whether Debug Logging should be shown for this client
+ */
+ public IPCClient(long clientId, boolean debugMode) {
+ this(clientId, debugMode, false, null);
+ }
+
+ /**
+ * Constructs a new IPCClient using the provided {@code clientId}.
+ * This is initially unconnected to Discord.
+ *
+ * @param clientId The Rich Presence application's client ID, which can be found
+ * here
+ * @param autoRegister Whether to register as an application with discord
+ * @param applicationId The application id to register with, usually the client id in string form
+ * @param optionalSteamId The steam id to register with, registers as a steam game if present
+ */
+ public IPCClient(long clientId, boolean autoRegister, String applicationId, String optionalSteamId) {
+ this(clientId, false, autoRegister, applicationId, optionalSteamId);
+ }
+
+ /**
+ * Constructs a new IPCClient using the provided {@code clientId}.
+ * This is initially unconnected to Discord.
+ *
+ * @param clientId The Rich Presence application's client ID, which can be found
+ * here
+ * @param autoRegister Whether to register as an application with discord
+ * @param applicationId The application id to register with, usually the client id in string form
+ */
+ public IPCClient(long clientId, boolean autoRegister, String applicationId) {
+ this(clientId, autoRegister, applicationId, null);
+ }
+
+ /**
+ * Constructs a new IPCClient using the provided {@code clientId}.
+ * This is initially unconnected to Discord.
+ *
+ * @param clientId The Rich Presence application's client ID, which can be found
+ * here
+ */
+ public IPCClient(long clientId) {
+ this(clientId, false, null);
+ }
+
+ /**
+ * Finds the current process ID.
+ *
+ * @return The current process ID.
+ */
+ private static int getPID() {
+ String pr = ManagementFactory.getRuntimeMXBean().getName();
+ return Integer.parseInt(pr.substring(0, pr.indexOf('@')));
+ }
+
+ /**
+ * Retrieves the current logger that should be used
+ *
+ * @param instance The logger instance
+ * @return the current logger to use
+ */
+ public Logger getCurrentLogger(final Logger instance) {
+ return forcedLogger != null ? forcedLogger : instance;
+ }
+
+ /**
+ * Sets the current logger that should be used
*
+ * @param forcedLogger The logger instance to be used
+ */
+ public void setForcedLogger(Logger forcedLogger) {
+ this.forcedLogger = forcedLogger;
+ }
+
+ /**
+ * Sets this IPCClient's {@link IPCListener} to handle received events.
+ *
+ * Setting this {@code null} will remove the currently active one.
+ *
+ * If set and autoRegister is true, then this client will register as a steam game
+ *
+ * @return optionalSteamId
+ */
+ public String getOptionalSteamId() {
+ return optionalSteamId;
+ }
+
+ /**
+ * Gets whether the client will register a run command with discord
+ *
+ * @return autoRegister
+ */
+ public boolean isAutoRegister() {
+ return autoRegister;
+ }
+
+ /**
+ * Gets encoding to send packets in.
* If the provided {@link Event} is added more than once,
@@ -199,22 +507,62 @@ public void subscribe(Event sub)
* other than {@link #close() closing} the connection
* and creating a new one.
*
- * @param sub The event {@link Event} to add.
+ * @param sub The event {@link Event} to add.
* @param callback The {@link Callback} to handle success or failure
+ * @throws IllegalStateException If a connection was not made prior to invoking
+ * this method.
+ */
+ public void subscribe(Event sub, Callback callback) {
+ checkConnected(true);
+ if (!sub.isSubscribable())
+ throw new IllegalStateException("Cannot subscribe to " + sub + " event!");
+
+ if (debugMode) {
+ getCurrentLogger(LOGGER).info(String.format("[DEBUG] Subscribing to Event: %s", sub.name()));
+ }
+
+ JsonObject pipeData = new JsonObject();
+ pipeData.addProperty("cmd", "SUBSCRIBE");
+ pipeData.addProperty("evt", sub.name());
+
+ pipe.send(OpCode.FRAME, pipeData, callback);
+ }
+
+ /**
+ * Responds to a {@link Event#ACTIVITY_JOIN_REQUEST} from a requester {@link User}.
*
- * @throws IllegalStateException
- * If a connection was not made prior to invoking
- * this method.
+ * @param user The {@link User} to respond to
+ * @param approvalMode The {@link ApprovalMode} to respond to the requester with
+ * @param callback The {@link Callback} to handle success or failure
*/
- public void subscribe(Event sub, Callback callback)
- {
+ public void respondToJoinRequest(User user, ApprovalMode approvalMode, Callback callback) {
checkConnected(true);
- if(!sub.isSubscribable())
- throw new IllegalStateException("Cannot subscribe to "+sub+" event!");
- LOGGER.debug(String.format("Subscribing to Event: %s", sub.name()));
- pipe.send(OpCode.FRAME, new JSONObject()
- .put("cmd", "SUBSCRIBE")
- .put("evt", sub.name()), callback);
+
+ if (user != null) {
+ if (debugMode) {
+ getCurrentLogger(LOGGER).info(String.format("[DEBUG] Sending response to %s as %s", user.getName(), approvalMode.name()));
+ }
+
+ JsonObject pipeData = new JsonObject();
+ pipeData.addProperty("cmd", approvalMode == ApprovalMode.ACCEPT ? "SEND_ACTIVITY_JOIN_INVITE" : "CLOSE_ACTIVITY_JOIN_REQUEST");
+
+ JsonObject args = new JsonObject();
+ args.addProperty("user_id", user.getId());
+
+ pipeData.add("args", args);
+
+ pipe.send(OpCode.FRAME, pipeData, callback);
+ }
+ }
+
+ /**
+ * Responds to a {@link Event#ACTIVITY_JOIN_REQUEST} from a requester {@link User}.
+ *
+ * @param user The {@link User} to respond to
+ * @param approvalMode The {@link ApprovalMode} to respond to the requester with
+ */
+ public void respondToJoinRequest(User user, ApprovalMode approvalMode) {
+ respondToJoinRequest(user, approvalMode, null);
}
/**
@@ -222,8 +570,7 @@ public void subscribe(Event sub, Callback callback)
*
* @return The IPCClient's current {@link PipeStatus}.
*/
- public PipeStatus getStatus()
- {
+ public PipeStatus getStatus() {
if (pipe == null) return PipeStatus.UNINITIALIZED;
return pipe.getStatus();
@@ -233,95 +580,61 @@ public PipeStatus getStatus()
* Attempts to close an open connection to Discord.
* This can be reopened with another call to {@link #connect(DiscordBuild...)}.
*
- * @throws IllegalStateException
- * If a connection was not made prior to invoking
- * this method.
+ * @throws IllegalStateException If a connection was not made prior to invoking
+ * this method.
*/
@Override
- public void close()
- {
+ public void close() {
checkConnected(true);
try {
pipe.close();
} catch (IOException e) {
- LOGGER.debug("Failed to close pipe", e);
+ if (debugMode) {
+ getCurrentLogger(LOGGER).info(String.format("[DEBUG] Failed to close pipe: %s", e));
+ }
}
}
/**
- * Gets the IPCClient's {@link DiscordBuild}.
- * A full breakdown of each is available
- * here.
+ * @return The current {@link User} of this IPCClient from the target {@link DiscordBuild}, or null if not found.
*/
- public enum Event
- {
- NULL(false), // used for confirmation
- READY(false),
- ERROR(false),
- ACTIVITY_JOIN(true),
- ACTIVITY_SPECTATE(true),
- ACTIVITY_JOIN_REQUEST(true),
- /**
- * A backup key, only important if the
- * IPCClient receives an unknown event
- * type in a JSON payload.
- */
- UNKNOWN(false);
-
- private final boolean subscribable;
-
- Event(boolean subscribable)
- {
- this.subscribable = subscribable;
- }
-
- public boolean isSubscribable()
- {
- return subscribable;
- }
-
- static Event of(String str)
- {
- if(str==null)
- return NULL;
- for(Event s : Event.values())
- {
- if(s != UNKNOWN && s.name().equalsIgnoreCase(str))
- return s;
- }
- return UNKNOWN;
- }
+ public User getCurrentUser() {
+ if (pipe == null) return null;
+
+ return pipe.getCurrentUser();
}
// Private methods
-
+
/**
* Makes sure that the client is connected (or not) depending on if it should
* for the current state.
@@ -329,122 +642,185 @@ static Event of(String str)
* @param connected Whether to check in the context of the IPCClient being
* connected or not.
*/
- private void checkConnected(boolean connected)
- {
- if(connected && getStatus() != PipeStatus.CONNECTED)
+ private void checkConnected(boolean connected) {
+ if (connected && getStatus() != PipeStatus.CONNECTED)
throw new IllegalStateException(String.format("IPCClient (ID: %d) is not connected!", clientId));
- if(!connected && getStatus() == PipeStatus.CONNECTED)
+ if (!connected && getStatus() == PipeStatus.CONNECTED)
throw new IllegalStateException(String.format("IPCClient (ID: %d) is already connected!", clientId));
}
-
+
/**
* Initializes this IPCClient's {@link IPCClient#readThread readThread}
* and calls the first {@link Pipe#read()}.
*/
- private void startReading()
- {
- readThread = new Thread(() -> {
- try
- {
- Packet p;
- while((p = pipe.read()).getOp() != OpCode.CLOSE)
- {
- JSONObject json = p.getJson();
- Event event = Event.of(json.optString("evt", null));
- String nonce = json.optString("nonce", null);
- switch(event)
- {
+ private void startReading() {
+ final IPCClient localInstance = this;
+
+ readThread = new Thread(() -> IPCClient.this.readPipe(localInstance), "IPCClient-Reader");
+ readThread.setDaemon(true);
+
+ if (debugMode) {
+ getCurrentLogger(LOGGER).info("[DEBUG] Starting IPCClient reading thread!");
+ }
+ readThread.start();
+ }
+
+ /**
+ * Call the first {@link Pipe#read()} via try-catch
+ *
+ * @param instance The {@link IPCClient} instance
+ */
+ private void readPipe(final IPCClient instance) {
+ try {
+ Packet p;
+ while ((p = pipe.read()).getOp() != OpCode.CLOSE) {
+ JsonObject json = p.getJson();
+
+ if (json != null) {
+ Event event = Event.of(json.has("evt") && !json.get("evt").isJsonNull() ? json.getAsJsonPrimitive("evt").getAsString() : null);
+ String nonce = json.has("nonce") && !json.get("nonce").isJsonNull() ? json.getAsJsonPrimitive("nonce").getAsString() : null;
+
+ switch (event) {
case NULL:
- if(nonce != null && callbacks.containsKey(nonce))
+ if (nonce != null && callbacks.containsKey(nonce))
callbacks.remove(nonce).succeed(p);
break;
-
+
case ERROR:
- if(nonce != null && callbacks.containsKey(nonce))
- callbacks.remove(nonce).fail(json.getJSONObject("data").optString("message", null));
+ if (nonce != null && callbacks.containsKey(nonce))
+ callbacks.remove(nonce).fail(json.has("data") && json.getAsJsonObject("data").has("message") ? json.getAsJsonObject("data").getAsJsonObject("message").getAsString() : null);
break;
-
+
case ACTIVITY_JOIN:
- LOGGER.debug("Reading thread received a 'join' event.");
+ if (debugMode) {
+ getCurrentLogger(LOGGER).info("[DEBUG] Reading thread received a 'join' event.");
+ }
break;
-
+
case ACTIVITY_SPECTATE:
- LOGGER.debug("Reading thread received a 'spectate' event.");
+ if (debugMode) {
+ getCurrentLogger(LOGGER).info("[DEBUG] Reading thread received a 'spectate' event.");
+ }
break;
-
+
case ACTIVITY_JOIN_REQUEST:
- LOGGER.debug("Reading thread received a 'join request' event.");
+ if (debugMode) {
+ getCurrentLogger(LOGGER).info("[DEBUG] Reading thread received a 'join request' event.");
+ }
break;
-
+
case UNKNOWN:
- LOGGER.debug("Reading thread encountered an event with an unknown type: " +
- json.getString("evt"));
+ if (debugMode) {
+ getCurrentLogger(LOGGER).info("[DEBUG] Reading thread encountered an event with an unknown type: " +
+ json.getAsJsonPrimitive("evt").getAsString());
+ }
+ break;
+ default:
break;
}
- if(listener != null && json.has("cmd") && json.getString("cmd").equals("DISPATCH"))
- {
- try
- {
- JSONObject data = json.getJSONObject("data");
- switch(Event.of(json.getString("evt")))
- {
+
+ if (listener != null && json.has("cmd") && json.getAsJsonPrimitive("cmd").getAsString().equals("DISPATCH")) {
+ try {
+ JsonObject data = json.getAsJsonObject("data");
+ switch (Event.of(json.getAsJsonPrimitive("evt").getAsString())) {
case ACTIVITY_JOIN:
- listener.onActivityJoin(this, data.getString("secret"));
+ listener.onActivityJoin(instance, data.getAsJsonPrimitive("secret").getAsString());
break;
-
+
case ACTIVITY_SPECTATE:
- listener.onActivitySpectate(this, data.getString("secret"));
+ listener.onActivitySpectate(instance, data.getAsJsonPrimitive("secret").getAsString());
break;
-
+
case ACTIVITY_JOIN_REQUEST:
- JSONObject u = data.getJSONObject("user");
- User user = new User(
- u.getString("username"),
- u.getString("discriminator"),
- Long.parseLong(u.getString("id")),
- u.optString("avatar", null)
+ final JsonObject u = data.getAsJsonObject("user");
+ final User user = new User(
+ u.getAsJsonPrimitive("username").getAsString(),
+ u.has("global_name") && u.get("global_name").isJsonPrimitive() ? u.getAsJsonPrimitive("global_name").getAsString() : null,
+ u.has("discriminator") && u.get("discriminator").isJsonPrimitive() ? u.getAsJsonPrimitive("discriminator").getAsString() : "0",
+ Long.parseLong(u.getAsJsonPrimitive("id").getAsString()),
+ u.has("avatar") && u.get("avatar").isJsonPrimitive() ? u.getAsJsonPrimitive("avatar").getAsString() : null
);
- listener.onActivityJoinRequest(this, data.optString("secret", null), user);
+ listener.onActivityJoinRequest(instance, data.has("secret") ? data.getAsJsonObject("secret").getAsString() : null, user);
+ break;
+ default:
break;
}
- }
- catch(Exception e)
- {
- LOGGER.error("Exception when handling event: ", e);
+ } catch (Exception e) {
+ getCurrentLogger(LOGGER).error(String.format("Exception when handling event: %s", e));
}
}
}
- pipe.setStatus(PipeStatus.DISCONNECTED);
- if(listener != null)
- listener.onClose(this, p.getJson());
}
- catch(IOException | JSONException ex)
- {
- if(ex instanceof IOException)
- LOGGER.error("Reading thread encountered an IOException", ex);
- else
- LOGGER.error("Reading thread encountered an JSONException", ex);
+ pipe.setStatus(PipeStatus.DISCONNECTED);
+ if (listener != null)
+ listener.onClose(instance, p.getJson());
+ } catch (IOException | JsonParseException ex) {
+ getCurrentLogger(LOGGER).error(String.format("Reading thread encountered an Exception: %s", ex));
- pipe.setStatus(PipeStatus.DISCONNECTED);
- if(listener != null)
- listener.onDisconnect(this, ex);
+ pipe.setStatus(PipeStatus.DISCONNECTED);
+ if (listener != null) {
+ RECONNECT_TIME_MS.reset();
+ updateReconnectTime();
+ listener.onDisconnect(instance, ex);
}
- });
+ }
+ }
- LOGGER.debug("Starting IPCClient reading thread!");
- readThread.start();
+ /**
+ * Sets the next delay before re-attempting connection.
+ */
+ private void updateReconnectTime() {
+ nextDelay = System.currentTimeMillis() + RECONNECT_TIME_MS.nextDelay();
+ }
+
+ /**
+ * Constants representing a Response to an Ask to Join or Spectate Request
+ */
+ public enum ApprovalMode {
+ ACCEPT, DENY
}
-
- // Private static methods
-
+
/**
- * Finds the current process ID.
- *
- * @return The current process ID.
+ * Constants representing events that can be subscribed to
+ * using {@link #subscribe(Event)}.
+ *
+ * A full breakdown of each is available
+ * here.
*/
- private static int getPID()
- {
- String pr = ManagementFactory.getRuntimeMXBean().getName();
- return Integer.parseInt(pr.substring(0,pr.indexOf('@')));
+ public enum Event {
+ NULL(false), // used for confirmation
+ READY(false),
+ ERROR(false),
+ ACTIVITY_JOIN(true),
+ ACTIVITY_SPECTATE(true),
+ ACTIVITY_JOIN_REQUEST(true),
+ /**
+ * A backup key, only important if the
+ * IPCClient receives an unknown event
+ * type in a JSON payload.
+ */
+ UNKNOWN(false);
+
+ private final boolean subscribable;
+
+ Event(boolean subscribable) {
+ this.subscribable = subscribable;
+ }
+
+ static Event of(String str) {
+ if (str == null)
+ return NULL;
+ for (Event s : Event.values()) {
+ if (s != UNKNOWN && s.name().equalsIgnoreCase(str))
+ return s;
+ }
+ return UNKNOWN;
+ }
+
+ public boolean isSubscribable() {
+ return subscribable;
+ }
}
}
diff --git a/src/main/java/com/jagrosh/discordipc/IPCListener.java b/src/main/java/com/jagrosh/discordipc/IPCListener.java
index 7496faf..5b50677 100644
--- a/src/main/java/com/jagrosh/discordipc/IPCListener.java
+++ b/src/main/java/com/jagrosh/discordipc/IPCListener.java
@@ -13,28 +13,28 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package com.jagrosh.discordipc;
+import com.google.gson.JsonObject;
import com.jagrosh.discordipc.entities.Packet;
import com.jagrosh.discordipc.entities.User;
-import org.json.JSONObject;
/**
- * An implementable listener used to handle events caught by an {@link IPCClient}.
* This is only true if the Callback is constructed with the parameter-less
* constructor ({@link #Callback()}) or another constructor that leaves
* one or both parameters {@code null}.
*
- * @return {@code true} if and only if the
+ * @return {@code true} if and only if the Callback is "empty"
*/
- public boolean isEmpty()
- {
+ public boolean isEmpty() {
return success == null && failure == null;
}
/**
* Launches the success {@link Consumer}.
+ *
+ * @param packet The packet to execute after success
*/
- public void succeed(Packet packet)
- {
- if(success != null)
+ public void succeed(Packet packet) {
+ if (success != null)
success.accept(packet);
}
@@ -113,9 +93,8 @@ public void succeed(Packet packet)
*
* @param message The message to launch the failure consumer with.
*/
- public void fail(String message)
- {
- if(failure != null)
+ public void fail(String message) {
+ if (failure != null)
failure.accept(message);
}
}
diff --git a/src/main/java/com/jagrosh/discordipc/entities/DiscordBuild.java b/src/main/java/com/jagrosh/discordipc/entities/DiscordBuild.java
index 2fa3967..af28908 100644
--- a/src/main/java/com/jagrosh/discordipc/entities/DiscordBuild.java
+++ b/src/main/java/com/jagrosh/discordipc/entities/DiscordBuild.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2017 Kaidan Gustave
+ * Copyright 2017 John Grosh (john.a.grosh@gmail.com).
*
* 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://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,
@@ -13,66 +13,80 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package com.jagrosh.discordipc.entities;
+import com.jagrosh.discordipc.IPCClient;
+
/**
* Constants representing various Discord client builds,
* such as Stable, Canary, Public Test Build (PTB)
*/
-public enum DiscordBuild
-{
+public enum DiscordBuild {
/**
* Constant for the current Discord Canary release.
*/
- CANARY("//canary.discordapp.com/api"),
+ CANARY("//canary.discord.com/api"),
/**
* Constant for the current Discord Public Test Build or PTB release.
*/
- PTB("//ptb.discordapp.com/api"),
+ PTB("//ptb.discord.com/api"),
/**
* Constant for the current stable Discord release.
*/
- STABLE("//discordapp.com/api"),
+ STABLE("//discord.com/api"),
/**
- * 'Wildcard' build constant used in {@link com.jagrosh.discordipc.IPCClient#connect(DiscordBuild...)
+ * 'Wildcard' build constant used in {@link IPCClient#connect(DiscordBuild...)
* IPCClient#connect(DiscordBuild...)} to signify that the build to target is not important, and
- * that the first valid build will be used.
- * These can be handled via an implementation of {@link com.jagrosh.discordipc.IPCListener IPCListener}.
+ * A data-packet received from Discord via an {@link IPCClient IPCClient}.
+ * These can be handled via an implementation of {@link IPCListener IPCListener}.
*
* @author John Grosh (john.a.grosh@gmail.com)
*/
-public class Packet
-{
+public class Packet {
private final OpCode op;
- private final JSONObject data;
+ private final JsonObject data;
+ private final String encoding;
/**
- * Constructs a new Packet using an {@link OpCode} and {@link JSONObject}.
+ * Constructs a new Packet using an {@link OpCode} and {@link JsonObject}.
*
- * @param op The OpCode value of this new Packet.
- * @param data The JSONObject payload of this new Packet.
+ * @param op The OpCode value of this new Packet.
+ * @param data The JSONObject payload of this new Packet.
+ * @param encoding encoding to send packets as
*/
- public Packet(OpCode op, JSONObject data)
- {
+ public Packet(OpCode op, JsonObject data, String encoding) {
this.op = op;
this.data = data;
+ this.encoding = encoding;
+ }
+
+ /**
+ * Constructs a new Packet using an {@link OpCode} and {@link JsonObject}.
+ *
+ * @param op The OpCode value of this new Packet.
+ * @param data The JSONObject payload of this new Packet.
+ */
+ @Deprecated
+ public Packet(OpCode op, JsonObject data) {
+ this(op, data, "UTF-8");
}
/**
@@ -47,10 +63,17 @@ public Packet(OpCode op, JSONObject data)
*
* @return This Packet as a {@code byte} array.
*/
- public byte[] toBytes()
- {
- byte[] d = data.toString().getBytes(StandardCharsets.UTF_8);
- ByteBuffer packet = ByteBuffer.allocate(d.length + 2*Integer.BYTES);
+ public byte[] toBytes() {
+ String s = data.toString();
+
+ byte[] d;
+ try {
+ d = s.getBytes(encoding);
+ } catch (UnsupportedEncodingException e) {
+ d = s.getBytes();
+ }
+
+ ByteBuffer packet = ByteBuffer.allocate(d.length + 2 * (Integer.SIZE / Byte.SIZE));
packet.putInt(Integer.reverseBytes(op.ordinal()));
packet.putInt(Integer.reverseBytes(d.length));
packet.put(d);
@@ -62,35 +85,39 @@ public byte[] toBytes()
*
* @return This Packet's OpCode.
*/
- public OpCode getOp()
- {
+ public OpCode getOp() {
return op;
}
/**
- * Gets the {@link JSONObject} value as a part of this {@link Packet}.
+ * Gets the Raw {@link JsonObject} value as a part of this {@link Packet}.
*
* @return The JSONObject value of this Packet.
*/
- public JSONObject getJson()
- {
+ public JsonObject getJson() {
return data;
}
-
+
@Override
- public String toString()
- {
- return "Pkt:"+getOp()+getJson().toString();
+ public String toString() {
+ return "Pkt:" + getOp() + getJson().toString();
+ }
+
+ public String toDecodedString() {
+ try {
+ return "Pkt:" + getOp() + new String(getJson().toString().getBytes(encoding));
+ } catch (UnsupportedEncodingException e) {
+ return "Pkt:" + getOp() + getJson().toString();
+ }
}
/**
* Discord response OpCode values that are
* sent with response data to and from Discord
- * and the {@link com.jagrosh.discordipc.IPCClient IPCClient}
+ * and the {@link IPCClient IPCClient}
* connected.
*/
- public enum OpCode
- {
+ public enum OpCode {
HANDSHAKE, FRAME, CLOSE, PING, PONG
}
}
diff --git a/src/main/java/com/jagrosh/discordipc/entities/PartyPrivacy.java b/src/main/java/com/jagrosh/discordipc/entities/PartyPrivacy.java
new file mode 100644
index 0000000..8b7d3b0
--- /dev/null
+++ b/src/main/java/com/jagrosh/discordipc/entities/PartyPrivacy.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 John Grosh (john.a.grosh@gmail.com).
+ *
+ * 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
+ *
+ * http://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.
+ */
+
+package com.jagrosh.discordipc.entities;
+
+/**
+ * Constants representing various Discord client party privacy levels,
+ * such as Public or Private
+ */
+public enum PartyPrivacy {
+ /**
+ * Constant for the "Private" Discord RPC Party privacy level.
+ */
+ Private,
+
+ /**
+ * Constant for the "Public" Discord RPC Party privacy level.
+ */
+ Public;
+
+ /**
+ * Gets a {@link PartyPrivacy} matching the specified index.
+ *
The {@code partySize} is the current size of the player's party.
*
The {@code partyMax} is the maximum number of player's allowed in the party.
*
- * @param partyId The ID of the player's party.
- * @param partySize The current size of the player's party.
- * @param partyMax The maximum number of player's allowed in the party.
- *
+ * @param partyId The ID of the player's party.
+ * @param partySize The current size of the player's party.
+ * @param partyMax The maximum number of player's allowed in the party.
+ * @param partyPrivacy The privacy level for the player's party.
* @return This Builder.
*/
- public Builder setParty(String partyId, int partySize, int partyMax)
- {
+ public Builder setParty(String partyId, int partySize, int partyMax, PartyPrivacy partyPrivacy) {
this.partyId = partyId;
this.partySize = partySize;
this.partyMax = partyMax;
+ this.partyPrivacy = partyPrivacy;
return this;
}
@@ -280,11 +562,9 @@ public Builder setParty(String partyId, int partySize, int partyMax)
* Sets the unique hashed string for Spectate and Join.
*
* @param matchSecret The unique hashed string for Spectate and Join.
- *
* @return This Builder.
*/
- public Builder setMatchSecret(String matchSecret)
- {
+ public Builder setMatchSecret(String matchSecret) {
this.matchSecret = matchSecret;
return this;
}
@@ -293,11 +573,9 @@ public Builder setMatchSecret(String matchSecret)
* Sets the unique hashed string for chat invitations and Ask to Join.
*
* @param joinSecret The unique hashed string for chat invitations and Ask to Join.
- *
* @return This Builder.
*/
- public Builder setJoinSecret(String joinSecret)
- {
+ public Builder setJoinSecret(String joinSecret) {
this.joinSecret = joinSecret;
return this;
}
@@ -306,26 +584,34 @@ public Builder setJoinSecret(String joinSecret)
* Sets the unique hashed string for Spectate button.
*
* @param spectateSecret The unique hashed string for Spectate button.
- *
* @return This Builder.
*/
- public Builder setSpectateSecret(String spectateSecret)
- {
+ public Builder setSpectateSecret(String spectateSecret) {
this.spectateSecret = spectateSecret;
return this;
}
+ /**
+ * Sets the button array to be used within the RichPresence
+ *
* Only implemented internally.
- * @param name user's name
- * @param discriminator user's discrim
- * @param id user's id
- * @param avatar user's avatar hash, or {@code null} if they have no avatar
+ *
+ * @param username user's name
+ * @param nickname user's nickname
+ * @param discriminator user's discriminator
+ * @param id user's id
+ * @param avatar user's avatar hash, or {@code null} if they have no avatar
*/
- public User(String name, String discriminator, long id, String avatar)
- {
- this.name = name;
+ public User(String username, String nickname, String discriminator, long id, String avatar) {
+ this.username = username;
+ this.nickname = nickname;
this.discriminator = discriminator;
this.id = id;
this.avatar = avatar;
@@ -51,9 +57,27 @@ public User(String name, String discriminator, long id, String avatar)
*
* @return The Users account name.
*/
- public String getName()
- {
- return name;
+ public String getName() {
+ return username;
+ }
+
+ /**
+ * Gets the Users nickname, if any.
+ *
+ * @return The Users nickname.
+ */
+ public String getNickname() {
+ return nickname;
+ }
+
+ /**
+ * Gets the Users nickname, or their account name if they
+ * do not have a custom nickname set on their account.
+ *
+ * @return The Users effective name.
+ */
+ public String getEffectiveName() {
+ return nickname == null ? username : nickname;
}
/**
@@ -61,8 +85,7 @@ public String getName()
*
* @return The Users discriminator.
*/
- public String getDiscriminator()
- {
+ public String getDiscriminator() {
return discriminator;
}
@@ -71,8 +94,7 @@ public String getDiscriminator()
*
* @return The Users Snowflake ID as a {@code long}.
*/
- public long getIdLong()
- {
+ public long getIdLong() {
return id;
}
@@ -81,8 +103,7 @@ public long getIdLong()
*
* @return The Users Snowflake ID as a {@code String}.
*/
- public String getId()
- {
+ public String getId() {
return Long.toString(id);
}
@@ -91,8 +112,7 @@ public String getId()
*
* @return The Users avatar ID.
*/
- public String getAvatarId()
- {
+ public String getAvatarId() {
return avatar;
}
@@ -101,10 +121,9 @@ public String getAvatarId()
*
* @return The Users avatar URL.
*/
- public String getAvatarUrl()
- {
+ public String getAvatarUrl() {
return getAvatarId() == null ? null : "https://cdn.discordapp.com/avatars/" + getId() + "/" + getAvatarId()
- + (getAvatarId().startsWith("a_") ? ".gif" : ".png");
+ + (getAvatarId().startsWith("a_") ? ".gif" : ".png");
}
/**
@@ -112,9 +131,14 @@ public String getAvatarUrl()
*
* @return The Users {@link DefaultAvatar} avatar ID.
*/
- public String getDefaultAvatarId()
- {
- return DefaultAvatar.values()[Integer.parseInt(getDiscriminator()) % DefaultAvatar.values().length].toString();
+ public String getDefaultAvatarId() {
+ int index;
+ if (getDiscriminator().equals("0")) {
+ index = ((int) getIdLong() >> 22) % 6;
+ } else {
+ index = Integer.parseInt(getDiscriminator()) % 5;
+ }
+ return DefaultAvatar.values()[index].toString();
}
/**
@@ -122,9 +146,8 @@ public String getDefaultAvatarId()
*
* @return The Users {@link DefaultAvatar} avatar URL.
*/
- public String getDefaultAvatarUrl()
- {
- return "https://discordapp.com/assets/" + getDefaultAvatarId() + ".png";
+ public String getDefaultAvatarUrl() {
+ return "https://discord.com/assets/" + getDefaultAvatarId() + ".png";
}
/**
@@ -133,55 +156,49 @@ public String getDefaultAvatarUrl()
*
* @return The Users effective avatar URL.
*/
- public String getEffectiveAvatarUrl()
- {
+ public String getEffectiveAvatarUrl() {
return getAvatarUrl() == null ? getDefaultAvatarUrl() : getAvatarUrl();
}
/**
- * Gets whether or not this User is a bot.
* If the environment becomes out of line with these principles
@@ -53,8 +52,15 @@ public enum PipeStatus
CONNECTED,
/**
- * Status for when the Pipe has received an {@link Packet.OpCode#CLOSE}.