Repository: jagrosh/DiscordIPC Branch: master Commit: a8d6631cc90b Files: 16 Total size: 79.5 KB Directory structure: gitextract__87p4r2a/ ├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src/ └── main/ └── java/ └── com/ └── jagrosh/ └── discordipc/ ├── IPCClient.java ├── IPCListener.java ├── entities/ │ ├── Callback.java │ ├── DiscordBuild.java │ ├── Packet.java │ ├── RichPresence.java │ ├── User.java │ └── pipe/ │ ├── Pipe.java │ ├── PipeStatus.java │ ├── UnixPipe.java │ └── WindowsPipe.java └── exceptions/ └── NoDiscordClientException.java ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Compiled class file *.class # Log file *.log # BlueJ files *.ctxt # Mobile Tools for Java (J2ME) .mtj.tmp/ # Package Files # *.jar *.war *.ear *.zip *.tar.gz *.rar # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* # Intellij Project Files /.idea/ /out/ *.iml # Testing /src/test/ /target/ ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2017 John A Grosh 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. ================================================ FILE: README.md ================================================ [version]: https://api.bintray.com/packages/jagrosh/maven/DiscordIPC/images/download.svg [download]: https://bintray.com/jagrosh/maven/DiscordIPC/_latestVersion [license]: https://img.shields.io/badge/License-Apache%202.0-lightgrey.svg [ ![version][] ][download] [ ![license][] ](https://github.com/jagrosh/DiscordIPC/tree/master/LICENSE) # DiscordIPC Connect locally to the Discord client using IPC for a subset of RPC features like Rich Presence and Activity Join/Spectate # Features - Setting Rich Presence - Listen for Join, Spectate, and Join-Request events - Detect and specify priority for client build (Stable, PTB, Canary) - 100% Java # Getting Started First you'll need to add this project as a dependency. If you're using maven: ```xml com.jagrosh DiscordIPC LATEST ``` ```xml central bintray http://jcenter.bintray.com ``` With gradle: ```groovy dependencies { compile 'com.jagrosh:DiscordIPC:LATEST' } repositories { jcenter() } ``` # Example Quick example, assuming you already have a GUI application ```java IPCClient client = new IPCClient(345229890980937739L); client.setListener(new IPCListener(){ @Override public void onReady(IPCClient client) { RichPresence.Builder builder = new RichPresence.Builder(); builder.setState("West of House") .setDetails("Frustration level: Over 9000") .setStartTimestamp(OffsetDateTime.now()) .setLargeImage("canary-large", "Discord Canary") .setSmallImage("ptb-small", "Discord PTB") .setParty("party1234", 1, 6) .setMatchSecret("xyzzy") .setJoinSecret("join") .setSpectateSecret("look"); client.sendRichPresence(builder.build()); } }); client.connect(); ``` ### Other Examples * [Monster Hunter Gathering Hall App](https://github.com/MHGatheringHall/App) - App for displaying in-game info for a non-PC game series # Official Discord-RPC Bindings The official RPC bindings can be found here: https://github.com/discordapp/discord-rpc A Java wrapper for the official bindings is available here: https://github.com/MinnDevelopment/Java-DiscordRPC ================================================ FILE: pom.xml ================================================ 4.0.0 com.jagrosh DiscordIPC 0.4 jar org.json json 20230227 org.slf4j slf4j-api 2.0.7 com.kohlschutter.junixsocket junixsocket-common 2.6.2 com.kohlschutter.junixsocket junixsocket-native-common 2.6.2 org.apache.maven.plugins maven-source-plugin attach-sources jar org.apache.maven.plugins maven-javadoc-plugin attach-javadocs jar UTF-8 1.8 1.8 DiscordIPC ================================================ FILE: src/main/java/com/jagrosh/discordipc/IPCClient.java ================================================ /* * 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; import com.jagrosh.discordipc.entities.*; import com.jagrosh.discordipc.entities.Packet.OpCode; import com.jagrosh.discordipc.entities.pipe.Pipe; import com.jagrosh.discordipc.entities.pipe.PipeStatus; import com.jagrosh.discordipc.exceptions.NoDiscordClientException; import org.json.JSONException; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Closeable; import java.io.IOException; import java.lang.management.ManagementFactory; import java.util.HashMap; /** * Represents a Discord IPC Client that can send and receive * 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 * 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.

