From 41ae79437ee63d7a726867fe1662c8ef0de37c08 Mon Sep 17 00:00:00 2001 From: Adam Date: Tue, 22 Jan 2019 19:13:32 -0500 Subject: [PATCH] Add task chat command Co-authored-by: Spedwards --- .../runelite/http/api/chat/ChatClient.java | 54 ++++++++ .../java/net/runelite/http/api/chat/Task.java | 36 +++++ .../http/service/chat/ChatController.java | 33 +++++ .../http/service/chat/ChatService.java | 43 ++++++ .../client/plugins/slayer/SlayerConfig.java | 11 ++ .../client/plugins/slayer/SlayerPlugin.java | 128 ++++++++++++++++++ .../plugins/slayer/SlayerPluginTest.java | 72 +++++++++- 7 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 http-api/src/main/java/net/runelite/http/api/chat/Task.java diff --git a/http-api/src/main/java/net/runelite/http/api/chat/ChatClient.java b/http-api/src/main/java/net/runelite/http/api/chat/ChatClient.java index d9e75ba1be..8ec5b2a351 100644 --- a/http-api/src/main/java/net/runelite/http/api/chat/ChatClient.java +++ b/http-api/src/main/java/net/runelite/http/api/chat/ChatClient.java @@ -24,7 +24,10 @@ */ package net.runelite.http.api.chat; +import com.google.gson.JsonParseException; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import net.runelite.http.api.RuneLiteAPI; import okhttp3.HttpUrl; import okhttp3.Request; @@ -118,4 +121,55 @@ public class ChatClient return Integer.parseInt(response.body().string()); } } + + public boolean submitTask(String username, String task, int amount, int initialAmount, String location) throws IOException + { + HttpUrl url = RuneLiteAPI.getApiBase().newBuilder() + .addPathSegment("chat") + .addPathSegment("task") + .addQueryParameter("name", username) + .addQueryParameter("task", task) + .addQueryParameter("amount", Integer.toString(amount)) + .addQueryParameter("initialAmount", Integer.toString(initialAmount)) + .addQueryParameter("location", location) + .build(); + + Request request = new Request.Builder() + .post(RequestBody.create(null, new byte[0])) + .url(url) + .build(); + + try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute()) + { + return response.isSuccessful(); + } + } + + public Task getTask(String username) throws IOException + { + HttpUrl url = RuneLiteAPI.getApiBase().newBuilder() + .addPathSegment("chat") + .addPathSegment("task") + .addQueryParameter("name", username) + .build(); + + Request request = new Request.Builder() + .url(url) + .build(); + + try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute()) + { + if (!response.isSuccessful()) + { + throw new IOException("Unable to look up task!"); + } + + InputStream in = response.body().byteStream(); + return RuneLiteAPI.GSON.fromJson(new InputStreamReader(in), Task.class); + } + catch (JsonParseException ex) + { + throw new IOException(ex); + } + } } diff --git a/http-api/src/main/java/net/runelite/http/api/chat/Task.java b/http-api/src/main/java/net/runelite/http/api/chat/Task.java new file mode 100644 index 0000000000..db38c90109 --- /dev/null +++ b/http-api/src/main/java/net/runelite/http/api/chat/Task.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2019, Adam + * 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.http.api.chat; + +import lombok.Data; + +@Data +public class Task +{ + private String task; + private int amount; + private int initialAmount; + private String location; +} diff --git a/http-service/src/main/java/net/runelite/http/service/chat/ChatController.java b/http-service/src/main/java/net/runelite/http/service/chat/ChatController.java index 9060737974..0a95b71336 100644 --- a/http-service/src/main/java/net/runelite/http/service/chat/ChatController.java +++ b/http-service/src/main/java/net/runelite/http/service/chat/ChatController.java @@ -27,6 +27,9 @@ package net.runelite.http.service.chat; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import net.runelite.http.api.chat.Task; import net.runelite.http.service.util.exception.NotFoundException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; @@ -39,6 +42,9 @@ import org.springframework.web.bind.annotation.RestController; @RequestMapping("/chat") public class ChatController { + private static final Pattern STRING_VALIDATION = Pattern.compile("[^a-zA-Z0-9' -]"); + private static final int STRING_MAX_LENGTH = 50; + private final Cache killCountCache = CacheBuilder.newBuilder() .expireAfterWrite(2, TimeUnit.MINUTES) .maximumSize(128L) @@ -100,4 +106,31 @@ public class ChatController } return kc; } + + @PostMapping("/task") + public void submitTask(@RequestParam String name, @RequestParam("task") String taskName, @RequestParam int amount, + @RequestParam int initialAmount, @RequestParam String location) + { + Matcher mTask = STRING_VALIDATION.matcher(taskName); + Matcher mLocation = STRING_VALIDATION.matcher(location); + if (mTask.find() || taskName.length() > STRING_MAX_LENGTH || + mLocation.find() || location.length() > STRING_MAX_LENGTH) + { + return; + } + + Task task = new Task(); + task.setTask(taskName); + task.setAmount(amount); + task.setInitialAmount(initialAmount); + task.setLocation(location); + + chatService.setTask(name, task); + } + + @GetMapping("/task") + public Task getTask(@RequestParam String name) + { + return chatService.getTask(name); + } } diff --git a/http-service/src/main/java/net/runelite/http/service/chat/ChatService.java b/http-service/src/main/java/net/runelite/http/service/chat/ChatService.java index 71b3d9a2c3..24b16086f8 100644 --- a/http-service/src/main/java/net/runelite/http/service/chat/ChatService.java +++ b/http-service/src/main/java/net/runelite/http/service/chat/ChatService.java @@ -24,7 +24,10 @@ */ package net.runelite.http.service.chat; +import com.google.common.collect.ImmutableMap; import java.time.Duration; +import java.util.Map; +import net.runelite.http.api.chat.Task; import net.runelite.http.service.util.redis.RedisPool; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -78,4 +81,44 @@ public class ChatService jedis.setex("qp." + name, (int) EXPIRE.getSeconds(), Integer.toString(qp)); } } + + public Task getTask(String name) + { + Map map; + + try (Jedis jedis = jedisPool.getResource()) + { + map = jedis.hgetAll("task." + name); + } + + if (map.isEmpty()) + { + return null; + } + + Task task = new Task(); + task.setTask(map.get("task")); + task.setAmount(Integer.parseInt(map.get("amount"))); + task.setInitialAmount(Integer.parseInt(map.get("initialAmount"))); + task.setLocation(map.get("location")); + return task; + } + + public void setTask(String name, Task task) + { + Map taskMap = ImmutableMap.builderWithExpectedSize(4) + .put("task", task.getTask()) + .put("amount", Integer.toString(task.getAmount())) + .put("initialAmount", Integer.toString(task.getInitialAmount())) + .put("location", task.getLocation()) + .build(); + + String key = "task." + name; + + try (Jedis jedis = jedisPool.getResource()) + { + jedis.hmset(key, taskMap); + jedis.expire(key, (int) EXPIRE.getSeconds()); + } + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerConfig.java index 8550d620b6..47aa58a1ab 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerConfig.java @@ -110,6 +110,17 @@ public interface SlayerConfig extends Config return true; } + @ConfigItem( + position = 8, + keyName = "taskCommand", + name = "Task Command", + description = "Configures whether the slayer task command is enabled
!task" + ) + default boolean taskCommand() + { + return true; + } + // Stored data @ConfigItem( keyName = "taskName", diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerPlugin.java index 5e76829955..8458ef4fcb 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerPlugin.java @@ -29,11 +29,13 @@ import com.google.common.annotations.VisibleForTesting; import com.google.inject.Provides; import java.awt.Color; import java.awt.image.BufferedImage; +import java.io.IOException; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.ScheduledExecutorService; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.inject.Inject; @@ -46,6 +48,7 @@ import net.runelite.api.ChatMessageType; import net.runelite.api.Client; import net.runelite.api.GameState; import net.runelite.api.ItemID; +import net.runelite.api.MessageNode; import net.runelite.api.NPC; import net.runelite.api.NPCComposition; import static net.runelite.api.Skill.SLAYER; @@ -57,13 +60,19 @@ import net.runelite.api.events.GameStateChanged; import net.runelite.api.events.GameTick; import net.runelite.api.events.NpcDespawned; import net.runelite.api.events.NpcSpawned; +import net.runelite.api.events.SetMessage; import net.runelite.api.vars.SlayerUnlock; import net.runelite.api.widgets.Widget; import net.runelite.api.widgets.WidgetInfo; import net.runelite.client.Notifier; import net.runelite.client.callback.ClientThread; +import net.runelite.client.chat.ChatColorType; +import net.runelite.client.chat.ChatCommandManager; +import net.runelite.client.chat.ChatMessageBuilder; +import net.runelite.client.chat.ChatMessageManager; import net.runelite.client.config.ConfigManager; import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.ChatInput; import net.runelite.client.game.ItemManager; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; @@ -71,6 +80,7 @@ import net.runelite.client.ui.overlay.OverlayManager; import net.runelite.client.ui.overlay.infobox.InfoBoxManager; import net.runelite.client.util.ColorUtil; import net.runelite.client.util.Text; +import net.runelite.http.api.chat.ChatClient; @PluginDescriptor( name = "Slayer", @@ -111,6 +121,11 @@ public class SlayerPlugin extends Plugin private static final int EXPEDITIOUS_CHARGE = 30; private static final int SLAUGHTER_CHARGE = 30; + // Chat Command + private static final String TASK_COMMAND_STRING = "!task"; + private static final Pattern TASK_STRING_VALIDATION = Pattern.compile("[^a-zA-Z0-9' -]"); + private static final int TASK_STRING_MAX_LENGTH = 50; + @Inject private Client client; @@ -144,6 +159,18 @@ public class SlayerPlugin extends Plugin @Inject private TargetMinimapOverlay targetMinimapOverlay; + @Inject + private ChatMessageManager chatMessageManager; + + @Inject + private ChatCommandManager chatCommandManager; + + @Inject + private ScheduledExecutorService executor; + + @Inject + private ChatClient chatClient; + @Getter(AccessLevel.PACKAGE) private List highlightedTargets = new ArrayList<>(); @@ -201,6 +228,8 @@ public class SlayerPlugin extends Plugin setSlaughterChargeCount(config.slaughter()); clientThread.invoke(() -> setTask(config.taskName(), config.amount(), config.initialAmount(), config.taskLocation())); } + + chatCommandManager.registerCommandAsync(TASK_COMMAND_STRING, this::taskLookup, this::taskSubmit); } @Override @@ -212,6 +241,8 @@ public class SlayerPlugin extends Plugin overlayManager.remove(targetMinimapOverlay); removeCounter(); highlightedTargets.clear(); + + chatCommandManager.unregisterCommand(TASK_COMMAND_STRING); } @Provides @@ -685,6 +716,103 @@ public class SlayerPlugin extends Plugin counter = null; } + void taskLookup(SetMessage setMessage, String message) + { + if (!config.taskCommand()) + { + return; + } + + ChatMessageType type = setMessage.getType(); + + final String player; + if (type.equals(ChatMessageType.PRIVATE_MESSAGE_SENT)) + { + player = client.getLocalPlayer().getName(); + } + else + { + player = Text.removeTags(setMessage.getName()) + .replace('\u00A0', ' '); + } + + net.runelite.http.api.chat.Task task; + try + { + task = chatClient.getTask(player); + } + catch (IOException ex) + { + log.debug("unable to lookup slayer task", ex); + return; + } + + if (TASK_STRING_VALIDATION.matcher(task.getTask()).find() || task.getTask().length() > TASK_STRING_MAX_LENGTH || + TASK_STRING_VALIDATION.matcher(task.getLocation()).find() || task.getLocation().length() > TASK_STRING_MAX_LENGTH) + { + log.debug("Validation failed for task name or location: {}", task); + return; + } + + int killed = task.getInitialAmount() - task.getAmount(); + + StringBuilder sb = new StringBuilder(); + sb.append(task.getTask()); + if (!Strings.isNullOrEmpty(task.getLocation())) + { + sb.append(" (").append(task.getLocation()).append(")"); + } + sb.append(": "); + if (killed < 0) + { + sb.append(task.getAmount()).append(" left"); + } + else + { + sb.append(killed).append('/').append(task.getInitialAmount()).append(" killed"); + } + + String response = new ChatMessageBuilder() + .append(ChatColorType.NORMAL) + .append("Slayer Task: ") + .append(ChatColorType.HIGHLIGHT) + .append(sb.toString()) + .build(); + + final MessageNode messageNode = setMessage.getMessageNode(); + messageNode.setRuneLiteFormatMessage(response); + chatMessageManager.update(messageNode); + client.refreshChat(); + } + + private boolean taskSubmit(ChatInput chatInput, String value) + { + if (Strings.isNullOrEmpty(taskName)) + { + return false; + } + + final String playerName = client.getLocalPlayer().getName(); + + executor.execute(() -> + { + try + { + chatClient.submitTask(playerName, capsString(taskName), amount, initialAmount, taskLocation); + } + catch (Exception ex) + { + log.warn("unable to submit slayer task", ex); + } + finally + { + chatInput.resume(); + } + }); + + return true; + } + //Utils private String capsString(String str) { diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/slayer/SlayerPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/slayer/SlayerPluginTest.java index be48ecb4a3..c0a16d3df0 100644 --- a/runelite-client/src/test/java/net/runelite/client/plugins/slayer/SlayerPluginTest.java +++ b/runelite-client/src/test/java/net/runelite/client/plugins/slayer/SlayerPluginTest.java @@ -27,31 +27,41 @@ package net.runelite.client.plugins.slayer; import com.google.inject.Guice; import com.google.inject.testing.fieldbinder.Bind; import com.google.inject.testing.fieldbinder.BoundFieldModule; +import java.io.IOException; +import java.util.concurrent.ScheduledExecutorService; import javax.inject.Inject; +import net.runelite.api.ChatMessageType; import static net.runelite.api.ChatMessageType.SERVER; import net.runelite.api.Client; +import net.runelite.api.MessageNode; import net.runelite.api.Player; import net.runelite.api.coords.LocalPoint; import net.runelite.api.events.ChatMessage; import net.runelite.api.events.GameTick; +import net.runelite.api.events.SetMessage; import net.runelite.api.widgets.Widget; import net.runelite.api.widgets.WidgetInfo; import net.runelite.client.Notifier; +import net.runelite.client.chat.ChatCommandManager; +import net.runelite.client.chat.ChatMessageManager; import net.runelite.client.game.ItemManager; import net.runelite.client.ui.overlay.OverlayManager; import net.runelite.client.ui.overlay.infobox.InfoBoxManager; +import net.runelite.http.api.chat.ChatClient; import static org.junit.Assert.assertEquals; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; import org.mockito.Mock; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import org.mockito.runners.MockitoJUnitRunner; - @RunWith(MockitoJUnitRunner.class) public class SlayerPluginTest { @@ -129,6 +139,22 @@ public class SlayerPluginTest @Bind Notifier notifier; + @Mock + @Bind + ChatMessageManager chatMessageManager; + + @Mock + @Bind + ChatCommandManager chatCommandManager; + + @Mock + @Bind + ScheduledExecutorService executor; + + @Mock + @Bind + ChatClient chatClient; + @Inject SlayerPlugin slayerPlugin; @@ -492,4 +518,48 @@ public class SlayerPluginTest slayerPlugin.killedOne(); assertEquals(30, slayerPlugin.getAmount()); } + + @Test + public void testTaskLookup() throws IOException + { + net.runelite.http.api.chat.Task task = new net.runelite.http.api.chat.Task(); + task.setTask("task"); + task.setLocation("loc"); + task.setAmount(42); + task.setInitialAmount(42); + + when(slayerConfig.taskCommand()).thenReturn(true); + when(chatClient.getTask(anyString())).thenReturn(task); + + SetMessage setMessage = new SetMessage(); + setMessage.setType(ChatMessageType.PUBLIC); + setMessage.setName("Adam"); + setMessage.setMessageNode(mock(MessageNode.class)); + + slayerPlugin.taskLookup(setMessage, "!task"); + + verify(chatMessageManager).update(any(MessageNode.class)); + } + + @Test + public void testTaskLookupInvalid() throws IOException + { + net.runelite.http.api.chat.Task task = new net.runelite.http.api.chat.Task(); + task.setTask("task<"); + task.setLocation("loc"); + task.setAmount(42); + task.setInitialAmount(42); + + when(slayerConfig.taskCommand()).thenReturn(true); + when(chatClient.getTask(anyString())).thenReturn(task); + + SetMessage setMessage = new SetMessage(); + setMessage.setType(ChatMessageType.PUBLIC); + setMessage.setName("Adam"); + setMessage.setMessageNode(mock(MessageNode.class)); + + slayerPlugin.taskLookup(setMessage, "!task"); + + verify(chatMessageManager, never()).update(any(MessageNode.class)); + } }