diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordPlugin.java index 59ac2837d6..1063ca7cef 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordPlugin.java @@ -25,13 +25,19 @@ */ package net.runelite.client.plugins.discord; +import com.google.common.base.Strings; import com.google.inject.Inject; import com.google.inject.Provides; import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; import java.time.temporal.ChronoUnit; import java.util.EnumSet; import java.util.HashMap; import java.util.Map; +import java.util.UUID; +import javax.imageio.ImageIO; +import lombok.extern.slf4j.Slf4j; import net.runelite.api.Client; import net.runelite.api.GameState; import net.runelite.api.Skill; @@ -43,7 +49,12 @@ import net.runelite.api.events.GameStateChanged; import net.runelite.api.events.VarbitChanged; import net.runelite.client.RuneLiteProperties; import net.runelite.client.config.ConfigManager; +import net.runelite.client.discord.DiscordService; +import net.runelite.client.discord.events.DiscordJoinGame; +import net.runelite.client.discord.events.DiscordJoinRequest; +import net.runelite.client.discord.events.DiscordReady; import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.PartyChanged; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; import net.runelite.client.task.Schedule; @@ -51,12 +62,24 @@ import net.runelite.client.ui.ClientToolbar; import net.runelite.client.ui.NavigationButton; import net.runelite.client.util.ImageUtil; import net.runelite.client.util.LinkBrowser; +import net.runelite.client.ws.PartyMember; +import net.runelite.client.ws.PartyService; +import net.runelite.client.ws.WSClient; +import net.runelite.http.api.RuneLiteAPI; +import net.runelite.http.api.ws.messages.party.UserJoin; +import net.runelite.http.api.ws.messages.party.UserPart; +import net.runelite.http.api.ws.messages.party.UserSync; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.Request; +import okhttp3.Response; @PluginDescriptor( name = "Discord", description = "Show your status and activity in the Discord user panel", tags = {"action", "activity", "external", "integration", "status"} ) +@Slf4j public class DiscordPlugin extends Plugin { @Inject @@ -74,6 +97,15 @@ public class DiscordPlugin extends Plugin @Inject private DiscordState discordState; + @Inject + private PartyService partyService; + + @Inject + private DiscordService discordService; + + @Inject + private WSClient wsClient; + private Map skillExp = new HashMap<>(); private NavigationButton discordButton; private boolean loginFlag; @@ -98,6 +130,13 @@ public class DiscordPlugin extends Plugin clientToolbar.addNavigation(discordButton); checkForGameStateUpdate(); + + if (discordService.getCurrentUser() != null) + { + partyService.setUsername(discordService.getCurrentUser().username + "#" + discordService.getCurrentUser().discriminator); + } + + wsClient.registerMessage(DiscordUserInfo.class); } @Override @@ -105,6 +144,8 @@ public class DiscordPlugin extends Plugin { clientToolbar.removeNavigation(discordButton); discordState.reset(); + partyService.changeParty(null); + wsClient.unregisterMessage(DiscordUserInfo.class); } @Subscribe @@ -176,6 +217,130 @@ public class DiscordPlugin extends Plugin } } + @Subscribe + public void onDiscordReady(DiscordReady event) + { + partyService.setUsername(event.getUsername() + "#" + event.getDiscriminator()); + } + + @Subscribe + public void onDiscordJoinRequest(DiscordJoinRequest request) + { + log.debug("Got discord join request {}", request); + if (partyService.isOwner() && partyService.getMembers().isEmpty()) + { + // First join, join also yourself + partyService.changeParty(partyService.getLocalPartyId()); + updatePresence(); + } + } + + @Subscribe + public void onDiscordJoinGame(DiscordJoinGame joinGame) + { + log.debug("Got discord join game {}", joinGame); + UUID partyId = UUID.fromString(joinGame.getJoinSecret()); + partyService.changeParty(partyId); + updatePresence(); + } + + @Subscribe + public void onDiscordUserInfo(final DiscordUserInfo event) + { + final PartyMember memberById = partyService.getMemberById(event.getMemberId()); + + if (memberById == null || memberById.getAvatar() != null) + { + return; + } + + String url = "https://cdn.discordapp.com/avatars/" + event.getUserId() + "/" + event.getAvatarId() + ".png"; + + if (Strings.isNullOrEmpty(event.getAvatarId())) + { + final String[] split = memberById.getName().split("#", 2); + + if (split.length == 2) + { + int disc = Integer.valueOf(split[1]); + int avatarId = disc % 5; + url = "https://cdn.discordapp.com/embed/avatars/" + avatarId + ".png"; + } + } + + log.debug("Got user avatar {}", url); + + final Request request = new Request.Builder() + .url(url) + .build(); + + RuneLiteAPI.CLIENT.newCall(request).enqueue(new Callback() + { + @Override + public void onFailure(Call call, IOException e) + { + + } + + @Override + public void onResponse(Call call, Response response) throws IOException + { + try + { + if (!response.isSuccessful()) + { + throw new IOException("Unexpected code " + response); + } + + final InputStream inputStream = response.body().byteStream(); + final BufferedImage image = ImageIO.read(inputStream); + memberById.setAvatar(image); + } + finally + { + response.close(); + } + } + }); + } + + @Subscribe + public void onUserJoin(final UserJoin event) + { + updatePresence(); + } + + @Subscribe + public void onUserSync(final UserSync event) + { + final PartyMember localMember = partyService.getLocalMember(); + + if (localMember != null) + { + if (discordService.getCurrentUser() != null) + { + final DiscordUserInfo userInfo = new DiscordUserInfo( + discordService.getCurrentUser().userId, + discordService.getCurrentUser().avatar); + + userInfo.setMemberId(localMember.getMemberId()); + wsClient.send(userInfo); + } + } + } + + @Subscribe + public void onUserPart(final UserPart event) + { + updatePresence(); + } + + @Subscribe + public void onPartyChanged(final PartyChanged event) + { + updatePresence(); + } + @Schedule( period = 1, unit = ChronoUnit.MINUTES @@ -185,6 +350,13 @@ public class DiscordPlugin extends Plugin discordState.checkForTimeout(); } + private void updatePresence() + { + discordState.triggerEvent(client.getGameState() == GameState.LOGGED_IN + ? DiscordGameEventType.IN_GAME + : DiscordGameEventType.IN_MENU); + } + private void checkForGameStateUpdate() { // Game state update does also full reset of discord state diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordState.java b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordState.java index 33dab59b1d..b14e04ad99 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordState.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordState.java @@ -31,10 +31,13 @@ import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.UUID; import javax.inject.Inject; import lombok.Data; import net.runelite.client.discord.DiscordPresence; import net.runelite.client.discord.DiscordService; +import net.runelite.client.ws.PartyService; +import static net.runelite.client.ws.PartyService.PARTY_MAX; /** * This class contains data about currently active discord state. @@ -49,16 +52,19 @@ class DiscordState private Instant updated; } + private final UUID partyId = UUID.randomUUID(); private final List events = new ArrayList<>(); private final DiscordService discordService; private final DiscordConfig config; + private PartyService party; private DiscordPresence lastPresence; @Inject - private DiscordState(final DiscordService discordService, final DiscordConfig config) + private DiscordState(final DiscordService discordService, final DiscordConfig config, final PartyService party) { this.discordService = discordService; this.config = config; + this.party = party; } /** @@ -135,12 +141,21 @@ class DiscordState } } - final DiscordPresence presence = DiscordPresence.builder() + final DiscordPresence.DiscordPresenceBuilder presenceBuilder = DiscordPresence.builder() .state(MoreObjects.firstNonNull(state, "")) .details(MoreObjects.firstNonNull(details, "")) .startTimestamp(event.getStart()) .smallImageKey(MoreObjects.firstNonNull(imageKey, "default")) - .build(); + .partyMax(PARTY_MAX) + .partySize(party.getMembers().size()); + + if (party.isOwner()) + { + presenceBuilder.partyId(partyId.toString()); + presenceBuilder.joinSecret(party.getPartyId().toString()); + } + + final DiscordPresence presence = presenceBuilder.build(); // This is to reduce amount of RPC calls if (!presence.equals(lastPresence)) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordUserInfo.java b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordUserInfo.java new file mode 100644 index 0000000000..b358a7d01e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordUserInfo.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2019, Tomas Slusny + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.discord; + +import lombok.Value; +import net.runelite.http.api.ws.messages.party.PartyMemberMessage; + +@Value +class DiscordUserInfo extends PartyMemberMessage +{ + private final String userId; + private final String avatarId; +}