* * 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 * an {@link IllegalStateException} to be thrown.
* This also means that the IPCClient cannot tell whether the client ID * provided is valid or not before a handshake. * * @author John Grosh (john.a.grosh@gmail.com) */ public final class IPCClient implements Closeable { private static final Logger LOGGER = LoggerFactory.getLogger(IPCClient.class); private final long clientId; private final HashMap callbacks = new HashMap<>(); private volatile Pipe pipe; private IPCListener listener = null; private Thread readThread = 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 = clientId; } /** * Sets this IPCClient's {@link IPCListener} to handle received events.

* * 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 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) { this.listener = listener; if (pipe != null) pipe.setListener(listener); } /** * Opens the connection between the IPCClient and Discord.

* * This must be called before any data is exchanged between the * 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. */ public void connect(DiscordBuild... preferredOrder) throws NoDiscordClientException { checkConnected(false); callbacks.clear(); pipe = null; pipe = Pipe.openPipe(this, clientId, callbacks, preferredOrder); LOGGER.debug("Client is now connected and ready!"); if(listener != null) listener.onReady(this); startReading(); } /** * Sends a {@link RichPresence} to the Discord client.

* * This is where the IPCClient will officially display * 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. * * @see RichPresence */ public void sendRichPresence(RichPresence presence) { sendRichPresence(presence, null); } /** * Sends a {@link RichPresence} to the Discord client.

* * This is where the IPCClient will officially display * 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. * * @see RichPresence */ 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); } /** * Adds an event {@link Event} to this IPCClient.
* If the provided {@link Event} is added more than once, * it does nothing. * Once added, there is no way to remove the subscription * other than {@link #close() closing} the connection * 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. */ public void subscribe(Event sub) { subscribe(sub, null); } /** * Adds an event {@link Event} to this IPCClient.
* If the provided {@link Event} is added more than once, * it does nothing. * Once added, there is no way to remove the subscription * other than {@link #close() closing} the connection * and creating a new one. * * @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!"); LOGGER.debug(String.format("Subscribing to Event: %s", sub.name())); pipe.send(OpCode.FRAME, new JSONObject() .put("cmd", "SUBSCRIBE") .put("evt", sub.name()), callback); } /** * Gets the IPCClient's current {@link PipeStatus}. * * @return The IPCClient's current {@link PipeStatus}. */ public PipeStatus getStatus() { if (pipe == null) return PipeStatus.UNINITIALIZED; return pipe.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. */ @Override public void close() { checkConnected(true); try { pipe.close(); } catch (IOException e) { LOGGER.debug("Failed to close pipe", e); } } /** * 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.

* * 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() { if (pipe == null) return null; return pipe.getDiscordBuild(); } /** * Constants representing events that can be subscribed to * using {@link #subscribe(Event)}.

* * Each event corresponds to a different function as a * component of the Rich Presence.
* A full breakdown of each is available * here. */ 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; } } // Private methods /** * Makes sure that the client is connected (or not) depending on if it should * for the current state. * * @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) throw new IllegalStateException(String.format("IPCClient (ID: %d) is not connected!", clientId)); 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) { case NULL: 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)); break; case ACTIVITY_JOIN: LOGGER.debug("Reading thread received a 'join' event."); break; case ACTIVITY_SPECTATE: LOGGER.debug("Reading thread received a 'spectate' event."); break; case ACTIVITY_JOIN_REQUEST: LOGGER.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")); break; } if(listener != null && json.has("cmd") && json.getString("cmd").equals("DISPATCH")) { try { JSONObject data = json.getJSONObject("data"); switch(Event.of(json.getString("evt"))) { case ACTIVITY_JOIN: listener.onActivityJoin(this, data.getString("secret")); break; case ACTIVITY_SPECTATE: listener.onActivitySpectate(this, data.getString("secret")); 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) ); listener.onActivityJoinRequest(this, data.optString("secret", null), user); break; } } catch(Exception e) { LOGGER.error("Exception when handling event: ", 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.onDisconnect(this, ex); } }); LOGGER.debug("Starting IPCClient reading thread!"); readThread.start(); } // Private static methods /** * 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('@'))); } } ================================================ FILE: src/main/java/com/jagrosh/discordipc/IPCListener.java ================================================ /* * 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; 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}.

