Skip to content

Commit

Permalink
added callbacks and changed reading logic
Browse files Browse the repository at this point in the history
  • Loading branch information
jagrosh committed Nov 16, 2017
1 parent 49bbbbd commit efaa756
Show file tree
Hide file tree
Showing 2 changed files with 206 additions and 49 deletions.
194 changes: 145 additions & 49 deletions src/main/java/com/jagrosh/discordipc/IPCClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package com.jagrosh.discordipc;

import com.jagrosh.discordipc.entities.Callback;
import com.jagrosh.discordipc.entities.DiscordBuild;
import com.jagrosh.discordipc.entities.Packet;
import com.jagrosh.discordipc.entities.Packet.OpCode;
Expand All @@ -26,6 +27,7 @@
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.management.ManagementFactory;
import java.util.HashMap;
import java.util.UUID;
import org.json.JSONException;
import org.json.JSONObject;
Expand Down Expand Up @@ -60,6 +62,7 @@ public final class IPCClient implements Closeable
private static final Logger LOGGER = LoggerFactory.getLogger(IPCClient.class);
private final int version = 1;
private final long clientId;
private final HashMap<String,Callback> callbacks = new HashMap<>();
private Status status = Status.CREATED;
private DiscordBuild build = null;
private IPCListener listener = null;
Expand Down Expand Up @@ -114,6 +117,7 @@ public void connect(DiscordBuild... preferredOrder) throws NoDiscordClientExcept
checkConnected(false);
if(preferredOrder.length == 0)
preferredOrder = new DiscordBuild[]{DiscordBuild.ANY};
callbacks.clear();

// store some files so we can get the preferred client
RandomAccessFile[] open = new RandomAccessFile[DiscordBuild.values().length];
Expand All @@ -123,7 +127,7 @@ public void connect(DiscordBuild... preferredOrder) throws NoDiscordClientExcept
{
pipe = new RandomAccessFile(getIPC(i), "rw");

send(OpCode.HANDSHAKE, new JSONObject().put("v",version).put("client_id", Long.toString(clientId)));
send(OpCode.HANDSHAKE, new JSONObject().put("v",version).put("client_id", Long.toString(clientId)), null);

Packet p = read(); // this is a valid client at this point

Expand Down Expand Up @@ -208,37 +212,81 @@ public void connect(DiscordBuild... preferredOrder) throws NoDiscordClientExcept
* @see RichPresence
*/
public void sendRichPresence(RichPresence presence)
{
sendRichPresence(presence, null);
}

/**
* Sends a {@link RichPresence} to the Discord client.<p>
*
* This is where the IPCClient will officially display
* a Rich Presence in the Discord client.<p>
*
* 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.toJson().toString());
send(OpCode.FRAME, new JSONObject()
.put("cmd","SET_ACTIVITY")
.put("args", new JSONObject()
.put("pid",getPID())
.put("activity",presence.toJson())));

.put("activity",presence.toJson())), callback);
}

/**
* Adds an event {@link Subscription} to this IPCClient.<br>
* If the provided {@link Subscription} is added more than once,
* Adds an event {@link Event} to this IPCClient.<br>
* 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.<br>
* 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 Subscription} 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(Subscription sub)
public void subscribe(Event sub, Callback callback)
{
checkConnected(true);
if(!sub.isSubscribable())
throw new IllegalStateException("Cannot subscribe to "+sub+" event!");
send(OpCode.FRAME, new JSONObject()
.put("cmd", "SUBSCRIBE")
.put("evt", sub.name()));
.put("evt", sub.name()), callback);
}

/**
Expand All @@ -263,7 +311,7 @@ public Status getStatus()
public void close()
{
checkConnected(true);
send(OpCode.CLOSE, new JSONObject());
send(OpCode.CLOSE, new JSONObject(), null);
}

/**
Expand Down Expand Up @@ -344,21 +392,38 @@ public enum Status
* A full breakdown of each is available
* <a href=https://discordapp.com/developers/docs/rich-presence/how-to>here</a>.
*/
public enum Subscription
public enum Event
{
ACTIVITY_JOIN,
ACTIVITY_SPECTATE,
ACTIVITY_JOIN_REQUEST,
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;
UNKNOWN(false);

private final boolean subscribable;

private Event(boolean subscribable)
{
this.subscribable = subscribable;
}

static Subscription of(String str)
public boolean isSubscribable()
{
for(Subscription s : Subscription.values())
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;
Expand Down Expand Up @@ -398,43 +463,70 @@ private void startReading()
while((p = 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();
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"))
{
JSONObject data = json.getJSONObject("data");

switch(Subscription.of(json.getString("evt")))
try
{
case ACTIVITY_JOIN:
LOGGER.debug("Reading thread received a 'join' event.");
listener.onActivityJoin(this, data.getString("secret"));
break;

case ACTIVITY_SPECTATE:
LOGGER.debug("Reading thread received a 'spectate' event.");
listener.onActivitySpectate(this, data.getString("secret"));
break;

case ACTIVITY_JOIN_REQUEST:
LOGGER.debug("Reading thread received a 'join request' event.");
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;

case UNKNOWN:
default: // I don't know why but let's just keep it safe.
LOGGER.error("Reading thread encountered an event with an unknown type: " +
json.getString("evt"));
break;
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);
}
}
}

status = Status.CLOSED;
if(listener != null)
listener.onClose(this, p.getJson());
Expand All @@ -461,12 +553,16 @@ private void startReading()
*
* @param op The {@link OpCode} to send data with.
* @param data The data to send.
* @param callback callback for the response
*/
private void send(OpCode op, JSONObject data)
private void send(OpCode op, JSONObject data, Callback callback)
{
try
{
Packet p = new Packet(op, data.put("nonce",generateNonce()));
String nonce = generateNonce();
Packet p = new Packet(op, data.put("nonce",nonce));
if(callback!=null && !callback.isEmpty())
callbacks.put(nonce, callback);
pipe.write(p.toBytes());
if(listener != null)
listener.onPacketSent(this, p);
Expand Down
61 changes: 61 additions & 0 deletions src/main/java/com/jagrosh/discordipc/entities/Callback.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2017 John Grosh ([email protected]).
*
* 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;

/**
*
* @author John Grosh ([email protected])
*/
public class Callback
{
private final Runnable success;
private final Consumer<String> failure;

public Callback(Runnable success)
{
this(success, null);
}

public Callback(Consumer<String> failure)
{
this(null, failure);
}

public Callback(Runnable success, Consumer<String> failure)
{
this.success = success;
this.failure = failure;
}

public boolean isEmpty()
{
return success == null && failure == null;
}

public void succeed()
{
if(success != null)
success.run();
}

public void fail(String message)
{
if(failure != null)
failure.accept(message);
}
}

0 comments on commit efaa756

Please sign in to comment.