* * Can be attached to an IPCClient using {@link IPCClient#setListener(IPCListener)}. * * @author John Grosh (john.a.grosh@gmail.com) */ 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) {} /** * Fired whenever an {@link IPCClient} receives a {@link Packet} to Discord. * * @param client The IPCClient receiving the Packet. * @param packet The Packet being received. */ default void onPacketReceived(IPCClient client, Packet packet) {} /** * Fired whenever a RichPresence activity informs us that * a user has clicked a "join" button. * * @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) {} /** * Fired whenever a RichPresence activity informs us that * a user has clicked a "spectate" button. * * @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) {} /** * Fired whenever a RichPresence activity informs us that * a user has clicked a "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. */ default 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) {} /** * Fired whenever an {@link IPCClient} has closed. * * @param client The now closed IPCClient. * @param json A {@link JSONObject} with close data. */ default 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. */ default void onDisconnect(IPCClient client, Throwable t) {} } ================================================ FILE: src/main/java/com/jagrosh/discordipc/entities/Callback.java ================================================ /* * 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; 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}. * * @author John Grosh (john.a.grosh@gmail.com) */ public class Callback { private final Consumer success; private final Consumer failure; /** * Constructs an empty Callback. */ public Callback() { this((Consumer) null, null); } /** * Constructs a Callback with a success {@link Consumer} that * occurs when the process it is attached to executes without * error. * * @param success The Consumer to launch after a successful process. */ public Callback(Consumer success) { this(success, null); } /** * Constructs a Callback with a success {@link Consumer} and * a failure {@link Consumer} that occurs when the process it is * attached to executes without or with error (respectively). * * @param success The Consumer to launch after a successful process. * @param failure The Consumer to launch if the process has an error. */ public Callback(Consumer success, Consumer failure) { this.success = success; this.failure = failure; } /** * @param success The Runnable to launch after a successful process. * @param failure The Consumer to launch if the process has an error. */ @Deprecated public Callback(Runnable success, Consumer failure) { this(p -> success.run(), failure); } /** * @param success The Runnable to launch after a successful process. */ @Deprecated public Callback(Runnable success) { this(p -> success.run(), null); } /** * Gets whether or not this Callback is "empty" which is more precisely * defined as not having a specified success {@link Consumer} and/or a * failure {@link Consumer}.
* 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 */ public boolean isEmpty() { return success == null && failure == null; } /** * Launches the success {@link Consumer}. */ public void succeed(Packet packet) { if(success != null) success.accept(packet); } /** * Launches the failure {@link Consumer} with the * provided message. * * @param message The message to launch the failure consumer with. */ public void fail(String message) { if(failure != null) failure.accept(message); } } ================================================ FILE: src/main/java/com/jagrosh/discordipc/entities/DiscordBuild.java ================================================ /* * Copyright 2017 Kaidan Gustave * * 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 builds, * such as Stable, Canary, Public Test Build (PTB) */ public enum DiscordBuild { /** * Constant for the current Discord Canary release. */ CANARY("//canary.discordapp.com/api"), /** * Constant for the current Discord Public Test Build or PTB release. */ PTB("//ptb.discordapp.com/api"), /** * Constant for the current stable Discord release. */ STABLE("//discordapp.com/api"), /** * 'Wildcard' build constant used in {@link com.jagrosh.discordipc.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.

* * Other than this exact function, there is no use for this value. */ ANY; private final String endpoint; DiscordBuild(String endpoint) { this.endpoint = endpoint; } DiscordBuild() { this(null); } /** * 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. */ public static DiscordBuild from(String endpoint) { for(DiscordBuild value : values()) { if(value.endpoint != null && value.endpoint.equals(endpoint)) { return value; } } return ANY; } } ================================================ FILE: src/main/java/com/jagrosh/discordipc/entities/Packet.java ================================================ /* * 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; 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}.
* These can be handled via an implementation of {@link com.jagrosh.discordipc.IPCListener IPCListener}. * * @author John Grosh (john.a.grosh@gmail.com) */ public class Packet { private final OpCode op; private final JSONObject data; /** * 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. */ public Packet(OpCode op, JSONObject data) { this.op = op; this.data = data; } /** * Converts this {@link Packet} to a {@code byte} array. * * @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); packet.putInt(Integer.reverseBytes(op.ordinal())); packet.putInt(Integer.reverseBytes(d.length)); packet.put(d); return packet.array(); } /** * Gets the {@link OpCode} value of this {@link Packet}. * * @return This Packet's OpCode. */ public OpCode getOp() { return op; } /** * Gets the {@link JSONObject} value as a part of this {@link Packet}. * * @return The JSONObject value of this Packet. */ public JSONObject getJson() { return data; } @Override public String toString() { 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} * connected. */ public enum OpCode { HANDSHAKE, FRAME, CLOSE, PING, PONG } } ================================================ FILE: src/main/java/com/jagrosh/discordipc/entities/RichPresence.java ================================================ /* * 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; import java.time.OffsetDateTime; import org.json.JSONArray; import org.json.JSONObject; /** * An encapsulation of all data needed to properly construct a JSON RichPresence payload. * *

These can be built using {@link RichPresence.Builder}. * * @author John Grosh (john.a.grosh@gmail.com) */ public class RichPresence { private final String state; private final String details; private final OffsetDateTime startTimestamp; private final OffsetDateTime endTimestamp; private final String largeImageKey; private final String largeImageText; private final String smallImageKey; private final String smallImageText; private final String partyId; private final int partySize; private final int partyMax; private final String matchSecret; private final String joinSecret; private final String spectateSecret; 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) { this.state = state; this.details = details; this.startTimestamp = startTimestamp; this.endTimestamp = endTimestamp; this.largeImageKey = largeImageKey; this.largeImageText = largeImageText; this.smallImageKey = smallImageKey; this.smallImageText = smallImageText; this.partyId = partyId; this.partySize = partySize; this.partyMax = partyMax; this.matchSecret = matchSecret; this.joinSecret = joinSecret; this.spectateSecret = spectateSecret; this.instance = instance; } /** * 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 * 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); } /** * A chain builder for a {@link RichPresence} object. * *

An accurate description of each field and it's functions can be found * here */ public static class Builder { private String state; private String details; private OffsetDateTime startTimestamp; private OffsetDateTime endTimestamp; private String largeImageKey; private String largeImageText; private String smallImageKey; private String smallImageText; private String partyId; private int partySize; private int partyMax; private String matchSecret; private String joinSecret; private String spectateSecret; private boolean instance; /** * Builds the {@link RichPresence} from the current state of this 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); } /** * 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) { this.state = state; 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) { this.details = details; 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) { this.startTimestamp = startTimestamp; return this; } /** * 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) { 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. * *

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 setLargeImage(String largeImageKey, String largeImageText) { this.largeImageKey = largeImageKey; this.largeImageText = largeImageText; return this; } /** * Sets the key of the uploaded image for the large profile artwork. * *

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); } /** * 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 setSmallImage(String smallImageKey, String smallImageText) { this.smallImageKey = smallImageKey; this.smallImageText = smallImageText; return this; } /** * Sets the key of the uploaded image for the small profile artwork. * *

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); } /** * Sets party configurations for a team, lobby, or other form of group. * *

The {@code partyId} is ID of the player's party. *
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. * * @return This Builder. */ public Builder setParty(String partyId, int partySize, int partyMax) { this.partyId = partyId; this.partySize = partySize; this.partyMax = partyMax; return this; } /** * 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) { this.matchSecret = matchSecret; return this; } /** * 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) { this.joinSecret = joinSecret; return this; } /** * 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) { this.spectateSecret = spectateSecret; return this; } /** * Marks the {@link #setMatchSecret(String) matchSecret} as a game * session with a specific beginning and end. * * @param instance Whether or not the {@code matchSecret} is a game * with a specific beginning and end. * * @return This Builder. */ public Builder setInstance(boolean instance) { this.instance = instance; return this; } } } ================================================ FILE: src/main/java/com/jagrosh/discordipc/entities/User.java ================================================ /* * 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; /** * 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) * onActivityJoinRequest}. * * @author John Grosh (john.a.grosh@gmail.com) */ public class User { private final String name; private final String discriminator; private final long id; private final String avatar; /** * Constructs a new {@link User}.
* 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 */ public User(String name, String discriminator, long id, String avatar) { this.name = name; this.discriminator = discriminator; this.id = id; this.avatar = avatar; } /** * Gets the Users account name. * * @return The Users account name. */ public String getName() { return name; } /** * Gets the Users discriminator. * * @return The Users discriminator. */ public String getDiscriminator() { return discriminator; } /** * Gets the Users Snowflake ID as a {@code long}. * * @return The Users Snowflake ID as a {@code long}. */ public long getIdLong() { return id; } /** * Gets the Users Snowflake ID as a {@code String}. * * @return The Users Snowflake ID as a {@code String}. */ public String getId() { return Long.toString(id); } /** * Gets the Users avatar ID. * * @return The Users avatar ID. */ public String getAvatarId() { return avatar; } /** * Gets the Users avatar URL. * * @return The Users avatar URL. */ public String getAvatarUrl() { return getAvatarId() == null ? null : "https://cdn.discordapp.com/avatars/" + getId() + "/" + getAvatarId() + (getAvatarId().startsWith("a_") ? ".gif" : ".png"); } /** * Gets the Users {@link DefaultAvatar} avatar ID. * * @return The Users {@link DefaultAvatar} avatar ID. */ public String getDefaultAvatarId() { return DefaultAvatar.values()[Integer.parseInt(getDiscriminator()) % DefaultAvatar.values().length].toString(); } /** * Gets the Users {@link DefaultAvatar} avatar URL. * * @return The Users {@link DefaultAvatar} avatar URL. */ public String getDefaultAvatarUrl() { return "https://discordapp.com/assets/" + getDefaultAvatarId() + ".png"; } /** * Gets the Users avatar URL, or their {@link DefaultAvatar} avatar URL if they * do not have a custom avatar set on their account. * * @return The Users effective avatar URL. */ public String getEffectiveAvatarUrl() { return getAvatarUrl() == null ? getDefaultAvatarUrl() : getAvatarUrl(); } /** * Gets whether or not 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() { return false; //bots cannot use RPC } /** * Gets the User as a discord formatted mention.

* * {@code <@SNOWFLAKE_ID> } * * @return A discord formatted mention of this User. */ public String getAsMention() { return "<@" + id + '>'; } @Override 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() { return Long.hashCode(id); } @Override public String toString() { return "U:" + getName() + '(' + id + ')'; } /** * Constants representing one of five different * default avatars a {@link User} can have. */ public enum DefaultAvatar { BLURPLE("6debd47ed13483642cf09e832ed0bc1b"), GREY("322c936a8c8be1b803cd94861bdfa868"), GREEN("dd4dbc0016779df1378e7812eabaa04d"), ORANGE("0e291f67c9274a1abdddeb3fd919cbaa"), RED("1cbd08c76f8af6dddce02c5138971129"); private final String text; DefaultAvatar(String text) { this.text = text; } @Override public String toString() { return text; } } } ================================================ FILE: src/main/java/com/jagrosh/discordipc/entities/pipe/Pipe.java ================================================ /* * 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.IPCListener; import com.jagrosh.discordipc.entities.Callback; import com.jagrosh.discordipc.entities.DiscordBuild; import com.jagrosh.discordipc.entities.Packet; import com.jagrosh.discordipc.exceptions.NoDiscordClientException; import org.json.JSONException; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.HashMap; import java.util.UUID; public abstract class Pipe { private static final Logger LOGGER = LoggerFactory.getLogger(Pipe.class); private static final int VERSION = 1; PipeStatus status = PipeStatus.CONNECTING; IPCListener listener; private DiscordBuild build; final IPCClient ipcClient; private final HashMap callbacks; Pipe(IPCClient ipcClient, HashMap callbacks) { this.ipcClient = ipcClient; this.callbacks = callbacks; } public static Pipe openPipe(IPCClient ipcClient, long clientId, HashMap callbacks, DiscordBuild... preferredOrder) throws NoDiscordClientException { if(preferredOrder == null || preferredOrder.length == 0) preferredOrder = new DiscordBuild[]{DiscordBuild.ANY}; Pipe pipe = null; // store some files so we can get the preferred client Pipe[] open = new Pipe[DiscordBuild.values().length]; for(int i = 0; i < 10; i++) { try { String location = getPipeLocation(i); LOGGER.debug(String.format("Searching for IPC: %s", location)); pipe = createPipe(ipcClient, callbacks, location); pipe.send(Packet.OpCode.HANDSHAKE, new JSONObject().put("v", VERSION).put("client_id", Long.toString(clientId)), null); Packet p = pipe.read(); // this is a valid client at this point pipe.build = DiscordBuild.from(p.getJson().getJSONObject("data") .getJSONObject("config") .getString("api_endpoint")); LOGGER.debug(String.format("Found a valid client (%s) with packet: %s", pipe.build.name(), p.toString())); // we're done if we found our first choice if(pipe.build == preferredOrder[0] || DiscordBuild.ANY == preferredOrder[0]) { LOGGER.info(String.format("Found preferred client: %s", pipe.build.name())); break; } open[pipe.build.ordinal()] = pipe; // didn't find first choice yet, so store what we have open[DiscordBuild.ANY.ordinal()] = pipe; // also store in 'any' for use later pipe.build = null; pipe = null; } catch(IOException | JSONException ex) { pipe = null; } } if(pipe == null) { // we already know we don't have our first pick // check each of the rest to see if we have that for(int i = 1; i < preferredOrder.length; i++) { DiscordBuild cb = preferredOrder[i]; LOGGER.debug(String.format("Looking for client build: %s", cb.name())); if(open[cb.ordinal()] != null) { pipe = open[cb.ordinal()]; open[cb.ordinal()] = null; if(cb == DiscordBuild.ANY) // if we pulled this from the 'any' slot, we need to figure out which build it was { for(int k = 0; k < open.length; k++) { if(open[k] == pipe) { pipe.build = DiscordBuild.values()[k]; open[k] = null; // we don't want to close this } } } else pipe.build = cb; LOGGER.info(String.format("Found preferred client: %s", pipe.build.name())); break; } } if(pipe == null) { throw new NoDiscordClientException(); } } // close unused files, except skip 'any' because its always a duplicate for(int i = 0; i < open.length; i++) { if(i == DiscordBuild.ANY.ordinal()) continue; if(open[i] != null) { try { open[i].close(); } catch(IOException ex) { // This isn't really important to applications and better // as debug info LOGGER.debug("Failed to close an open IPC pipe!", ex); } } } pipe.status = PipeStatus.CONNECTED; return pipe; } private static Pipe createPipe(IPCClient ipcClient, HashMap callbacks, String location) { String osName = System.getProperty("os.name").toLowerCase(); if (osName.contains("win")) { return new WindowsPipe(ipcClient, callbacks, location); } else if (osName.contains("linux") || osName.contains("mac")) { try { return new UnixPipe(ipcClient, callbacks, location); } catch (IOException e) { throw new RuntimeException(e); } } else { throw new RuntimeException("Unsupported OS: " + osName); } } /** * Sends json with the given {@link Packet.OpCode}. * * @param op The {@link Packet.OpCode} to send data with. * @param data The data to send. * @param callback callback for the response */ public void send(Packet.OpCode op, JSONObject data, Callback callback) { try { String nonce = generateNonce(); Packet p = new Packet(op, data.put("nonce",nonce)); if(callback!=null && !callback.isEmpty()) callbacks.put(nonce, callback); write(p.toBytes()); LOGGER.debug(String.format("Sent packet: %s", p.toString())); if(listener != null) listener.onPacketSent(ipcClient, p); } catch(IOException ex) { LOGGER.error("Encountered an IOException while sending a packet and disconnected!"); status = PipeStatus.DISCONNECTED; } } /** * Blocks until reading a {@link Packet} or until the * read thread encounters bad data. * * @return A valid {@link Packet}. * * @throws IOException * If the pipe breaks. * @throws JSONException * If the read thread receives bad data. */ public abstract Packet read() throws IOException, JSONException; public abstract void write(byte[] b) throws IOException; /** * Generates a nonce. * * @return A random {@link UUID}. */ private static String generateNonce() { return UUID.randomUUID().toString(); } public PipeStatus getStatus() { return status; } public void setStatus(PipeStatus status) { this.status = status; } public void setListener(IPCListener listener) { this.listener = listener; } public abstract void close() throws IOException; public DiscordBuild getDiscordBuild() { return build; } // a list of system property keys to get IPC file from different unix systems. private final static String[] unixPaths = {"XDG_RUNTIME_DIR","TMPDIR","TMP","TEMP"}; /** * Finds the IPC location in the current system. * * @param i Index to try getting the IPC at. * * @return The IPC location. */ private static String getPipeLocation(int i) { if(System.getProperty("os.name").contains("Win")) return "\\\\?\\pipe\\discord-ipc-"+i; String tmppath = null; for(String str : unixPaths) { tmppath = System.getenv(str); if(tmppath != null) break; } if(tmppath == null) tmppath = "/tmp"; return tmppath+"/discord-ipc-"+i; } } ================================================ FILE: src/main/java/com/jagrosh/discordipc/entities/pipe/PipeStatus.java ================================================ /* * 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.IPCListener; import com.jagrosh.discordipc.entities.DiscordBuild; import com.jagrosh.discordipc.entities.Packet; /** * Constants representing various status that an {@link IPCClient} can have. */ public enum PipeStatus { /** * 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.

* * This will become set whenever the #connect() method is called. */ CONNECTING, /** * 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.
* If the environment becomes out of line with these principles * in any way, the IPCClient in question will become * {@link PipeStatus#DISCONNECTED}. */ CONNECTED, /** * 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. */ CLOSED, /** * 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 * {@link IPCListener#onDisconnect(IPCClient, Throwable)} will be made if one * 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. */ DISCONNECTED } ================================================ FILE: src/main/java/com/jagrosh/discordipc/entities/pipe/UnixPipe.java ================================================ /* * 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 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.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.file.Paths; import java.util.HashMap; public class UnixPipe extends Pipe { private static final Logger LOGGER = LoggerFactory.getLogger(UnixPipe.class); private final AFUNIXSocket socket; UnixPipe(IPCClient ipcClient, HashMap callbacks, String location) throws IOException { super(ipcClient, callbacks); socket = AFUNIXSocket.newInstance(); socket.connect(AFUNIXSocketAddress.of(Paths.get(location))); } @SuppressWarnings("ResultOfMethodCallIgnored") @Override public Packet read() throws IOException, JSONException { InputStream is = socket.getInputStream(); while(is.available() == 0 && status == PipeStatus.CONNECTED) { try { Thread.sleep(50); } catch(InterruptedException ignored) {} } /*byte[] buf = new byte[is.available()]; is.read(buf, 0, buf.length); LOGGER.info(new String(buf)); if (true) return null;*/ if(status==PipeStatus.DISCONNECTED) throw new IOException("Disconnected!"); if(status==PipeStatus.CLOSED) return new Packet(Packet.OpCode.CLOSE, null); // Read the op and length. Both are signed ints byte[] d = new byte[8]; is.read(d); ByteBuffer bb = ByteBuffer.wrap(d); Packet.OpCode op = Packet.OpCode.values()[Integer.reverseBytes(bb.getInt())]; d = new byte[Integer.reverseBytes(bb.getInt())]; is.read(d); Packet p = new Packet(op, new JSONObject(new String(d))); LOGGER.debug(String.format("Received packet: %s", p.toString())); if(listener != null) listener.onPacketReceived(ipcClient, p); return p; } @Override public void write(byte[] b) throws IOException { socket.getOutputStream().write(b); } @Override public void close() throws IOException { LOGGER.debug("Closing IPC pipe..."); send(Packet.OpCode.CLOSE, new JSONObject(), null); status = PipeStatus.CLOSED; socket.close(); } } ================================================ FILE: src/main/java/com/jagrosh/discordipc/entities/pipe/WindowsPipe.java ================================================ /* * 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 com.jagrosh.discordipc.entities.Packet; import org.json.JSONException; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.util.HashMap; public class WindowsPipe extends Pipe { private static final Logger LOGGER = LoggerFactory.getLogger(WindowsPipe.class); private final RandomAccessFile file; WindowsPipe(IPCClient ipcClient, HashMap callbacks, String location) { super(ipcClient, callbacks); try { this.file = new RandomAccessFile(location, "rw"); } catch (FileNotFoundException e) { throw new RuntimeException(e); } } @Override public void write(byte[] b) throws IOException { file.write(b); } @Override public Packet read() throws IOException, JSONException { while(file.length() == 0 && status == PipeStatus.CONNECTED) { try { Thread.sleep(50); } catch(InterruptedException ignored) {} } if(status==PipeStatus.DISCONNECTED) throw new IOException("Disconnected!"); if(status==PipeStatus.CLOSED) return new Packet(Packet.OpCode.CLOSE, null); Packet.OpCode op = Packet.OpCode.values()[Integer.reverseBytes(file.readInt())]; int len = Integer.reverseBytes(file.readInt()); byte[] d = new byte[len]; file.readFully(d); Packet p = new Packet(op, new JSONObject(new String(d))); LOGGER.debug(String.format("Received packet: %s", p.toString())); if(listener != null) listener.onPacketReceived(ipcClient, p); return p; } @Override public void close() throws IOException { LOGGER.debug("Closing IPC pipe..."); send(Packet.OpCode.CLOSE, new JSONObject(), null); status = PipeStatus.CLOSED; file.close(); } } ================================================ FILE: src/main/java/com/jagrosh/discordipc/exceptions/NoDiscordClientException.java ================================================ /* * 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.exceptions; import com.jagrosh.discordipc.entities.DiscordBuild; /** * An exception thrown when an {@link com.jagrosh.discordipc.IPCClient IPCClient} * when the client cannot find the proper application to use for RichPresence when * attempting to {@link com.jagrosh.discordipc.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 { }