diff --git a/http-api/src/main/java/net/runelite/http/api/config/ConfigClient.java b/http-api/src/main/java/net/runelite/http/api/config/ConfigClient.java new file mode 100644 index 0000000000..fbdb2610e6 --- /dev/null +++ b/http-api/src/main/java/net/runelite/http/api/config/ConfigClient.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2017, 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.config; + +import com.google.gson.JsonParseException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.UUID; +import net.runelite.http.api.RuneLiteAPI; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ConfigClient +{ + private static final Logger logger = LoggerFactory.getLogger(ConfigClient.class); + + private static final MediaType TEXT_PLAIN = MediaType.parse("text/plain"); + + private final UUID uuid; + + public ConfigClient(UUID uuid) + { + this.uuid = uuid; + } + + public Configuration get() throws IOException + { + HttpUrl url = RuneLiteAPI.getApiBase().newBuilder() + .addPathSegment("config") + .build(); + + logger.debug("Built URI: {}", url); + + Request request = new Request.Builder() + .header(RuneLiteAPI.RUNELITE_AUTH, uuid.toString()) + .url(url) + .build(); + + try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute()) + { + InputStream in = response.body().byteStream(); + return RuneLiteAPI.GSON.fromJson(new InputStreamReader(in), Configuration.class); + } + catch (JsonParseException ex) + { + throw new IOException(ex); + } + } + + public void set(String key, String value) + { + HttpUrl url = RuneLiteAPI.getApiBase().newBuilder() + .addPathSegment("config") + .addPathSegment(key) + .build(); + + logger.debug("Built URI: {}", url); + + Request request = new Request.Builder() + .put(RequestBody.create(TEXT_PLAIN, value)) + .header(RuneLiteAPI.RUNELITE_AUTH, uuid.toString()) + .url(url) + .build(); + + RuneLiteAPI.CLIENT.newCall(request).enqueue(new Callback() + { + @Override + public void onFailure(Call call, IOException e) + { + logger.warn("Unable to synchronize configuration item", e); + } + + @Override + public void onResponse(Call call, Response response) + { + response.close(); + logger.debug("Synchronized configuration value '{}' to '{}'", key, value); + } + }); + } + + public void unset(String key) + { + HttpUrl url = RuneLiteAPI.getApiBase().newBuilder() + .addPathSegment("config") + .addPathSegment(key) + .build(); + + logger.debug("Built URI: {}", url); + + Request request = new Request.Builder() + .delete() + .header(RuneLiteAPI.RUNELITE_AUTH, uuid.toString()) + .url(url) + .build(); + + RuneLiteAPI.CLIENT.newCall(request).enqueue(new Callback() + { + @Override + public void onFailure(Call call, IOException e) + { + logger.warn("Unable to unset configuration item", e); + } + + @Override + public void onResponse(Call call, Response response) + { + response.close(); + logger.debug("Unset configuration value '{}'", key); + } + }); + } +} \ No newline at end of file diff --git a/http-api/src/main/java/net/runelite/http/api/config/ConfigEntry.java b/http-api/src/main/java/net/runelite/http/api/config/ConfigEntry.java new file mode 100644 index 0000000000..33db4064e9 --- /dev/null +++ b/http-api/src/main/java/net/runelite/http/api/config/ConfigEntry.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2017, 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.config; + +public class ConfigEntry +{ + private String key; + private String value; + + public String getKey() + { + return key; + } + + public void setKey(String key) + { + this.key = key; + } + + public String getValue() + { + return value; + } + + public void setValue(String value) + { + this.value = value; + } +} \ No newline at end of file diff --git a/http-api/src/main/java/net/runelite/http/api/config/Configuration.java b/http-api/src/main/java/net/runelite/http/api/config/Configuration.java new file mode 100644 index 0000000000..7d5b86d9b2 --- /dev/null +++ b/http-api/src/main/java/net/runelite/http/api/config/Configuration.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2017, 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.config; + +import java.util.ArrayList; +import java.util.List; + +public class Configuration +{ + private List config = new ArrayList<>(); + + public Configuration(List config) + { + this.config = config; + } + + public List getConfig() + { + return config; + } +} \ No newline at end of file diff --git a/http-api/src/main/java/net/runelite/http/api/discord/DiscordMessage.java b/http-api/src/main/java/net/runelite/http/api/discord/DiscordMessage.java index 061568da9d..48cc3c3665 100644 --- a/http-api/src/main/java/net/runelite/http/api/discord/DiscordMessage.java +++ b/http-api/src/main/java/net/runelite/http/api/discord/DiscordMessage.java @@ -48,6 +48,7 @@ public class DiscordMessage String avatarUrl; @SerializedName("tts") boolean textToSpeech; + @Builder.Default List embeds = new ArrayList<>(); public DiscordMessage() diff --git a/http-api/src/main/java/net/runelite/http/api/ws/messages/party/PartyChatMessage.java b/http-api/src/main/java/net/runelite/http/api/ws/messages/party/PartyChatMessage.java index cc67d2a051..c61ea86187 100644 --- a/http-api/src/main/java/net/runelite/http/api/ws/messages/party/PartyChatMessage.java +++ b/http-api/src/main/java/net/runelite/http/api/ws/messages/party/PartyChatMessage.java @@ -24,9 +24,11 @@ */ package net.runelite.http.api.ws.messages.party; +import lombok.EqualsAndHashCode; import lombok.Value; import net.runelite.api.events.Event; @Value +@EqualsAndHashCode(callSuper = true) public class PartyChatMessage extends PartyMemberMessage implements Event { private final String value; diff --git a/http-service-plus/build.gradle b/http-service-plus/build.gradle new file mode 100644 index 0000000000..072c1c74a6 --- /dev/null +++ b/http-service-plus/build.gradle @@ -0,0 +1,30 @@ +apply plugin: 'war' + +description = 'Web Service Plus' +dependencies { + implementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.1.6.RELEASE' + implementation group: 'org.springframework.boot', name: 'spring-boot-devtools', version: '2.1.6.RELEASE' + implementation group: 'org.springframework', name: 'spring-jdbc', version: '5.1.8.RELEASE' + implementation group: 'org.mapstruct', name: 'mapstruct-jdk8', version: '1.3.0.Final' + api project(':http-api') + api project(':cache') + api project(':http-service') + implementation group: 'org.sql2o', name: 'sql2o', version: '1.6.0' + implementation group: 'com.google.guava', name: 'guava', version: '28.0-jre' + implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.5' + implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.26' + implementation group: 'com.github.scribejava', name: 'scribejava-apis', version: '6.7.0' + implementation group: 'io.minio', name: 'minio', version: '6.0.8' + implementation(group: 'redis.clients', name: 'jedis', version: '3.1.0') { + exclude(module: 'commons-pool2') + } + testImplementation(group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: '2.1.6.RELEASE') { + exclude(module: 'commons-logging') + } + testImplementation group: 'com.squareup.okhttp3', name: 'mockwebserver', version: '4.0.1' + testImplementation group: 'com.h2database', name: 'h2', version: '1.4.199' + providedCompile group: 'org.springframework.boot', name: 'spring-boot-starter-tomcat', version: '2.1.6.RELEASE' + providedCompile group: 'org.projectlombok', name: 'lombok', version: '1.18.8' + annotationProcessor group: 'org.projectlombok', name: 'lombok', version: '1.18.8' + providedCompile group: 'org.mariadb.jdbc', name: 'mariadb-java-client', version: '2.4.2' +} diff --git a/http-service-plus/src/main/java/net/runelite/http/service/SpringBootWebApplication.java b/http-service-plus/src/main/java/net/runelite/http/service/SpringBootWebApplication.java new file mode 100644 index 0000000000..50d94e97a9 --- /dev/null +++ b/http-service-plus/src/main/java/net/runelite/http/service/SpringBootWebApplication.java @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2017, 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.service; + +import ch.qos.logback.classic.LoggerContext; +import com.google.common.base.Strings; +import java.io.IOException; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.servlet.ServletException; +import javax.sql.DataSource; +import lombok.extern.slf4j.Slf4j; +import net.runelite.http.api.RuneLiteAPI; +import net.runelite.http.service.util.InstantConverter; +import okhttp3.Cache; +import okhttp3.OkHttpClient; +import org.slf4j.ILoggerFactory; +import org.slf4j.impl.StaticLoggerBinder; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.sql2o.Sql2o; +import org.sql2o.converters.Converter; +import org.sql2o.quirks.NoQuirks; + +@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) +@EnableScheduling +@Slf4j +public class SpringBootWebApplication extends SpringBootServletInitializer +{ + @Bean + protected ServletContextListener listener() + { + return new ServletContextListener() + { + @Override + public void contextInitialized(ServletContextEvent sce) + { + log.info("RuneLitePlus API started"); + } + + @Override + public void contextDestroyed(ServletContextEvent sce) + { + // Destroy okhttp client + OkHttpClient client = RuneLiteAPI.CLIENT; + client.dispatcher().executorService().shutdown(); + client.connectionPool().evictAll(); + try + { + Cache cache = client.cache(); + if (cache != null) + { + cache.close(); + } + } + catch (IOException ex) + { + log.warn(null, ex); + } + + log.info("RuneLitePlus API stopped"); + } + + }; + } + + @ConfigurationProperties(prefix = "datasource.runeliteplus") + @Bean("dataSourceRuneLite") + public DataSourceProperties dataSourceProperties() + { + return new DataSourceProperties(); + } + + @ConfigurationProperties(prefix = "datasource.runeliteplus-cache") + @Bean("dataSourceRuneLiteCache") + public DataSourceProperties dataSourcePropertiesCache() + { + return new DataSourceProperties(); + } + + @ConfigurationProperties(prefix = "datasource.runeliteplus-tracker") + @Bean("dataSourceRuneLiteTracker") + public DataSourceProperties dataSourcePropertiesTracker() + { + return new DataSourceProperties(); + } + + @Bean(value = "runelite", destroyMethod = "") + public DataSource runeliteDataSource(@Qualifier("dataSourceRuneLite") DataSourceProperties dataSourceProperties) + { + return getDataSource(dataSourceProperties); + } + + @Bean(value = "runelite-cache2", destroyMethod = "") + public DataSource runeliteCache2DataSource(@Qualifier("dataSourceRuneLiteCache") DataSourceProperties dataSourceProperties) + { + return getDataSource(dataSourceProperties); + } + + @Bean(value = "runelite-tracker", destroyMethod = "") + public DataSource runeliteTrackerDataSource(@Qualifier("dataSourceRuneLiteTracker") DataSourceProperties dataSourceProperties) + { + return getDataSource(dataSourceProperties); + } + + @Bean("Runelite SQL2O") + public Sql2o sql2o(@Qualifier("runelite") DataSource dataSource) + { + return createSql2oFromDataSource(dataSource); + } + + @Bean("Runelite Cache SQL2O") + public Sql2o cacheSql2o(@Qualifier("runelite-cache2") DataSource dataSource) + { + return createSql2oFromDataSource(dataSource); + } + + @Bean("Runelite XP Tracker SQL2O") + public Sql2o trackerSql2o(@Qualifier("runelite-tracker") DataSource dataSource) + { + return createSql2oFromDataSource(dataSource); + } + + private static DataSource getDataSource(DataSourceProperties dataSourceProperties) + { + if (!Strings.isNullOrEmpty(dataSourceProperties.getJndiName())) + { + // Use JNDI provided datasource, which is already configured with pooling + JndiDataSourceLookup dataSourceLookup = new JndiDataSourceLookup(); + return dataSourceLookup.getDataSource(dataSourceProperties.getJndiName()); + } + else + { + return dataSourceProperties.initializeDataSourceBuilder().build(); + } + } + + private static Sql2o createSql2oFromDataSource(final DataSource dataSource) + { + final Map converters = new HashMap<>(); + converters.put(Instant.class, new InstantConverter()); + return new Sql2o(dataSource, new NoQuirks(converters)); + } + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) + { + return application.sources(SpringBootWebApplication.class); + } + + @Override + public void onStartup(ServletContext servletContext) throws ServletException + { + super.onStartup(servletContext); + ILoggerFactory loggerFactory = StaticLoggerBinder.getSingleton().getLoggerFactory(); + if (loggerFactory instanceof LoggerContext) + { + LoggerContext loggerContext = (LoggerContext) loggerFactory; + loggerContext.setPackagingDataEnabled(false); + log.debug("Disabling logback packaging data"); + } + } + + public static void main(String[] args) + { + SpringApplication.run(SpringBootWebApplication.class, args); + } +} diff --git a/http-service-plus/src/main/java/net/runelite/http/service/SpringWebMvcConfigurer.java b/http-service-plus/src/main/java/net/runelite/http/service/SpringWebMvcConfigurer.java new file mode 100644 index 0000000000..841eb28eeb --- /dev/null +++ b/http-service-plus/src/main/java/net/runelite/http/service/SpringWebMvcConfigurer.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2018, 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.service; + +import java.util.List; +import net.runelite.http.api.RuneLiteAPI; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.GsonHttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@EnableWebMvc +public class SpringWebMvcConfigurer implements WebMvcConfigurer +{ + /** + * Configure .js as application/json to trick Cloudflare into caching json responses + */ + @Override + public void configureContentNegotiation(ContentNegotiationConfigurer configurer) + { + configurer.mediaType("js", MediaType.APPLICATION_JSON); + } + + /** + * Use GSON instead of Jackson for JSON serialization + * @param converters + */ + @Override + public void extendMessageConverters(List> converters) + { + // Could not figure out a better way to force GSON + converters.removeIf(MappingJackson2HttpMessageConverter.class::isInstance); + + GsonHttpMessageConverter gsonHttpMessageConverter = new GsonHttpMessageConverter(); + gsonHttpMessageConverter.setGson(RuneLiteAPI.GSON); + converters.add(gsonHttpMessageConverter); + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/animation/AnimationCache.java b/http-service-plus/src/main/java/net/runelite/http/service/animation/AnimationCache.java similarity index 100% rename from http-service/src/main/java/net/runelite/http/service/animation/AnimationCache.java rename to http-service-plus/src/main/java/net/runelite/http/service/animation/AnimationCache.java diff --git a/http-service/src/main/java/net/runelite/http/service/animation/AnimationController.java b/http-service-plus/src/main/java/net/runelite/http/service/animation/AnimationController.java similarity index 100% rename from http-service/src/main/java/net/runelite/http/service/animation/AnimationController.java rename to http-service-plus/src/main/java/net/runelite/http/service/animation/AnimationController.java diff --git a/http-service/src/main/java/net/runelite/http/service/animation/AnimationEndpoint.java b/http-service-plus/src/main/java/net/runelite/http/service/animation/AnimationEndpoint.java similarity index 100% rename from http-service/src/main/java/net/runelite/http/service/animation/AnimationEndpoint.java rename to http-service-plus/src/main/java/net/runelite/http/service/animation/AnimationEndpoint.java diff --git a/http-service/src/main/java/net/runelite/http/service/animation/AnimationEntry.java b/http-service-plus/src/main/java/net/runelite/http/service/animation/AnimationEntry.java similarity index 100% rename from http-service/src/main/java/net/runelite/http/service/animation/AnimationEntry.java rename to http-service-plus/src/main/java/net/runelite/http/service/animation/AnimationEntry.java diff --git a/http-service-plus/src/main/java/net/runelite/http/service/chat/ChatController.java b/http-service-plus/src/main/java/net/runelite/http/service/chat/ChatController.java new file mode 100644 index 0000000000..df287e6512 --- /dev/null +++ b/http-service-plus/src/main/java/net/runelite/http/service/chat/ChatController.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2018, 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.service.chat; + +import java.util.regex.Pattern; +import com.google.common.base.Strings; +import net.runelite.http.api.chat.House; +import net.runelite.http.service.util.exception.NotFoundException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@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; + + @Autowired + private ChatService chatService; + + @PostMapping("/layout") + public void submitLayout(@RequestParam String name, @RequestParam String layout) + { + if (Strings.isNullOrEmpty(layout)) + { + return; + } + + chatService.setLayout(name, layout); + } + + @GetMapping("/layout") + public String getLayout(@RequestParam String name) + { + String layout = chatService.getLayout(name); + if (layout == null) + { + throw new NotFoundException(); + } + return layout; + } + + @PostMapping("/hosts") + public void submitHost(@RequestParam int world, @RequestParam String location, @RequestParam String owner, @RequestParam boolean guildedAltar, @RequestParam boolean occultAltar, @RequestParam boolean spiritTree, @RequestParam boolean fairyRing, @RequestParam boolean wildernessObelisk, @RequestParam boolean repairStand, @RequestParam boolean combatDummy, @RequestParam(required = false, defaultValue = "false") boolean remove) + { + if (!location.equals("Rimmington") && !location.equals("Yanille")) + { + return; + } + + House house = new House(); + house.setOwner(owner); + house.setGuildedAltarPresent(guildedAltar); + house.setOccultAltarPresent(occultAltar); + house.setSpiritTreePresent(spiritTree); + house.setFairyRingPresent(fairyRing); + house.setWildernessObeliskPresent(wildernessObelisk); + house.setRepairStandPresent(repairStand); + house.setCombatDummyPresent(combatDummy); + + if (remove) + { + chatService.removeHost(world, location, house); + } + else + { + chatService.addHost(world, location, house); + } + } + + @GetMapping("/hosts") + public House[] getHosts(@RequestParam int world, @RequestParam String location) + { + return chatService.getHosts(world, location); + } +} \ No newline at end of file diff --git a/http-service-plus/src/main/java/net/runelite/http/service/chat/ChatService.java b/http-service-plus/src/main/java/net/runelite/http/service/chat/ChatService.java new file mode 100644 index 0000000000..aa23d3f18d --- /dev/null +++ b/http-service-plus/src/main/java/net/runelite/http/service/chat/ChatService.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2018, 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.service.chat; + +import java.time.Duration; +import java.util.List; +import net.runelite.http.api.chat.ChatClient; +import net.runelite.http.api.RuneLiteAPI; +import net.runelite.http.api.chat.House; +import net.runelite.http.service.util.redis.RedisPool; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import redis.clients.jedis.Jedis; + +@Service +public class ChatService +{ + private static final Duration EXPIRE = Duration.ofMinutes(2); + + private final RedisPool jedisPool; + private final ChatClient chatClient = new ChatClient(); + + + @Autowired + public ChatService(RedisPool jedisPool) + { + this.jedisPool = jedisPool; + } + + public String getLayout(String name) + { + String value; + try (Jedis jedis = jedisPool.getResource()) + { + value = jedis.get("layout." + name); + } + return value; + } + + public void setLayout(String name, String layout) + { + if (!chatClient.testLayout(layout)) + { + throw new IllegalArgumentException(layout); + } + + try (Jedis jedis = jedisPool.getResource()) + { + jedis.setex("layout." + name, (int) EXPIRE.getSeconds(), layout); + } + } + + public void addHost(int world, String location, House house) + { + String houseJSON = house.toString(); + + String key = "hosts.w" + Integer.toString(world) + "." + location; + + try (Jedis jedis = jedisPool.getResource()) + { + jedis.rpush(key, houseJSON); + } + } + + public House[] getHosts(int world, String location) + { + List json; + String key = "hosts.w" + Integer.toString(world) + "." + location; + + try (Jedis jedis = jedisPool.getResource()) + { + json = jedis.lrange(key, 0, 25); + } + + if (json.isEmpty()) + { + return null; + } + + House[] hosts = new House[json.size()]; + for (int i = 0; i < json.size(); i++) + { + hosts[i] = RuneLiteAPI.GSON.fromJson(json.get(i), House.class); + } + return hosts; + } + + public void removeHost(int world, String location, House house) + { + String json = house.toString(); + String key = "hosts.w" + Integer.toString(world) + "." + location; + + try (Jedis jedis = jedisPool.getResource()) + { + jedis.lrem(key, 0, json); + } + } +} \ No newline at end of file diff --git a/http-service-plus/src/main/java/net/runelite/http/service/xtea/XteaCache.java b/http-service-plus/src/main/java/net/runelite/http/service/xtea/XteaCache.java new file mode 100644 index 0000000000..7c5f2bb5b0 --- /dev/null +++ b/http-service-plus/src/main/java/net/runelite/http/service/xtea/XteaCache.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2018, 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.service.xtea; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +class XteaCache +{ + private int region; + private int key1; + private int key2; + private int key3; + private int key4; +} diff --git a/http-service-plus/src/main/java/net/runelite/http/service/xtea/XteaController.java b/http-service-plus/src/main/java/net/runelite/http/service/xtea/XteaController.java new file mode 100644 index 0000000000..b0c592fc70 --- /dev/null +++ b/http-service-plus/src/main/java/net/runelite/http/service/xtea/XteaController.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2018, 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.service.xtea; + +import java.util.List; +import java.util.stream.Collectors; +import net.runelite.http.api.xtea.XteaKey; +import net.runelite.http.api.xtea.XteaRequest; +import net.runelite.http.service.util.exception.NotFoundException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import static org.springframework.web.bind.annotation.RequestMethod.POST; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/xtea") +public class XteaController +{ + @Autowired + private XteaEndpoint xteaService; + + @RequestMapping(method = POST) + public void submit(@RequestBody XteaRequest xteaRequest) + { + xteaService.submit(xteaRequest); + } + + @GetMapping + public List get() + { + return xteaService.get().stream() + .map(XteaController::entryToKey) + .collect(Collectors.toList()); + } + + @GetMapping("/{region}") + public XteaKey getRegion(@PathVariable int region) + { + XteaEntry xteaRegion = xteaService.getRegion(region); + if (xteaRegion == null) + { + throw new NotFoundException(); + } + + return entryToKey(xteaRegion); + } + + private static XteaKey entryToKey(XteaEntry xe) + { + XteaKey xteaKey = new XteaKey(); + xteaKey.setRegion(xe.getRegion()); + xteaKey.setKeys(new int[] + { + xe.getKey1(), + xe.getKey2(), + xe.getKey3(), + xe.getKey4() + }); + return xteaKey; + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/xtea/XteaEndpoint.java b/http-service-plus/src/main/java/net/runelite/http/service/xtea/XteaEndpoint.java similarity index 100% rename from http-service/src/main/java/net/runelite/http/service/xtea/XteaEndpoint.java rename to http-service-plus/src/main/java/net/runelite/http/service/xtea/XteaEndpoint.java diff --git a/http-service-plus/src/main/java/net/runelite/http/service/xtea/XteaEntry.java b/http-service-plus/src/main/java/net/runelite/http/service/xtea/XteaEntry.java new file mode 100644 index 0000000000..e1e86d0bf7 --- /dev/null +++ b/http-service-plus/src/main/java/net/runelite/http/service/xtea/XteaEntry.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2017, 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.service.xtea; + +import java.time.Instant; + +public class XteaEntry +{ + private int region; + private Instant time; + private int rev; + private int key1; + private int key2; + private int key3; + private int key4; + + public int getRegion() + { + return region; + } + + public void setRegion(int region) + { + this.region = region; + } + + public Instant getTime() + { + return time; + } + + public void setTime(Instant time) + { + this.time = time; + } + + public int getRev() + { + return rev; + } + + public void setRev(int rev) + { + this.rev = rev; + } + + public int getKey1() + { + return key1; + } + + public void setKey1(int key1) + { + this.key1 = key1; + } + + public int getKey2() + { + return key2; + } + + public void setKey2(int key2) + { + this.key2 = key2; + } + + public int getKey3() + { + return key3; + } + + public void setKey3(int key3) + { + this.key3 = key3; + } + + public int getKey4() + { + return key4; + } + + public void setKey4(int key4) + { + this.key4 = key4; + } + +} diff --git a/http-service-plus/src/main/resources/application-dev.yaml b/http-service-plus/src/main/resources/application-dev.yaml new file mode 100644 index 0000000000..cc2286e9b3 --- /dev/null +++ b/http-service-plus/src/main/resources/application-dev.yaml @@ -0,0 +1,31 @@ +# Enable debug logging +debug: true +logging.level.net.runelite: DEBUG + +# Development data sources +datasource: + runelite: + jndiName: + driverClassName: org.mariadb.jdbc.Driver + type: org.mariadb.jdbc.MariaDbDataSource + url: jdbc:mariadb://localhost:3306/runelite + username: runelite + password: runelite + runelite-cache: + jndiName: + driverClassName: org.mariadb.jdbc.Driver + type: org.mariadb.jdbc.MariaDbDataSource + url: jdbc:mariadb://localhost:3306/cache + username: runelite + password: runelite + runelite-tracker: + jndiName: + driverClassName: org.mariadb.jdbc.Driver + type: org.mariadb.jdbc.MariaDbDataSource + url: jdbc:mariadb://localhost:3306/xptracker + username: runelite + password: runelite + +# Development oauth callback (without proxy) +oauth: + callback: http://localhost:8080/account/callback diff --git a/http-service-plus/src/main/resources/application.yaml b/http-service-plus/src/main/resources/application.yaml new file mode 100644 index 0000000000..8e46babb83 --- /dev/null +++ b/http-service-plus/src/main/resources/application.yaml @@ -0,0 +1,39 @@ +datasource: + runeliteplus: + jndiName: java:comp/env/jdbc/runelite + runeliteplus-cache: + jndiName: java:comp/env/jdbc/runelite-cache2 + runeliteplus-tracker: + jndiName: java:comp/env/jdbc/runelite-tracker + +# By default Spring tries to register the datasource as an MXBean, +# so if multiple apis are deployed on one web container with +# shared datasource it tries to register it multiples times and +# fails when starting the 2nd api +spring.jmx.enabled: false + +# Google OAuth client +oauth: + client-id: + client-secret: + callback: https://api.runelite.net/oauth/ + +# Minio client storage for cache +minio: + endpoint: http://localhost:9000 + accesskey: AM54M27O4WZK65N6F8IP + secretkey: /PZCxzmsJzwCHYlogcymuprniGCaaLUOET2n6yMP + bucket: runelite + +# Redis client for temporary data storage +redis: + pool.size: 10 + host: http://localhost:6379 + + +# Twitter client for feed +runelite: + twitter: + consumerkey: + secretkey: + listid: 968949795153948673 \ No newline at end of file diff --git a/http-service-plus/src/main/resources/net/runelite/http/service/xp/schema.sql b/http-service-plus/src/main/resources/net/runelite/http/service/xp/schema.sql new file mode 100644 index 0000000000..70f83a30fd --- /dev/null +++ b/http-service-plus/src/main/resources/net/runelite/http/service/xp/schema.sql @@ -0,0 +1,135 @@ +-- MySQL dump 10.16 Distrib 10.2.18-MariaDB, for Linux (x86_64) +-- +-- Host: localhost Database: xptracker +-- ------------------------------------------------------ +-- Server version 10.2.18-MariaDB + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `player` +-- + +DROP TABLE IF EXISTS `player`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `player` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(32) NOT NULL, + `tracked_since` timestamp NOT NULL DEFAULT current_timestamp(), + `last_updated` timestamp NOT NULL DEFAULT current_timestamp(), + `rank` int(11) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `xp` +-- + +DROP TABLE IF EXISTS `xp`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `xp` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `time` timestamp NOT NULL DEFAULT current_timestamp(), + `player` int(11) NOT NULL, + `attack_xp` int(11) NOT NULL, + `defence_xp` int(11) NOT NULL, + `strength_xp` int(11) NOT NULL, + `hitpoints_xp` int(11) NOT NULL, + `ranged_xp` int(11) NOT NULL, + `prayer_xp` int(11) NOT NULL, + `magic_xp` int(11) NOT NULL, + `cooking_xp` int(11) NOT NULL, + `woodcutting_xp` int(11) NOT NULL, + `fletching_xp` int(11) NOT NULL, + `fishing_xp` int(11) NOT NULL, + `firemaking_xp` int(11) NOT NULL, + `crafting_xp` int(11) NOT NULL, + `smithing_xp` int(11) NOT NULL, + `mining_xp` int(11) NOT NULL, + `herblore_xp` int(11) NOT NULL, + `agility_xp` int(11) NOT NULL, + `thieving_xp` int(11) NOT NULL, + `slayer_xp` int(11) NOT NULL, + `farming_xp` int(11) NOT NULL, + `runecraft_xp` int(11) NOT NULL, + `hunter_xp` int(11) NOT NULL, + `construction_xp` int(11) NOT NULL, + `overall_xp` int(11) GENERATED ALWAYS AS (`attack_xp` + `defence_xp` + `strength_xp` + `hitpoints_xp` + `ranged_xp` + `prayer_xp` + `magic_xp` + `cooking_xp` + `woodcutting_xp` + `fletching_xp` + `fishing_xp` + `firemaking_xp` + `crafting_xp` + `smithing_xp` + `mining_xp` + `herblore_xp` + `agility_xp` + `thieving_xp` + `slayer_xp` + `farming_xp` + `runecraft_xp` + `hunter_xp` + `construction_xp`) VIRTUAL, + `attack_level` int(11) GENERATED ALWAYS AS (level_for_xp(`attack_xp` AS `attack_xp`)) VIRTUAL, + `defence_level` int(11) GENERATED ALWAYS AS (level_for_xp(`defence_xp` AS `defence_xp`)) VIRTUAL, + `strength_level` int(11) GENERATED ALWAYS AS (level_for_xp(`strength_xp` AS `strength_xp`)) VIRTUAL, + `hitpoints_level` int(11) GENERATED ALWAYS AS (level_for_xp(`hitpoints_xp` AS `hitpoints_xp`)) VIRTUAL, + `ranged_level` int(11) GENERATED ALWAYS AS (level_for_xp(`ranged_xp` AS `ranged_xp`)) VIRTUAL, + `prayer_level` int(11) GENERATED ALWAYS AS (level_for_xp(`prayer_xp` AS `prayer_xp`)) VIRTUAL, + `magic_level` int(11) GENERATED ALWAYS AS (level_for_xp(`magic_xp` AS `magic_xp`)) VIRTUAL, + `cooking_level` int(11) GENERATED ALWAYS AS (level_for_xp(`cooking_xp` AS `cooking_xp`)) VIRTUAL, + `woodcutting_level` int(11) GENERATED ALWAYS AS (level_for_xp(`woodcutting_xp` AS `woodcutting_xp`)) VIRTUAL, + `fletching_level` int(11) GENERATED ALWAYS AS (level_for_xp(`fletching_xp` AS `fletching_xp`)) VIRTUAL, + `fishing_level` int(11) GENERATED ALWAYS AS (level_for_xp(`fishing_xp` AS `fishing_xp`)) VIRTUAL, + `firemaking_level` int(11) GENERATED ALWAYS AS (level_for_xp(`firemaking_xp` AS `firemaking_xp`)) VIRTUAL, + `crafting_level` int(11) GENERATED ALWAYS AS (level_for_xp(`crafting_xp` AS `crafting_xp`)) VIRTUAL, + `smithing_level` int(11) GENERATED ALWAYS AS (level_for_xp(`smithing_xp` AS `smithing_xp`)) VIRTUAL, + `mining_level` int(11) GENERATED ALWAYS AS (level_for_xp(`mining_xp` AS `mining_xp`)) VIRTUAL, + `herblore_level` int(11) GENERATED ALWAYS AS (level_for_xp(`herblore_xp` AS `herblore_xp`)) VIRTUAL, + `agility_level` int(11) GENERATED ALWAYS AS (level_for_xp(`agility_xp` AS `agility_xp`)) VIRTUAL, + `thieving_level` int(11) GENERATED ALWAYS AS (level_for_xp(`thieving_xp` AS `thieving_xp`)) VIRTUAL, + `slayer_level` int(11) GENERATED ALWAYS AS (level_for_xp(`slayer_xp` AS `slayer_xp`)) VIRTUAL, + `farming_level` int(11) GENERATED ALWAYS AS (level_for_xp(`farming_xp` AS `farming_xp`)) VIRTUAL, + `runecraft_level` int(11) GENERATED ALWAYS AS (level_for_xp(`runecraft_xp` AS `runecraft_xp`)) VIRTUAL, + `hunter_level` int(11) GENERATED ALWAYS AS (level_for_xp(`hunter_xp` AS `hunter_xp`)) VIRTUAL, + `construction_level` int(11) GENERATED ALWAYS AS (level_for_xp(`construction_xp` AS `construction_xp`)) VIRTUAL, + `overall_level` int(11) GENERATED ALWAYS AS (`attack_level` + `defence_level` + `strength_level` + `hitpoints_level` + `ranged_level` + `prayer_level` + `magic_level` + `cooking_level` + `woodcutting_level` + `fletching_level` + `fishing_level` + `firemaking_level` + `crafting_level` + `smithing_level` + `mining_level` + `herblore_level` + `agility_level` + `thieving_level` + `slayer_level` + `farming_level` + `runecraft_level` + `hunter_level` + `construction_level`) VIRTUAL, + `attack_rank` int(11) NOT NULL, + `defence_rank` int(11) NOT NULL, + `strength_rank` int(11) NOT NULL, + `hitpoints_rank` int(11) NOT NULL, + `ranged_rank` int(11) NOT NULL, + `prayer_rank` int(11) NOT NULL, + `magic_rank` int(11) NOT NULL, + `cooking_rank` int(11) NOT NULL, + `woodcutting_rank` int(11) NOT NULL, + `fletching_rank` int(11) NOT NULL, + `fishing_rank` int(11) NOT NULL, + `firemaking_rank` int(11) NOT NULL, + `crafting_rank` int(11) NOT NULL, + `smithing_rank` int(11) NOT NULL, + `mining_rank` int(11) NOT NULL, + `herblore_rank` int(11) NOT NULL, + `agility_rank` int(11) NOT NULL, + `thieving_rank` int(11) NOT NULL, + `slayer_rank` int(11) NOT NULL, + `farming_rank` int(11) NOT NULL, + `runecraft_rank` int(11) NOT NULL, + `hunter_rank` int(11) NOT NULL, + `construction_rank` int(11) NOT NULL, + `overall_rank` int(11) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `player_time` (`player`,`time`), + KEY `idx_time` (`time`), + CONSTRAINT `fk_player` FOREIGN KEY (`player`) REFERENCES `player` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2019-02-15 21:01:17 \ No newline at end of file diff --git a/http-service-plus/src/main/templates/markdown.hbs b/http-service-plus/src/main/templates/markdown.hbs new file mode 100644 index 0000000000..1beacd052a --- /dev/null +++ b/http-service-plus/src/main/templates/markdown.hbs @@ -0,0 +1,110 @@ +{{#info}} +# {{title}} +{{join schemes " | "}}://{{host}}{{basePath}} + +{{description}} + +{{#contact}} +[**Contact the developer**](mailto:{{email}}) +{{/contact}} + +**Version** {{version}} + +{{#if termsOfService}} +[**Terms of Service**]({{termsOfService}}) +{{/if}} + +{{/info}} + +{{#if consumes}}__Consumes:__ {{join consumes ", "}}{{/if}} + +{{#if produces}}__Produces:__ {{join produces ", "}}{{/if}} + +{{#if securityDefinitions}} +# Security Definitions +{{> security}} +{{/if}} + +
+Table Of Contents +[toc] +
+ +# APIs + +{{#each paths}} +## {{@key}} +{{#this}} +{{#get}} +### GET +{{> operation}} +{{/get}} + +{{#put}} +### PUT +{{> operation}} +{{/put}} + +{{#post}} +### POST + +{{> operation}} + +{{/post}} + +{{#delete}} +### DELETE +{{> operation}} +{{/delete}} + +{{#option}} +### OPTION +{{> operation}} +{{/option}} + +{{#patch}} +### PATCH +{{> operation}} +{{/patch}} + +{{#head}} +### HEAD +{{> operation}} +{{/head}} + +{{/this}} +{{/each}} + +# Definitions +{{#each definitions}} +## {{@key}} + + + + + + + + + + {{#each this.properties}} + + + + + + + + {{/each}} +
nametyperequireddescriptionexample
{{@key}} + {{#ifeq type "array"}} + {{#items.$ref}} + {{type}}[{{basename items.$ref}}] + {{/items.$ref}} + {{^items.$ref}}{{type}}[{{items.type}}]{{/items.$ref}} + {{else}} + {{#$ref}}{{basename $ref}}{{/$ref}} + {{^$ref}}{{type}}{{#format}} ({{format}}){{/format}}{{/$ref}} + {{/ifeq}} + {{#required}}required{{/required}}{{^required}}optional{{/required}}{{#description}}{{{description}}}{{/description}}{{^description}}-{{/description}}{{example}}
+{{/each}} diff --git a/http-service-plus/src/main/templates/operation.hbs b/http-service-plus/src/main/templates/operation.hbs new file mode 100644 index 0000000000..f7015850b8 --- /dev/null +++ b/http-service-plus/src/main/templates/operation.hbs @@ -0,0 +1,71 @@ +{{#deprecated}}-deprecated-{{/deprecated}} +{{summary}} + +{{description}} + +{{#if externalDocs.url}}{{externalDocs.description}}. [See external documents for more details]({{externalDocs.url}}) +{{/if}} + +{{#if security}} +#### Security +{{/if}} + +{{#security}} +{{#each this}} +* {{@key}} +{{#this}} * {{this}} +{{/this}} +{{/each}} +{{/security}} + +#### Request + +{{#if consumes}}__Content-Type:__ {{join consumes ", "}}{{/if}} + +##### Parameters +{{#if parameters}} + + + + + + + + + +{{/if}} + +{{#parameters}} + + + + + + +{{#ifeq in "body"}} + +{{else}} + {{#ifeq type "array"}} + + {{else}} + + {{/ifeq}} +{{/ifeq}} + +{{/parameters}} +{{#if parameters}} +
NameLocated inRequiredDescriptionDefaultSchema
{{name}}{{in}}{{#if required}}yes{{else}}no{{/if}}{{description}}{{#if pattern}} (**Pattern**: `{{pattern}}`){{/if}} - + {{#ifeq schema.type "array"}}Array[{{basename schema.items.$ref}}]{{/ifeq}} + {{#schema.$ref}}{{basename schema.$ref}} {{/schema.$ref}} + Array[{{items.type}}] ({{collectionFormat}}){{type}} {{#format}}({{format}}){{/format}}
+{{/if}} + + +#### Response + +{{#if produces}}__Content-Type:__ {{join produces ", "}}{{/if}} + +| Status Code | Reason | Response Model | +|-------------|-------------|----------------| +{{#each responses}}| {{@key}} | {{description}} | {{#schema.$ref}}{{basename schema.$ref}}{{/schema.$ref}}{{#ifeq schema.type "array"}}Array[{{basename schema.items.$ref}}]{{/ifeq}}{{^schema}} - {{/schema}}| +{{/each}} diff --git a/http-service-plus/src/main/templates/security.hbs b/http-service-plus/src/main/templates/security.hbs new file mode 100644 index 0000000000..04f86e8380 --- /dev/null +++ b/http-service-plus/src/main/templates/security.hbs @@ -0,0 +1,88 @@ +{{#each securityDefinitions}} +### {{@key}} +{{#this}} +{{#ifeq type "oauth2"}} + + + + + +{{#if description}} + + + + +{{/if}} +{{#if authorizationUrl}} + + + + +{{/if}} +{{#if flow}} + + + + +{{/if}} +{{#if tokenUrl}} + + + + +{{/if}} +{{#if scopes}} + + +{{#each scopes}} + + + + +{{/each}} + +{{/if}} +
type{{type}}
description{{description}}
authorizationUrl{{authorizationUrl}}
flow{{flow}}
tokenUrl{{tokenUrl}}
scopes{{@key}}{{this}}
+{{/ifeq}} +{{#ifeq type "apiKey"}} + + + + + +{{#if description}} + + + + +{{/if}} +{{#if name}} + + + + +{{/if}} +{{#if in}} + + + + +{{/if}} +
type{{type}}
description{{description}}
name{{name}}
in{{in}}
+{{/ifeq}} +{{#ifeq type "basic"}} + + + + + +{{#if description}} + + + + +{{/if}} +
type{{type}}
description{{description}}
+{{/ifeq}} +{{/this}} +{{/each}} \ No newline at end of file diff --git a/http-service-plus/src/main/templates/template.html.hbs b/http-service-plus/src/main/templates/template.html.hbs new file mode 100644 index 0000000000..da587c2cc4 --- /dev/null +++ b/http-service-plus/src/main/templates/template.html.hbs @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + {{info.title}} {{info.version}} + + + + + \ No newline at end of file diff --git a/http-service-plus/src/main/webapp/WEB-INF/web.xml b/http-service-plus/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..0b07dbb973 --- /dev/null +++ b/http-service-plus/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,32 @@ + + + + RuneLite API + \ No newline at end of file diff --git a/http-service-plus/src/test/resources/application-test.yaml b/http-service-plus/src/test/resources/application-test.yaml new file mode 100644 index 0000000000..0532963ade --- /dev/null +++ b/http-service-plus/src/test/resources/application-test.yaml @@ -0,0 +1,17 @@ +# Use in-memory database for tests +datasource: + runelite: + jndiName: + driverClassName: org.h2.Driver + type: org.h2.jdbcx.JdbcDataSource + url: jdbc:h2:mem:runelite + runelite-cache: + jndiName: + driverClassName: org.h2.Driver + type: org.h2.jdbcx.JdbcDataSource + url: jdbc:h2:mem:cache + runelite-tracker: + jndiName: + driverClassName: org.h2.Driver + type: org.h2.jdbcx.JdbcDataSource + url: jdbc:h2:mem:xptracker diff --git a/http-service-plus/src/test/resources/net/runelite/http/service/worlds/worldlist b/http-service-plus/src/test/resources/net/runelite/http/service/worlds/worldlist new file mode 100644 index 0000000000..1d1360e579 Binary files /dev/null and b/http-service-plus/src/test/resources/net/runelite/http/service/worlds/worldlist differ diff --git a/http-service/build.gradle b/http-service/build.gradle index 127256d7a5..5049a4a196 100644 --- a/http-service/build.gradle +++ b/http-service/build.gradle @@ -6,6 +6,7 @@ dependencies { implementation group: 'org.springframework.boot', name: 'spring-boot-devtools', version: '2.1.6.RELEASE' implementation group: 'org.springframework', name: 'spring-jdbc', version: '5.1.8.RELEASE' implementation group: 'org.mapstruct', name: 'mapstruct-jdk8', version: '1.3.0.Final' + api project(':runelite-api') api project(':http-api') api project(':cache') implementation group: 'org.sql2o', name: 'sql2o', version: '1.6.0' @@ -14,6 +15,7 @@ dependencies { implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.26' implementation group: 'com.github.scribejava', name: 'scribejava-apis', version: '6.7.0' implementation group: 'io.minio', name: 'minio', version: '6.0.8' + implementation group: 'org.mongodb', name: 'mongodb-driver-sync', version: '3.10.2' implementation(group: 'redis.clients', name: 'jedis', version: '3.1.0') { exclude(module: 'commons-pool2') } diff --git a/http-service/src/main/java/net/runelite/http/service/SpringBootWebApplication.java b/http-service/src/main/java/net/runelite/http/service/SpringBootWebApplication.java index c24ebd8d49..a69d307d57 100644 --- a/http-service/src/main/java/net/runelite/http/service/SpringBootWebApplication.java +++ b/http-service/src/main/java/net/runelite/http/service/SpringBootWebApplication.java @@ -26,10 +26,13 @@ package net.runelite.http.service; import ch.qos.logback.classic.LoggerContext; import com.google.common.base.Strings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; import java.io.IOException; import java.time.Instant; import java.util.HashMap; import java.util.Map; +import javax.naming.NamingException; import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; @@ -43,6 +46,7 @@ import okhttp3.OkHttpClient; import org.slf4j.ILoggerFactory; import org.slf4j.impl.StaticLoggerBinder; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; @@ -52,6 +56,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; import org.springframework.context.annotation.Bean; import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup; +import org.springframework.jndi.JndiTemplate; import org.springframework.scheduling.annotation.EnableScheduling; import org.sql2o.Sql2o; import org.sql2o.converters.Converter; @@ -70,7 +75,7 @@ public class SpringBootWebApplication extends SpringBootServletInitializer @Override public void contextInitialized(ServletContextEvent sce) { - log.info("RuneLitePlus API started"); + log.info("RuneLite API started"); } @Override @@ -126,7 +131,7 @@ public class SpringBootWebApplication extends SpringBootServletInitializer return getDataSource(dataSourceProperties); } - @Bean(value = "runelite-cache2", destroyMethod = "") + @Bean(value = "runelite-cache", destroyMethod = "") public DataSource runeliteCache2DataSource(@Qualifier("dataSourceRuneLiteCache") DataSourceProperties dataSourceProperties) { return getDataSource(dataSourceProperties); @@ -145,7 +150,7 @@ public class SpringBootWebApplication extends SpringBootServletInitializer } @Bean("Runelite Cache SQL2O") - public Sql2o cacheSql2o(@Qualifier("runelite-cache2") DataSource dataSource) + public Sql2o cacheSql2o(@Qualifier("runelite-cache") DataSource dataSource) { return createSql2oFromDataSource(dataSource); } @@ -156,6 +161,24 @@ public class SpringBootWebApplication extends SpringBootServletInitializer return createSql2oFromDataSource(dataSource); } + @Bean + public MongoClient mongoClient(@Value("${mongo.host:}") String host, @Value("${mongo.jndiName:}") String jndiName) throws NamingException + { + if (!Strings.isNullOrEmpty(jndiName)) + { + JndiTemplate jndiTemplate = new JndiTemplate(); + return jndiTemplate.lookup(jndiName, MongoClient.class); + } + else if (!Strings.isNullOrEmpty(host)) + { + return MongoClients.create(host); + } + else + { + throw new RuntimeException("Either mongo.host or mongo.jndiName must be set"); + } + } + private static DataSource getDataSource(DataSourceProperties dataSourceProperties) { if (!Strings.isNullOrEmpty(dataSourceProperties.getJndiName())) diff --git a/http-service/src/main/java/net/runelite/http/service/SpringWebMvcConfigurer.java b/http-service/src/main/java/net/runelite/http/service/SpringWebMvcConfigurer.java index 841eb28eeb..704e4f9cb2 100644 --- a/http-service/src/main/java/net/runelite/http/service/SpringWebMvcConfigurer.java +++ b/http-service/src/main/java/net/runelite/http/service/SpringWebMvcConfigurer.java @@ -33,11 +33,11 @@ import org.springframework.http.converter.json.GsonHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer; import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; @Configuration @EnableWebMvc -public class SpringWebMvcConfigurer implements WebMvcConfigurer +public class SpringWebMvcConfigurer extends WebMvcConfigurerAdapter { /** * Configure .js as application/json to trick Cloudflare into caching json responses diff --git a/http-service/src/main/java/net/runelite/http/service/account/AccountService.java b/http-service/src/main/java/net/runelite/http/service/account/AccountService.java new file mode 100644 index 0000000000..aa11c0e63e --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/account/AccountService.java @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2017, 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.service.account; + +import com.github.scribejava.apis.GoogleApi20; +import com.github.scribejava.core.builder.ServiceBuilder; +import com.github.scribejava.core.model.OAuth2AccessToken; +import com.github.scribejava.core.model.OAuthRequest; +import com.github.scribejava.core.model.Response; +import com.github.scribejava.core.model.Verb; +import com.github.scribejava.core.oauth.OAuth20Service; +import com.google.gson.Gson; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import net.runelite.http.api.RuneLiteAPI; +import net.runelite.http.api.account.OAuthResponse; +import net.runelite.http.api.ws.WebsocketGsonFactory; +import net.runelite.http.api.ws.WebsocketMessage; +import net.runelite.http.api.ws.messages.LoginResponse; +import net.runelite.http.service.account.beans.SessionEntry; +import net.runelite.http.service.account.beans.UserEntry; +import net.runelite.http.service.util.redis.RedisPool; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.sql2o.Connection; +import org.sql2o.Sql2o; +import org.sql2o.Sql2oException; +import redis.clients.jedis.Jedis; + +@RestController +@RequestMapping("/account") +public class AccountService +{ + private static final Logger logger = LoggerFactory.getLogger(AccountService.class); + + private static final String CREATE_SESSIONS = "CREATE TABLE IF NOT EXISTS `sessions` (\n" + + " `user` int(11) NOT NULL PRIMARY KEY,\n" + + " `uuid` varchar(36) NOT NULL,\n" + + " `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n" + + " `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n" + + " UNIQUE KEY `uuid` (`uuid`),\n" + + " KEY `user` (`user`)\n" + + ") ENGINE=InnoDB"; + + private static final String CREATE_USERS = "CREATE TABLE IF NOT EXISTS `users` (\n" + + " `id` int(11) NOT NULL AUTO_INCREMENT,\n" + + " `username` tinytext NOT NULL,\n" + + " `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n" + + " PRIMARY KEY (`id`),\n" + + " UNIQUE KEY `username` (`username`(64))\n" + + ") ENGINE=InnoDB"; + + private static final String SESSIONS_FK = "ALTER TABLE `sessions`\n" + + " ADD CONSTRAINT `id` FOREIGN KEY (`user`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;"; + + private static final String SCOPE = "https://www.googleapis.com/auth/userinfo.email"; + private static final String USERINFO = "https://www.googleapis.com/oauth2/v2/userinfo"; + private static final String RL_REDIR = "https://runelite.net/logged-in"; + + private final Gson gson = RuneLiteAPI.GSON; + private final Gson websocketGson = WebsocketGsonFactory.build(); + + private final Sql2o sql2o; + private final String oauthClientId; + private final String oauthClientSecret; + private final String oauthCallback; + private final AuthFilter auth; + private final RedisPool jedisPool; + + @Autowired + public AccountService( + @Qualifier("Runelite SQL2O") Sql2o sql2o, + @Value("${oauth.client-id}") String oauthClientId, + @Value("${oauth.client-secret}") String oauthClientSecret, + @Value("${oauth.callback}") String oauthCallback, + AuthFilter auth, + RedisPool jedisPool + ) + { + this.sql2o = sql2o; + this.oauthClientId = oauthClientId; + this.oauthClientSecret = oauthClientSecret; + this.oauthCallback = oauthCallback; + this.auth = auth; + this.jedisPool = jedisPool; + + try (Connection con = sql2o.open()) + { + con.createQuery(CREATE_SESSIONS) + .executeUpdate(); + + con.createQuery(CREATE_USERS) + .executeUpdate(); + + try + { + con.createQuery(SESSIONS_FK) + .executeUpdate(); + } + catch (Sql2oException ex) + { + // Ignore, happens when index already exists + } + } + } + + @GetMapping("/login") + public OAuthResponse login(@RequestParam UUID uuid) + { + State state = new State(); + state.setUuid(uuid); + state.setApiVersion(RuneLiteAPI.getVersion()); + + OAuth20Service service = new ServiceBuilder(oauthClientId) + .apiSecret(oauthClientSecret) + .defaultScope(SCOPE) + .callback(oauthCallback) + .build(GoogleApi20.instance()); + + final Map additionalParams = new HashMap<>(); + additionalParams.put("prompt", "select_account"); + + final String authorizationUrl = service.createAuthorizationUrlBuilder() + .state(gson.toJson(state)) + .additionalParams(additionalParams) + .build(); + + OAuthResponse lr = new OAuthResponse(); + lr.setOauthUrl(authorizationUrl); + lr.setUid(uuid); + + return lr; + } + + @GetMapping("/callback") + public Object callback( + HttpServletRequest request, + HttpServletResponse response, + @RequestParam(required = false) String error, + @RequestParam String code, + @RequestParam("state") String stateStr + ) throws InterruptedException, ExecutionException, IOException + { + if (error != null) + { + logger.info("Error in oauth callback: {}", error); + return null; + } + + State state = gson.fromJson(stateStr, State.class); + + logger.info("Got authorization code {} for uuid {}", code, state.getUuid()); + + OAuth20Service service = new ServiceBuilder(oauthClientId) + .apiSecret(oauthClientSecret) + .defaultScope(SCOPE) + .callback(oauthCallback) + .build(GoogleApi20.instance()); + + OAuth2AccessToken accessToken = service.getAccessToken(code); + + // Access user info + OAuthRequest orequest = new OAuthRequest(Verb.GET, USERINFO); + service.signRequest(accessToken, orequest); + + Response oresponse = service.execute(orequest); + + if (oresponse.getCode() / 100 != 2) + { + // Could be a forged result + return null; + } + + UserInfo userInfo = gson.fromJson(oresponse.getBody(), UserInfo.class); + + logger.info("Got user info: {}", userInfo); + + try (Connection con = sql2o.open()) + { + con.createQuery("insert ignore into users (username) values (:username)") + .addParameter("username", userInfo.getEmail()) + .executeUpdate(); + + UserEntry user = con.createQuery("select id from users where username = :username") + .addParameter("username", userInfo.getEmail()) + .executeAndFetchFirst(UserEntry.class); + + if (user == null) + { + logger.warn("Unable to find newly created user session"); + return null; // that's weird + } + + // insert session + con.createQuery("insert ignore into sessions (user, uuid) values (:user, :uuid)") + .addParameter("user", user.getId()) + .addParameter("uuid", state.getUuid().toString()) + .executeUpdate(); + + logger.info("Created session for user {}", userInfo.getEmail()); + } + + response.sendRedirect(RL_REDIR); + + notifySession(state.getUuid(), userInfo.getEmail()); + + return ""; + } + + private void notifySession(UUID uuid, String username) + { + LoginResponse response = new LoginResponse(); + response.setUsername(username); + + try (Jedis jedis = jedisPool.getResource()) + { + jedis.publish("session." + uuid, websocketGson.toJson(response, WebsocketMessage.class)); + } + } + + @GetMapping("/logout") + public void logout(HttpServletRequest request, HttpServletResponse response) throws IOException + { + SessionEntry session = auth.handle(request, response); + + if (session == null) + { + return; + } + + try (Connection con = sql2o.open()) + { + con.createQuery("delete from sessions where uuid = :uuid") + .addParameter("uuid", session.getUuid().toString()) + .executeUpdate(); + } + } + + @GetMapping("/session-check") + public void sessionCheck(HttpServletRequest request, HttpServletResponse response) throws IOException + { + auth.handle(request, response); + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/account/AuthFilter.java b/http-service/src/main/java/net/runelite/http/service/account/AuthFilter.java new file mode 100644 index 0000000000..1f57127bf5 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/account/AuthFilter.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2017, 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.service.account; + +import java.io.IOException; +import net.runelite.http.service.account.beans.SessionEntry; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.UUID; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import net.runelite.http.api.RuneLiteAPI; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; +import org.sql2o.Connection; +import org.sql2o.Sql2o; + +@Service +public class AuthFilter +{ + private final Sql2o sql2o; + + @Autowired + public AuthFilter(@Qualifier("Runelite SQL2O") Sql2o sql2o) + { + this.sql2o = sql2o; + } + + public SessionEntry handle(HttpServletRequest request, HttpServletResponse response) throws IOException + { + String runeliteAuth = request.getHeader(RuneLiteAPI.RUNELITE_AUTH); + if (runeliteAuth == null) + { + response.sendError(401, "Access denied"); + return null; + } + + UUID uuid = UUID.fromString(runeliteAuth); + + try (Connection con = sql2o.open()) + { + SessionEntry sessionEntry = con.createQuery("select user, uuid, created from sessions where uuid = :uuid") + .addParameter("uuid", uuid.toString()) + .executeAndFetchFirst(SessionEntry.class); + + if (sessionEntry == null) + { + response.sendError(401, "Access denied"); + return null; + } + + Instant now = Instant.now(); + + con.createQuery("update sessions set last_used = :last_used where uuid = :uuid") + .addParameter("last_used", Timestamp.from(now)) + .addParameter("uuid", uuid.toString()) + .executeUpdate(); + + sessionEntry.setLastUsed(now); + + return sessionEntry; + } + } + +} diff --git a/http-service/src/main/java/net/runelite/http/service/account/State.java b/http-service/src/main/java/net/runelite/http/service/account/State.java new file mode 100644 index 0000000000..50b47b2c19 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/account/State.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2017, 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.service.account; + +import java.util.UUID; + +public class State +{ + private UUID uuid; + private String apiVersion; + + public UUID getUuid() + { + return uuid; + } + + public void setUuid(UUID uuid) + { + this.uuid = uuid; + } + + public String getApiVersion() + { + return apiVersion; + } + + public void setApiVersion(String apiVersion) + { + this.apiVersion = apiVersion; + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/account/UserInfo.java b/http-service/src/main/java/net/runelite/http/service/account/UserInfo.java new file mode 100644 index 0000000000..a1cde03f79 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/account/UserInfo.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2017, 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.service.account; + +public class UserInfo +{ + private String email; + + @Override + public String toString() + { + return "UserInfo{" + "email=" + email + '}'; + } + + public String getEmail() + { + return email; + } + + public void setEmail(String email) + { + this.email = email; + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/account/beans/SessionEntry.java b/http-service/src/main/java/net/runelite/http/service/account/beans/SessionEntry.java new file mode 100644 index 0000000000..ded67c7a45 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/account/beans/SessionEntry.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2017, 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.service.account.beans; + +import java.time.Instant; +import java.util.UUID; + +public class SessionEntry +{ + private int user; + private UUID uuid; + private Instant created; + private Instant lastUsed; + + public int getUser() + { + return user; + } + + public void setUser(int user) + { + this.user = user; + } + + public UUID getUuid() + { + return uuid; + } + + public void setUuid(UUID uuid) + { + this.uuid = uuid; + } + + public Instant getCreated() + { + return created; + } + + public void setCreated(Instant created) + { + this.created = created; + } + + public Instant getLastUsed() + { + return lastUsed; + } + + public void setLastUsed(Instant lastUsed) + { + this.lastUsed = lastUsed; + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/util/CacheControlFilter.java b/http-service/src/main/java/net/runelite/http/service/account/beans/UserEntry.java similarity index 55% rename from http-service/src/main/java/net/runelite/http/service/util/CacheControlFilter.java rename to http-service/src/main/java/net/runelite/http/service/account/beans/UserEntry.java index f0bd95e914..83dd4152ac 100644 --- a/http-service/src/main/java/net/runelite/http/service/util/CacheControlFilter.java +++ b/http-service/src/main/java/net/runelite/http/service/account/beans/UserEntry.java @@ -22,41 +22,36 @@ * (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.service.util; +package net.runelite.http.service.account.beans; -import org.springframework.core.MethodParameter; -import org.springframework.http.MediaType; -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.server.ServerHttpRequest; -import org.springframework.http.server.ServerHttpResponse; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; - -@ControllerAdvice -public class CacheControlFilter implements ResponseBodyAdvice +public class UserEntry { + private int id; + private String username; @Override - public boolean supports(MethodParameter returnType, Class> converterType) + public String toString() { - return true; + return "UserEntry{" + "id=" + id + ", username=" + username + '}'; } - @Override - public Object beforeBodyWrite( - Object body, - MethodParameter returnType, - MediaType selectedContentType, - Class> selectedConverterType, - ServerHttpRequest request, - ServerHttpResponse response - ) + public int getId() { - if (!response.getHeaders().containsKey("Cache-Control")) - { - response.getHeaders().add("Cache-Control", "no-cache, no-store, must-revalidate"); - response.getHeaders().add("pragma", "no-cache"); - } - return body; + return id; } -} \ No newline at end of file + + public void setId(int id) + { + this.id = id; + } + + public String getUsername() + { + return username; + } + + public void setUsername(String username) + { + this.username = username; + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/cache/CacheController.java b/http-service/src/main/java/net/runelite/http/service/cache/CacheController.java new file mode 100644 index 0000000000..f86f453412 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/cache/CacheController.java @@ -0,0 +1,362 @@ +/* + * Copyright (c) 2017, 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.service.cache; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; +import javax.imageio.ImageIO; +import lombok.extern.slf4j.Slf4j; +import net.runelite.cache.ConfigType; +import net.runelite.cache.IndexType; +import net.runelite.cache.definitions.ItemDefinition; +import net.runelite.cache.definitions.ModelDefinition; +import net.runelite.cache.definitions.NpcDefinition; +import net.runelite.cache.definitions.ObjectDefinition; +import net.runelite.cache.definitions.SpriteDefinition; +import net.runelite.cache.definitions.TextureDefinition; +import net.runelite.cache.definitions.loaders.ItemLoader; +import net.runelite.cache.definitions.loaders.ModelLoader; +import net.runelite.cache.definitions.loaders.NpcLoader; +import net.runelite.cache.definitions.loaders.ObjectLoader; +import net.runelite.cache.definitions.loaders.SpriteLoader; +import net.runelite.cache.definitions.loaders.TextureLoader; +import net.runelite.cache.definitions.providers.ItemProvider; +import net.runelite.cache.definitions.providers.ModelProvider; +import net.runelite.cache.definitions.providers.SpriteProvider; +import net.runelite.cache.definitions.providers.TextureProvider; +import net.runelite.cache.fs.ArchiveFiles; +import net.runelite.cache.fs.Container; +import net.runelite.cache.fs.FSFile; +import net.runelite.cache.item.ItemSpriteFactory; +import net.runelite.http.api.cache.Cache; +import net.runelite.http.api.cache.CacheArchive; +import net.runelite.http.api.cache.CacheIndex; +import net.runelite.http.service.cache.beans.ArchiveEntry; +import net.runelite.http.service.cache.beans.CacheEntry; +import net.runelite.http.service.cache.beans.IndexEntry; +import net.runelite.http.service.util.exception.NotFoundException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/cache") +@Slf4j +public class CacheController +{ + @Autowired + private CacheService cacheService; + + @GetMapping("/") + public List listCaches() + { + return cacheService.listCaches().stream() + .map(entry -> new Cache(entry.getId(), entry.getRevision(), entry.getDate())) + .collect(Collectors.toList()); + } + + @GetMapping("{cacheId}") + public List listIndexes(@PathVariable int cacheId) + { + CacheEntry cache = cacheService.findCache(cacheId); + if (cache == null) + { + throw new NotFoundException(); + } + + List indexes = cacheService.findIndexesForCache(cache); + + return indexes.stream() + .map(entry -> new CacheIndex(entry.getIndexId(), entry.getRevision())) + .collect(Collectors.toList()); + } + + @GetMapping("{cacheId}/{indexId}") + public List listArchives(@PathVariable int cacheId, + @PathVariable int indexId) + { + CacheEntry cache = cacheService.findCache(cacheId); + if (cache == null) + { + throw new NotFoundException(); + } + + IndexEntry indexEntry = cacheService.findIndexForCache(cache, indexId); + if (indexEntry == null) + { + throw new NotFoundException(); + } + + List archives = cacheService.findArchivesForIndex(indexEntry); + + return archives.stream() + .map(archive -> new CacheArchive(archive.getArchiveId(), archive.getNameHash(), archive.getRevision())) + .collect(Collectors.toList()); + } + + @GetMapping("{cacheId}/{indexId}/{archiveId}") + public CacheArchive getCacheArchive(@PathVariable int cacheId, + @PathVariable int indexId, + @PathVariable int archiveId) + { + CacheEntry cache = cacheService.findCache(cacheId); + if (cache == null) + { + throw new NotFoundException(); + } + + IndexEntry indexEntry = cacheService.findIndexForCache(cache, indexId); + if (indexEntry == null) + { + throw new NotFoundException(); + } + + ArchiveEntry archiveEntry = cacheService.findArchiveForIndex(indexEntry, archiveId); + if (archiveEntry == null) + { + throw new NotFoundException(); + } + + return new CacheArchive(archiveEntry.getArchiveId(), + archiveEntry.getNameHash(), archiveEntry.getRevision()); + } + + @GetMapping("{cacheId}/{indexId}/{archiveId}/data") + public byte[] getArchiveData( + @PathVariable int cacheId, + @PathVariable int indexId, + @PathVariable int archiveId + ) + { + CacheEntry cache = cacheService.findCache(cacheId); + if (cache == null) + { + throw new NotFoundException(); + } + + IndexEntry indexEntry = cacheService.findIndexForCache(cache, indexId); + if (indexEntry == null) + { + throw new NotFoundException(); + } + + ArchiveEntry archiveEntry = cacheService.findArchiveForIndex(indexEntry, archiveId); + if (archiveEntry == null) + { + throw new NotFoundException(); + } + + return cacheService.getArchive(archiveEntry); + } + + private ArchiveEntry findConfig(ConfigType config) + { + CacheEntry cache = cacheService.findMostRecent(); + if (cache == null) + { + throw new NotFoundException(); + } + + IndexEntry indexEntry = cacheService.findIndexForCache(cache, IndexType.CONFIGS.getNumber()); + if (indexEntry == null) + { + throw new NotFoundException(); + } + + ArchiveEntry archiveEntry = cacheService.findArchiveForIndex(indexEntry, config.getId()); + if (archiveEntry == null) + { + throw new NotFoundException(); + } + + return archiveEntry; + } + + @GetMapping("item/{itemId}") + public ItemDefinition getItem(@PathVariable int itemId) throws IOException + { + ArchiveEntry archiveEntry = findConfig(ConfigType.ITEM); + + ArchiveFiles archiveFiles = cacheService.getArchiveFiles(archiveEntry); + if (archiveFiles == null) + { + throw new NotFoundException(); + } + + FSFile file = archiveFiles.findFile(itemId); + if (file == null) + { + throw new NotFoundException(); + } + + ItemDefinition itemdef = new ItemLoader().load(itemId, file.getContents()); + return itemdef; + } + + @GetMapping(path = "item/{itemId}/image", produces = "image/png") + public ResponseEntity getItemImage( + @PathVariable int itemId, + @RequestParam(defaultValue = "1") int quantity, + @RequestParam(defaultValue = "1") int border, + @RequestParam(defaultValue = "3153952") int shadowColor + ) throws IOException + { + final CacheEntry cache = cacheService.findMostRecent(); + ItemProvider itemProvider = new ItemProvider() + { + @Override + public ItemDefinition provide(int itemId) + { + try + { + return getItem(itemId); + } + catch (IOException ex) + { + log.warn(null, ex); + return null; + } + } + }; + ModelProvider modelProvider = new ModelProvider() + { + @Override + public ModelDefinition provide(int modelId) throws IOException + { + IndexEntry indexEntry = cacheService.findIndexForCache(cache, IndexType.MODELS.getNumber()); + ArchiveEntry archiveEntry = cacheService.findArchiveForIndex(indexEntry, modelId); + byte[] archiveData = Container.decompress(cacheService.getArchive(archiveEntry), null).data; + return new ModelLoader().load(modelId, archiveData); + } + }; + SpriteProvider spriteProvider = new SpriteProvider() + { + @Override + public SpriteDefinition provide(int spriteId, int frameId) + { + try + { + IndexEntry indexEntry = cacheService.findIndexForCache(cache, IndexType.SPRITES.getNumber()); + ArchiveEntry archiveEntry = cacheService.findArchiveForIndex(indexEntry, spriteId); + byte[] archiveData = Container.decompress(cacheService.getArchive(archiveEntry), null).data; + SpriteDefinition[] defs = new SpriteLoader().load(spriteId, archiveData); + return defs[frameId]; + } + catch (Exception ex) + { + log.warn(null, ex); + return null; + } + } + }; + + TextureProvider textureProvider2 = new TextureProvider() + { + @Override + public TextureDefinition[] provide() + { + try + { + IndexEntry indexEntry = cacheService.findIndexForCache(cache, IndexType.TEXTURES.getNumber()); + ArchiveEntry archiveEntry = cacheService.findArchiveForIndex(indexEntry, 0); + ArchiveFiles archiveFiles = cacheService.getArchiveFiles(archiveEntry); + TextureLoader loader = new TextureLoader(); + TextureDefinition[] defs = new TextureDefinition[archiveFiles.getFiles().size()]; + int i = 0; + for (FSFile file : archiveFiles.getFiles()) + { + TextureDefinition def = loader.load(file.getFileId(), file.getContents()); + defs[i++] = def; + } + return defs; + } + catch (Exception ex) + { + log.warn(null, ex); + return null; + } + } + }; + + BufferedImage itemImage = ItemSpriteFactory.createSprite(itemProvider, modelProvider, spriteProvider, textureProvider2, + itemId, quantity, border, shadowColor, false); + ByteArrayOutputStream bao = new ByteArrayOutputStream(); + ImageIO.write(itemImage, "png", bao); + return ResponseEntity.ok(bao.toByteArray()); + } + + @GetMapping("object/{objectId}") + public ObjectDefinition getObject( + @PathVariable int objectId + ) throws IOException + { + ArchiveEntry archiveEntry = findConfig(ConfigType.OBJECT); + + ArchiveFiles archiveFiles = cacheService.getArchiveFiles(archiveEntry); + if (archiveFiles == null) + { + throw new NotFoundException(); + } + + FSFile file = archiveFiles.findFile(objectId); + if (file == null) + { + throw new NotFoundException(); + } + + ObjectDefinition objectdef = new ObjectLoader().load(objectId, file.getContents()); + return objectdef; + } + + @GetMapping("npc/{npcId}") + public NpcDefinition getNpc( + @PathVariable int npcId + ) throws IOException + { + ArchiveEntry archiveEntry = findConfig(ConfigType.NPC); + + ArchiveFiles archiveFiles = cacheService.getArchiveFiles(archiveEntry); + if (archiveFiles == null) + { + throw new NotFoundException(); + } + + FSFile file = archiveFiles.findFile(npcId); + if (file == null) + { + throw new NotFoundException(); + } + + NpcDefinition npcdef = new NpcLoader().load(npcId, file.getContents()); + return npcdef; + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/cache/CacheDAO.java b/http-service/src/main/java/net/runelite/http/service/cache/CacheDAO.java new file mode 100644 index 0000000000..08282ca38e --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/cache/CacheDAO.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2017, 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.service.cache; + +import java.util.List; +import net.runelite.cache.IndexType; +import net.runelite.http.service.cache.beans.ArchiveEntry; +import net.runelite.http.service.cache.beans.CacheEntry; +import net.runelite.http.service.cache.beans.FileEntry; +import net.runelite.http.service.cache.beans.IndexEntry; +import org.sql2o.Connection; +import org.sql2o.Query; +import org.sql2o.ResultSetIterable; + +class CacheDAO +{ + public List listCaches(Connection con) + { + return con.createQuery("select id, revision, date from cache") + .executeAndFetch(CacheEntry.class); + } + + public CacheEntry findMostRecent(Connection con) + { + return con.createQuery("select id, revision, date from cache order by revision desc, date desc limit 1") + .executeAndFetchFirst(CacheEntry.class); + } + + public List findIndexesForCache(Connection con, CacheEntry cache) + { + return con.createQuery("select id, indexId, crc, revision from `index` where cache = :cache") + .addParameter("cache", cache.getId()) + .executeAndFetch(IndexEntry.class); + } + + public IndexEntry findIndexForCache(Connection con, CacheEntry cache, int indexId) + { + return con.createQuery("select id, indexId, crc, revision from `index` " + + "where cache = :id " + + "and indexId = :indexId") + .addParameter("id", cache.getId()) + .addParameter("indexId", indexId) + .executeAndFetchFirst(IndexEntry.class); + } + + public ResultSetIterable findArchivesForIndex(Connection con, IndexEntry indexEntry) + { + return con.createQuery("select archive.id, archive.archiveId, archive.nameHash," + + " archive.crc, archive.revision, archive.hash from index_archive " + + "join archive on index_archive.archive = archive.id " + + "where index_archive.index = :id") + .addParameter("id", indexEntry.getId()) + .executeAndFetchLazy(ArchiveEntry.class); + } + + public ArchiveEntry findArchiveForIndex(Connection con, IndexEntry indexEntry, int archiveId) + { + return con.createQuery("select archive.id, archive.archiveId, archive.nameHash," + + " archive.crc, archive.revision, archive.hash from index_archive " + + "join archive on index_archive.archive = archive.id " + + "where index_archive.index = :id " + + "and archive.archiveId = :archiveId") + .addParameter("id", indexEntry.getId()) + .addParameter("archiveId", archiveId) + .executeAndFetchFirst(ArchiveEntry.class); + } + + public ArchiveEntry findArchiveByName(Connection con, CacheEntry cache, IndexType index, int nameHash) + { + return con.createQuery("select archive.id, archive.archiveId, archive.nameHash," + + " archive.crc, archive.revision, archive.hash from archive " + + "join index_archive on index_archive.archive = archive.id " + + "join `index` on index.id = index_archive.index " + + "where index.cache = :cacheId " + + "and index.indexId = :indexId " + + "and archive.nameHash = :nameHash " + + "limit 1") + .addParameter("cacheId", cache.getId()) + .addParameter("indexId", index.getNumber()) + .addParameter("nameHash", nameHash) + .executeAndFetchFirst(ArchiveEntry.class); + } + + public ResultSetIterable findFilesForArchive(Connection con, ArchiveEntry archiveEntry) + { + Query findFilesForArchive = con.createQuery("select id, fileId, nameHash from file " + + "where archive = :archive"); + + return findFilesForArchive + .addParameter("archive", archiveEntry.getId()) + .executeAndFetchLazy(FileEntry.class); + } + + public CacheEntry findCache(Connection con, int cacheId) + { + return con.createQuery("select id, revision, date from cache " + + "where id = :cacheId") + .addParameter("cacheId", cacheId) + .executeAndFetchFirst(CacheEntry.class); + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/cache/CacheService.java b/http-service/src/main/java/net/runelite/http/service/cache/CacheService.java new file mode 100644 index 0000000000..c0b767a68c --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/cache/CacheService.java @@ -0,0 +1,254 @@ +/* + * Copyright (c) 2017-2018, 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.service.cache; + +import com.google.common.collect.Iterables; +import com.google.common.io.BaseEncoding; +import com.google.common.io.ByteStreams; +import io.minio.MinioClient; +import io.minio.errors.ErrorResponseException; +import io.minio.errors.InsufficientDataException; +import io.minio.errors.InternalException; +import io.minio.errors.InvalidArgumentException; +import io.minio.errors.InvalidBucketNameException; +import io.minio.errors.InvalidEndpointException; +import io.minio.errors.InvalidPortException; +import io.minio.errors.NoResponseException; +import java.io.IOException; +import java.io.InputStream; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import net.runelite.cache.ConfigType; +import net.runelite.cache.IndexType; +import net.runelite.cache.definitions.ItemDefinition; +import net.runelite.cache.definitions.loaders.ItemLoader; +import net.runelite.cache.fs.ArchiveFiles; +import net.runelite.cache.fs.Container; +import net.runelite.cache.fs.FSFile; +import net.runelite.http.service.cache.beans.ArchiveEntry; +import net.runelite.http.service.cache.beans.CacheEntry; +import net.runelite.http.service.cache.beans.FileEntry; +import net.runelite.http.service.cache.beans.IndexEntry; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Service; +import org.sql2o.Connection; +import org.sql2o.ResultSetIterable; +import org.sql2o.Sql2o; +import org.xmlpull.v1.XmlPullParserException; + +@Service +@Slf4j +public class CacheService +{ + @Autowired + @Qualifier("Runelite Cache SQL2O") + private Sql2o sql2o; + + @Value("${minio.bucket}") + private String minioBucket; + + private final MinioClient minioClient; + + @Autowired + public CacheService( + @Value("${minio.endpoint}") String minioEndpoint, + @Value("${minio.accesskey}") String accessKey, + @Value("${minio.secretkey}") String secretKey + ) throws InvalidEndpointException, InvalidPortException + { + this.minioClient = new MinioClient(minioEndpoint, accessKey, secretKey); + } + + @Bean + public MinioClient minioClient() + { + return minioClient; + } + + /** + * retrieve archive from storage + * + * @param archiveEntry + * @return + */ + public byte[] getArchive(ArchiveEntry archiveEntry) + { + String hashStr = BaseEncoding.base16().encode(archiveEntry.getHash()); + String path = new StringBuilder() + .append(hashStr, 0, 2) + .append('/') + .append(hashStr.substring(2)) + .toString(); + + try (InputStream in = minioClient.getObject(minioBucket, path)) + { + return ByteStreams.toByteArray(in); + } + catch (InvalidBucketNameException | NoSuchAlgorithmException | InsufficientDataException + | IOException | InvalidKeyException | NoResponseException | XmlPullParserException + | ErrorResponseException | InternalException | InvalidArgumentException ex) + { + log.warn(null, ex); + return null; + } + } + + public ArchiveFiles getArchiveFiles(ArchiveEntry archiveEntry) throws IOException + { + CacheDAO cacheDao = new CacheDAO(); + + try (Connection con = sql2o.open(); + ResultSetIterable files = cacheDao.findFilesForArchive(con, archiveEntry)) + { + byte[] archiveData = getArchive(archiveEntry); + + if (archiveData == null) + { + return null; + } + + Container result = Container.decompress(archiveData, null); + if (result == null) + { + return null; + } + + byte[] decompressedData = result.data; + + ArchiveFiles archiveFiles = new ArchiveFiles(); + for (FileEntry fileEntry : files) + { + FSFile file = new FSFile(fileEntry.getFileId()); + archiveFiles.addFile(file); + file.setNameHash(fileEntry.getNameHash()); + } + archiveFiles.loadContents(decompressedData); + return archiveFiles; + } + } + + public List listCaches() + { + try (Connection con = sql2o.open()) + { + CacheDAO cacheDao = new CacheDAO(); + return cacheDao.listCaches(con); + } + } + + public CacheEntry findCache(int cacheId) + { + try (Connection con = sql2o.open()) + { + CacheDAO cacheDao = new CacheDAO(); + return cacheDao.findCache(con, cacheId); + } + } + + public CacheEntry findMostRecent() + { + try (Connection con = sql2o.open()) + { + CacheDAO cacheDao = new CacheDAO(); + return cacheDao.findMostRecent(con); + } + } + + public List findIndexesForCache(CacheEntry cacheEntry) + { + try (Connection con = sql2o.open()) + { + CacheDAO cacheDao = new CacheDAO(); + return cacheDao.findIndexesForCache(con, cacheEntry); + } + } + + public IndexEntry findIndexForCache(CacheEntry cahceEntry, int indexId) + { + try (Connection con = sql2o.open()) + { + CacheDAO cacheDao = new CacheDAO(); + return cacheDao.findIndexForCache(con, cahceEntry, indexId); + } + } + + public List findArchivesForIndex(IndexEntry indexEntry) + { + try (Connection con = sql2o.open()) + { + CacheDAO cacheDao = new CacheDAO(); + ResultSetIterable archiveEntries = cacheDao.findArchivesForIndex(con, indexEntry); + List archives = new ArrayList<>(); + Iterables.addAll(archives, archiveEntries); + return archives; + } + } + + public ArchiveEntry findArchiveForIndex(IndexEntry indexEntry, int archiveId) + { + try (Connection con = sql2o.open()) + { + CacheDAO cacheDao = new CacheDAO(); + return cacheDao.findArchiveForIndex(con, indexEntry, archiveId); + } + } + + public ArchiveEntry findArchiveForTypeAndName(CacheEntry cache, IndexType index, int nameHash) + { + try (Connection con = sql2o.open()) + { + CacheDAO cacheDao = new CacheDAO(); + return cacheDao.findArchiveByName(con, cache, index, nameHash); + } + } + + public List getItems() throws IOException + { + CacheEntry cache = findMostRecent(); + if (cache == null) + { + return Collections.emptyList(); + } + + IndexEntry indexEntry = findIndexForCache(cache, IndexType.CONFIGS.getNumber()); + ArchiveEntry archiveEntry = findArchiveForIndex(indexEntry, ConfigType.ITEM.getId()); + ArchiveFiles archiveFiles = getArchiveFiles(archiveEntry); + final ItemLoader itemLoader = new ItemLoader(); + final List result = new ArrayList<>(archiveFiles.getFiles().size()); + for (FSFile file : archiveFiles.getFiles()) + { + ItemDefinition itemDef = itemLoader.load(file.getFileId(), file.getContents()); + result.add(itemDef); + } + return result; + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/cache/beans/ArchiveEntry.java b/http-service/src/main/java/net/runelite/http/service/cache/beans/ArchiveEntry.java new file mode 100644 index 0000000000..acda96e77f --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/cache/beans/ArchiveEntry.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2017, 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.service.cache.beans; + +import lombok.Data; + +@Data +public class ArchiveEntry +{ + private int id; + private int archiveId; + private int nameHash; + private int crc; + private int revision; + private byte[] hash; +} diff --git a/http-service/src/main/java/net/runelite/http/service/cache/beans/CacheEntry.java b/http-service/src/main/java/net/runelite/http/service/cache/beans/CacheEntry.java new file mode 100644 index 0000000000..231ad7c655 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/cache/beans/CacheEntry.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2017, 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.service.cache.beans; + +import java.time.Instant; +import lombok.Data; + +@Data +public class CacheEntry +{ + private int id; + private int revision; + private Instant date; +} diff --git a/http-service/src/main/java/net/runelite/http/service/cache/beans/FileEntry.java b/http-service/src/main/java/net/runelite/http/service/cache/beans/FileEntry.java new file mode 100644 index 0000000000..c5f35a4cc3 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/cache/beans/FileEntry.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2017, 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.service.cache.beans; + +import lombok.Data; + +@Data +public class FileEntry +{ + private int id; + private int archiveId; + private int fileId; + private int nameHash; +} diff --git a/http-service/src/main/java/net/runelite/http/service/cache/beans/IndexEntry.java b/http-service/src/main/java/net/runelite/http/service/cache/beans/IndexEntry.java new file mode 100644 index 0000000000..8d60927c71 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/cache/beans/IndexEntry.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2017, 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.service.cache.beans; + +import lombok.Data; + +@Data +public class IndexEntry +{ + private int id; + private int indexId; + private int crc; + private int revision; +} 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 df287e6512..7b5132e47a 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 @@ -24,9 +24,13 @@ */ 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 com.google.common.base.Strings; -import net.runelite.http.api.chat.House; +import net.runelite.http.api.chat.Duels; +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; @@ -42,62 +46,166 @@ 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) + .build(); + @Autowired private ChatService chatService; - @PostMapping("/layout") - public void submitLayout(@RequestParam String name, @RequestParam String layout) + @PostMapping("/kc") + public void submitKc(@RequestParam String name, @RequestParam String boss, @RequestParam int kc) { - if (Strings.isNullOrEmpty(layout)) + if (kc <= 0) { return; } - chatService.setLayout(name, layout); + chatService.setKc(name, boss, kc); + killCountCache.put(new KillCountKey(name, boss), kc); } - @GetMapping("/layout") - public String getLayout(@RequestParam String name) + @GetMapping("/kc") + public int getKc(@RequestParam String name, @RequestParam String boss) { - String layout = chatService.getLayout(name); - if (layout == null) + Integer kc = killCountCache.getIfPresent(new KillCountKey(name, boss)); + if (kc == null) + { + kc = chatService.getKc(name, boss); + if (kc != null) + { + killCountCache.put(new KillCountKey(name, boss), kc); + } + } + + if (kc == null) { throw new NotFoundException(); } - return layout; + return kc; } - @PostMapping("/hosts") - public void submitHost(@RequestParam int world, @RequestParam String location, @RequestParam String owner, @RequestParam boolean guildedAltar, @RequestParam boolean occultAltar, @RequestParam boolean spiritTree, @RequestParam boolean fairyRing, @RequestParam boolean wildernessObelisk, @RequestParam boolean repairStand, @RequestParam boolean combatDummy, @RequestParam(required = false, defaultValue = "false") boolean remove) + @PostMapping("/qp") + public void submitQp(@RequestParam String name, @RequestParam int qp) { - if (!location.equals("Rimmington") && !location.equals("Yanille")) + if (qp < 0) { return; } - House house = new House(); - house.setOwner(owner); - house.setGuildedAltarPresent(guildedAltar); - house.setOccultAltarPresent(occultAltar); - house.setSpiritTreePresent(spiritTree); - house.setFairyRingPresent(fairyRing); - house.setWildernessObeliskPresent(wildernessObelisk); - house.setRepairStandPresent(repairStand); - house.setCombatDummyPresent(combatDummy); - - if (remove) - { - chatService.removeHost(world, location, house); - } - else - { - chatService.addHost(world, location, house); - } + chatService.setQp(name, qp); } - @GetMapping("/hosts") - public House[] getHosts(@RequestParam int world, @RequestParam String location) + @GetMapping("/qp") + public int getQp(@RequestParam String name) { - return chatService.getHosts(world, location); + Integer kc = chatService.getQp(name); + if (kc == null) + { + throw new NotFoundException(); + } + return kc; } -} \ No newline at end of file + + @PostMapping("/gc") + public void submitGc(@RequestParam String name, @RequestParam int gc) + { + if (gc < 0) + { + return; + } + + chatService.setGc(name, gc); + } + + @GetMapping("/gc") + public int getKc(@RequestParam String name) + { + Integer gc = chatService.getGc(name); + if (gc == null) + { + throw new NotFoundException(); + } + return gc; + } + + @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); + } + + @PostMapping("/pb") + public void submitPb(@RequestParam String name, @RequestParam String boss, @RequestParam int pb) + { + if (pb < 0) + { + return; + } + + chatService.setPb(name, boss, pb); + } + + @GetMapping("/pb") + public int getPb(@RequestParam String name, @RequestParam String boss) + { + Integer pb = chatService.getPb(name, boss); + if (pb == null) + { + throw new NotFoundException(); + } + return pb; + } + + @PostMapping("/duels") + public void submitDuels(@RequestParam String name, @RequestParam int wins, + @RequestParam int losses, + @RequestParam int winningStreak, @RequestParam int losingStreak) + { + if (wins < 0 || losses < 0 || winningStreak < 0 || losingStreak < 0) + { + return; + } + + Duels duels = new Duels(); + duels.setWins(wins); + duels.setLosses(losses); + duels.setWinningStreak(winningStreak); + duels.setLosingStreak(losingStreak); + + chatService.setDuels(name, duels); + } + + @GetMapping("/duels") + public Duels getDuels(@RequestParam String name) + { + Duels duels = chatService.getDuels(name); + if (duels == null) + { + throw new NotFoundException(); + } + return duels; + } +} 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 aa23d3f18d..0b0762d3ea 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,11 +24,11 @@ */ package net.runelite.http.service.chat; +import com.google.common.collect.ImmutableMap; import java.time.Duration; -import java.util.List; -import net.runelite.http.api.chat.ChatClient; -import net.runelite.http.api.RuneLiteAPI; -import net.runelite.http.api.chat.House; +import java.util.Map; +import net.runelite.http.api.chat.Task; +import net.runelite.http.api.chat.Duels; import net.runelite.http.service.util.redis.RedisPool; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -40,8 +40,6 @@ public class ChatService private static final Duration EXPIRE = Duration.ofMinutes(2); private final RedisPool jedisPool; - private final ChatClient chatClient = new ChatClient(); - @Autowired public ChatService(RedisPool jedisPool) @@ -49,72 +47,155 @@ public class ChatService this.jedisPool = jedisPool; } - public String getLayout(String name) + public Integer getKc(String name, String boss) { String value; try (Jedis jedis = jedisPool.getResource()) { - value = jedis.get("layout." + name); + value = jedis.get("kc." + name + "." + boss); } - return value; + return value == null ? null : Integer.parseInt(value); } - public void setLayout(String name, String layout) + public void setKc(String name, String boss, int kc) { - if (!chatClient.testLayout(layout)) - { - throw new IllegalArgumentException(layout); - } - try (Jedis jedis = jedisPool.getResource()) { - jedis.setex("layout." + name, (int) EXPIRE.getSeconds(), layout); + jedis.setex("kc." + name + "." + boss, (int) EXPIRE.getSeconds(), Integer.toString(kc)); } } - public void addHost(int world, String location, House house) + public Integer getQp(String name) { - String houseJSON = house.toString(); - - String key = "hosts.w" + Integer.toString(world) + "." + location; - + String value; try (Jedis jedis = jedisPool.getResource()) { - jedis.rpush(key, houseJSON); + value = jedis.get("qp." + name); + } + return value == null ? null : Integer.parseInt(value); + } + + public void setQp(String name, int qp) + { + try (Jedis jedis = jedisPool.getResource()) + { + jedis.setex("qp." + name, (int) EXPIRE.getSeconds(), Integer.toString(qp)); } } - public House[] getHosts(int world, String location) + public Task getTask(String name) { - List json; - String key = "hosts.w" + Integer.toString(world) + "." + location; + Map map; try (Jedis jedis = jedisPool.getResource()) { - json = jedis.lrange(key, 0, 25); + map = jedis.hgetAll("task." + name); } - if (json.isEmpty()) + if (map.isEmpty()) { return null; } - House[] hosts = new House[json.size()]; - for (int i = 0; i < json.size(); i++) - { - hosts[i] = RuneLiteAPI.GSON.fromJson(json.get(i), House.class); - } - return hosts; + 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 removeHost(int world, String location, House house) + public void setTask(String name, Task task) { - String json = house.toString(); - String key = "hosts.w" + Integer.toString(world) + "." + location; + 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.lrem(key, 0, json); + jedis.hmset(key, taskMap); + jedis.expire(key, (int) EXPIRE.getSeconds()); } } -} \ No newline at end of file + + public Integer getPb(String name, String boss) + { + String value; + try (Jedis jedis = jedisPool.getResource()) + { + value = jedis.get("pb." + boss + "." + name); + } + return value == null ? null : Integer.parseInt(value); + } + + public void setPb(String name, String boss, int pb) + { + try (Jedis jedis = jedisPool.getResource()) + { + jedis.setex("pb." + boss + "." + name, (int) EXPIRE.getSeconds(), Integer.toString(pb)); + } + } + + public Integer getGc(String name) + { + String value; + try (Jedis jedis = jedisPool.getResource()) + { + value = jedis.get("gc." + name); + } + return value == null ? null : Integer.parseInt(value); + } + + public void setGc(String name, int gc) + { + try (Jedis jedis = jedisPool.getResource()) + { + jedis.setex("gc." + name, (int) EXPIRE.getSeconds(), Integer.toString(gc)); + } + } + + public Duels getDuels(String name) + { + Map map; + + try (Jedis jedis = jedisPool.getResource()) + { + map = jedis.hgetAll("duels." + name); + } + + if (map.isEmpty()) + { + return null; + } + + Duels duels = new Duels(); + duels.setWins(Integer.parseInt(map.get("wins"))); + duels.setLosses(Integer.parseInt(map.get("losses"))); + duels.setWinningStreak(Integer.parseInt(map.get("winningStreak"))); + duels.setLosingStreak(Integer.parseInt(map.get("losingStreak"))); + return duels; + } + + public void setDuels(String name, Duels duels) + { + Map duelsMap = ImmutableMap.builderWithExpectedSize(4) + .put("wins", Integer.toString(duels.getWins())) + .put("losses", Integer.toString(duels.getLosses())) + .put("winningStreak", Integer.toString(duels.getWinningStreak())) + .put("losingStreak", Integer.toString(duels.getLosingStreak())) + .build(); + + String key = "duels." + name; + + try (Jedis jedis = jedisPool.getResource()) + { + jedis.hmset(key, duelsMap); + jedis.expire(key, (int) EXPIRE.getSeconds()); + } + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/chat/KillCountKey.java b/http-service/src/main/java/net/runelite/http/service/chat/KillCountKey.java new file mode 100644 index 0000000000..07ca775dad --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/chat/KillCountKey.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2018, 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.service.chat; + +import lombok.Value; + +@Value +class KillCountKey +{ + private String username; + private String boss; +} diff --git a/http-service/src/main/java/net/runelite/http/service/config/ConfigController.java b/http-service/src/main/java/net/runelite/http/service/config/ConfigController.java new file mode 100644 index 0000000000..51d3f485f0 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/config/ConfigController.java @@ -0,0 +1,109 @@ +/* + * 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.service.config; + +import java.io.IOException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import net.runelite.http.api.config.Configuration; +import net.runelite.http.service.account.AuthFilter; +import net.runelite.http.service.account.beans.SessionEntry; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import static org.springframework.web.bind.annotation.RequestMethod.DELETE; +import static org.springframework.web.bind.annotation.RequestMethod.PUT; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/config") +public class ConfigController +{ + private final ConfigService configService; + private final AuthFilter authFilter; + + @Autowired + public ConfigController(ConfigService configService, AuthFilter authFilter) + { + this.configService = configService; + this.authFilter = authFilter; + } + + @GetMapping + public Configuration get(HttpServletRequest request, HttpServletResponse response) throws IOException + { + SessionEntry session = authFilter.handle(request, response); + + if (session == null) + { + return null; + } + + return configService.get(session.getUser()); + } + + @RequestMapping(path = "/{key:.+}", method = PUT) + public void setKey( + HttpServletRequest request, + HttpServletResponse response, + @PathVariable String key, + @RequestBody(required = false) String value + ) throws IOException + { + SessionEntry session = authFilter.handle(request, response); + + if (session == null) + { + return; + } + + if (!configService.setKey(session.getUser(), key, value)) + { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + + @RequestMapping(path = "/{key:.+}", method = DELETE) + public void unsetKey( + HttpServletRequest request, + HttpServletResponse response, + @PathVariable String key + ) throws IOException + { + SessionEntry session = authFilter.handle(request, response); + + if (session == null) + { + return; + } + + if (!configService.unsetKey(session.getUser(), key)) + { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/config/ConfigService.java b/http-service/src/main/java/net/runelite/http/service/config/ConfigService.java new file mode 100644 index 0000000000..bbe8557d88 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/config/ConfigService.java @@ -0,0 +1,287 @@ +/* + * Copyright (c) 2017-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.service.config; + +import com.google.common.annotations.VisibleForTesting; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSyntaxException; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import static com.mongodb.client.model.Filters.eq; +import com.mongodb.client.model.IndexOptions; +import com.mongodb.client.model.Indexes; +import com.mongodb.client.model.UpdateOptions; +import static com.mongodb.client.model.Updates.set; +import static com.mongodb.client.model.Updates.unset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; +import net.runelite.http.api.RuneLiteAPI; +import net.runelite.http.api.config.ConfigEntry; +import net.runelite.http.api.config.Configuration; +import org.bson.Document; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class ConfigService +{ + private static final int MAX_DEPTH = 8; + private static final int MAX_VALUE_LENGTH = 262144; + + private final Gson GSON = RuneLiteAPI.GSON; + private final UpdateOptions upsertUpdateOptions = new UpdateOptions().upsert(true); + + private final MongoCollection mongoCollection; + + @Autowired + public ConfigService( + MongoClient mongoClient + ) + { + + MongoDatabase database = mongoClient.getDatabase("config"); + MongoCollection collection = database.getCollection("config"); + this.mongoCollection = collection; + + // Create unique index on _userId + IndexOptions indexOptions = new IndexOptions().unique(true); + collection.createIndex(Indexes.ascending("_userId"), indexOptions); + } + + private Document getConfig(int userId) + { + return mongoCollection.find(eq("_userId", userId)).first(); + } + + public Configuration get(int userId) + { + Map configMap = getConfig(userId); + + if (configMap == null || configMap.isEmpty()) + { + return new Configuration(Collections.emptyList()); + } + + List config = new ArrayList<>(); + + for (String group : configMap.keySet()) + { + // Reserved keys + if (group.startsWith("_") || group.startsWith("$")) + { + continue; + } + + Map groupMap = (Map) configMap.get(group); + + for (Map.Entry entry : groupMap.entrySet()) + { + String key = entry.getKey(); + Object value = entry.getValue(); + + if (value instanceof Map || value instanceof Collection) + { + value = GSON.toJson(entry.getValue()); + } + else if (value == null) + { + continue; + } + + ConfigEntry configEntry = new ConfigEntry(); + configEntry.setKey(group + "." + key.replace(':', '.')); + configEntry.setValue(value.toString()); + config.add(configEntry); + } + } + + return new Configuration(config); + } + + public boolean setKey( + int userId, + String key, + @Nullable String value + ) + { + if (key.startsWith("$") || key.startsWith("_")) + { + return false; + } + + String[] split = key.split("\\.", 2); + if (split.length != 2) + { + return false; + } + + if (!validateJson(value)) + { + return false; + } + + Object jsonValue = parseJsonString(value); + mongoCollection.updateOne(eq("_userId", userId), + set(split[0] + "." + split[1].replace('.', ':'), jsonValue), + upsertUpdateOptions); + return true; + } + + public boolean unsetKey( + int userId, + String key + ) + { + if (key.startsWith("$") || key.startsWith("_")) + { + return false; + } + + String[] split = key.split("\\.", 2); + if (split.length != 2) + { + return false; + } + + mongoCollection.updateOne(eq("_userId", userId), + unset(split[0] + "." + split[1].replace('.', ':'))); + return true; + } + + @VisibleForTesting + static Object parseJsonString(String value) + { + Object jsonValue; + try + { + jsonValue = RuneLiteAPI.GSON.fromJson(value, Object.class); + + if (jsonValue instanceof Double || jsonValue instanceof Float) + { + Number number = (Number) jsonValue; + if (Math.floor(number.doubleValue()) == number.doubleValue() && !Double.isInfinite(number.doubleValue())) + { + // value is an int or long. 'number' might be truncated so parse it from 'value' + try + { + jsonValue = Integer.parseInt(value); + } + catch (NumberFormatException ex) + { + try + { + jsonValue = Long.parseLong(value); + } + catch (NumberFormatException ex2) + { + + } + } + } + } + } + catch (JsonSyntaxException ex) + { + jsonValue = value; + } + return jsonValue; + } + + @VisibleForTesting + static boolean validateJson(String value) + { + try + { + // I couldn't figure out a better way to do this than a second json parse + JsonElement jsonElement = RuneLiteAPI.GSON.fromJson(value, JsonElement.class); + return validateObject(jsonElement, 1); + } + catch (JsonSyntaxException ex) + { + // the client submits the string representation of objects which is not always valid json, + // eg. a value with a ':' in it. We just ignore it now. We can't json encode the values client + // side due to them already being strings, which prevents gson from being able to convert them + // to ints/floats/maps etc. + return value.length() < MAX_VALUE_LENGTH; + } + } + + private static boolean validateObject(JsonElement jsonElement, int depth) + { + if (depth >= MAX_DEPTH) + { + return false; + } + + if (jsonElement.isJsonObject()) + { + JsonObject jsonObject = jsonElement.getAsJsonObject(); + + for (Map.Entry entry : jsonObject.entrySet()) + { + JsonElement element = entry.getValue(); + + if (!validateObject(element, depth + 1)) + { + return false; + } + } + } + else if (jsonElement.isJsonArray()) + { + JsonArray jsonArray = jsonElement.getAsJsonArray(); + + for (int i = 0; i < jsonArray.size(); ++i) + { + JsonElement element = jsonArray.get(i); + + if (!validateObject(element, depth + 1)) + { + return false; + } + } + } + else if (jsonElement.isJsonPrimitive()) + { + JsonPrimitive jsonPrimitive = jsonElement.getAsJsonPrimitive(); + String value = jsonPrimitive.getAsString(); + if (value.length() >= MAX_VALUE_LENGTH) + { + return false; + } + } + + return true; + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/examine/ExamineController.java b/http-service/src/main/java/net/runelite/http/service/examine/ExamineController.java new file mode 100644 index 0000000000..b7e079497f --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/examine/ExamineController.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2017, 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.service.examine; + +import static net.runelite.http.service.examine.ExamineType.ITEM; +import static net.runelite.http.service.examine.ExamineType.NPC; +import static net.runelite.http.service.examine.ExamineType.OBJECT; +import net.runelite.http.service.item.ItemEntry; +import net.runelite.http.service.item.ItemService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import static org.springframework.web.bind.annotation.RequestMethod.POST; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/examine") +public class ExamineController +{ + private final ExamineService examineService; + private final ItemService itemService; + + @Autowired + public ExamineController(ExamineService examineService, ItemService itemService) + { + this.examineService = examineService; + this.itemService = itemService; + } + + @GetMapping("/npc/{id}") + public String getNpc(@PathVariable int id) + { + return examineService.get(NPC, id); + } + + @GetMapping("/object/{id}") + public String getObject(@PathVariable int id) + { + return examineService.get(OBJECT, id); + } + + @GetMapping("/item/{id}") + public String getItem(@PathVariable int id) + { + // Tradeable item examine info is available from the Jagex item API + ItemEntry item = itemService.getItem(id); + if (item != null) + { + return item.getDescription(); + } + + return examineService.get(ITEM, id); + } + + @RequestMapping(path = "/npc/{id}", method = POST) + public void submitNpc(@PathVariable int id, @RequestBody String examine) + { + examineService.insert(NPC, id, examine); + } + + @RequestMapping(path = "/object/{id}", method = POST) + public void submitObject(@PathVariable int id, @RequestBody String examine) + { + examineService.insert(OBJECT, id, examine); + } + + @RequestMapping(path = "/item/{id}", method = POST) + public void submitItem(@PathVariable int id, @RequestBody String examine) + { + examineService.insert(ITEM, id, examine); + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/examine/ExamineEntry.java b/http-service/src/main/java/net/runelite/http/service/examine/ExamineEntry.java new file mode 100644 index 0000000000..5222ef3059 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/examine/ExamineEntry.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2017, 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.service.examine; + +import java.time.Instant; + +public class ExamineEntry +{ + private ExamineType type; + private int id; + private Instant time; + private int count; + private String text; + + public ExamineType getType() + { + return type; + } + + public void setType(ExamineType type) + { + this.type = type; + } + + public int getId() + { + return id; + } + + public void setId(int id) + { + this.id = id; + } + + public Instant getTime() + { + return time; + } + + public void setTime(Instant time) + { + this.time = time; + } + + public int getCount() + { + return count; + } + + public void setCount(int count) + { + this.count = count; + } + + public String getText() + { + return text; + } + + public void setText(String text) + { + this.text = text; + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/examine/ExamineService.java b/http-service/src/main/java/net/runelite/http/service/examine/ExamineService.java new file mode 100644 index 0000000000..c7fa265eea --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/examine/ExamineService.java @@ -0,0 +1,94 @@ +/* + * 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.service.examine; + +import java.sql.Timestamp; +import java.time.Instant; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; +import org.sql2o.Connection; +import org.sql2o.Sql2o; + +@Service +public class ExamineService +{ + private static final String CREATE_EXAMINE = "CREATE TABLE IF NOT EXISTS `examine` (\n" + + " `type` enum('OBJECT','NPC','ITEM') NOT NULL,\n" + + " `id` int(11) NOT NULL,\n" + + " `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n" + + " `count` int(11) NOT NULL,\n" + + " `text` tinytext NOT NULL,\n" + + " UNIQUE KEY `type` (`type`,`id`,`text`(64))\n" + + ") ENGINE=InnoDB"; + + private final Sql2o sql2o; + + @Autowired + public ExamineService(@Qualifier("Runelite SQL2O") Sql2o sql2o) + { + this.sql2o = sql2o; + + try (Connection con = sql2o.open()) + { + con.createQuery(CREATE_EXAMINE) + .executeUpdate(); + } + } + + public String get(ExamineType type, int id) + { + try (Connection con = sql2o.open()) + { + ExamineEntry entry = con.createQuery("select text from examine where type = :type and id = :id " + + "order by count desc limit 1") + .addParameter("type", type.toString()) + .addParameter("id", id) + .executeAndFetchFirst(ExamineEntry.class); + + if (entry != null) + { + return entry.getText(); + } + } + + return null; + } + + public void insert(ExamineType type, int id, String examine) + { + try (Connection con = sql2o.open()) + { + con.createQuery("insert into examine (type, id, time, count, text) values " + + "(:type, :id, :time, :count, :text) on duplicate key update count = count + 1") + .addParameter("type", type.toString()) + .addParameter("id", id) + .addParameter("time", Timestamp.from(Instant.now())) + .addParameter("count", 1) + .addParameter("text", examine) + .executeUpdate(); + } + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/examine/ExamineType.java b/http-service/src/main/java/net/runelite/http/service/examine/ExamineType.java new file mode 100644 index 0000000000..86826c0325 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/examine/ExamineType.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2017, 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.service.examine; + +public enum ExamineType +{ + OBJECT, + NPC, + ITEM; +} diff --git a/http-service/src/main/java/net/runelite/http/service/feed/FeedController.java b/http-service/src/main/java/net/runelite/http/service/feed/FeedController.java new file mode 100644 index 0000000000..c679cd319f --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/feed/FeedController.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2018, Lotto + * 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.service.feed; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import net.runelite.http.api.feed.FeedItem; +import net.runelite.http.api.feed.FeedResult; +import net.runelite.http.service.feed.blog.BlogService; +import net.runelite.http.service.feed.osrsnews.OSRSNewsService; +import net.runelite.http.service.feed.twitter.TwitterService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.CacheControl; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/feed") +@Slf4j +public class FeedController +{ + private final BlogService blogService; + private final TwitterService twitterService; + private final OSRSNewsService osrsNewsService; + + private FeedResult feedResult; + + @Autowired + public FeedController(BlogService blogService, TwitterService twitterService, OSRSNewsService osrsNewsService) + { + this.blogService = blogService; + this.twitterService = twitterService; + this.osrsNewsService = osrsNewsService; + } + + @Scheduled(fixedDelay = 10 * 60 * 1000) + public void updateFeed() + { + List items = new ArrayList<>(); + + try + { + items.addAll(blogService.getBlogPosts()); + } + catch (IOException e) + { + log.warn(e.getMessage()); + } + + try + { + items.addAll(twitterService.getTweets()); + } + catch (IOException e) + { + log.warn(e.getMessage()); + } + + try + { + items.addAll(osrsNewsService.getNews()); + } + catch (IOException e) + { + log.warn(e.getMessage()); + } + + feedResult = new FeedResult(items); + } + + @GetMapping + public ResponseEntity getFeed() + { + if (feedResult == null) + { + return ResponseEntity.notFound() + .build(); + } + + return ResponseEntity.ok() + .cacheControl(CacheControl.maxAge(10, TimeUnit.MINUTES).cachePublic()) + .body(feedResult); + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/feed/blog/BlogService.java b/http-service/src/main/java/net/runelite/http/service/feed/blog/BlogService.java new file mode 100644 index 0000000000..9da6cc171b --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/feed/blog/BlogService.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2018, Lotto + * 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.service.feed.blog; + +import java.io.IOException; +import java.io.InputStream; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import net.runelite.http.api.RuneLiteAPI; +import net.runelite.http.api.feed.FeedItem; +import net.runelite.http.api.feed.FeedItemType; +import net.runelite.http.service.util.exception.InternalServerErrorException; +import okhttp3.HttpUrl; +import okhttp3.Request; +import okhttp3.Response; +import org.springframework.stereotype.Service; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +@Service +public class BlogService +{ + private static final HttpUrl RSS_URL = HttpUrl.parse("https://runelite.net/atom.xml"); + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); + + public List getBlogPosts() throws IOException + { + Request request = new Request.Builder() + .url(RSS_URL) + .build(); + + try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute()) + { + if (!response.isSuccessful()) + { + throw new IOException("Error getting blog posts: " + response); + } + + try + { + InputStream in = response.body().byteStream(); + Document document = DocumentBuilderFactory.newInstance() + .newDocumentBuilder() + .parse(in); + + Element documentElement = document.getDocumentElement(); + NodeList documentItems = documentElement.getElementsByTagName("entry"); + + List items = new ArrayList<>(); + + for (int i = 0; i < Math.min(documentItems.getLength(), 3); i++) + { + Node item = documentItems.item(i); + NodeList children = item.getChildNodes(); + + String title = null; + String summary = null; + String link = null; + long timestamp = -1; + + for (int j = 0; j < children.getLength(); j++) + { + Node childItem = children.item(j); + String nodeName = childItem.getNodeName(); + + switch (nodeName) + { + case "title": + title = childItem.getTextContent(); + break; + case "summary": + summary = childItem.getTextContent().replace("\n", "").trim(); + break; + case "link": + link = childItem.getAttributes().getNamedItem("href").getTextContent(); + break; + case "updated": + timestamp = DATE_FORMAT.parse(childItem.getTextContent()).getTime(); + break; + } + } + + if (title == null || summary == null || link == null || timestamp == -1) + { + throw new InternalServerErrorException("Failed to find title, summary, link and/or timestamp in the blog post feed"); + } + + items.add(new FeedItem(FeedItemType.BLOG_POST, title, summary, link, timestamp)); + } + + return items; + } + catch (ParserConfigurationException | SAXException | ParseException e) + { + throw new InternalServerErrorException("Failed to parse blog posts: " + e.getMessage()); + } + } + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/feed/osrsnews/OSRSNewsService.java b/http-service/src/main/java/net/runelite/http/service/feed/osrsnews/OSRSNewsService.java new file mode 100644 index 0000000000..0ae08bc85c --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/feed/osrsnews/OSRSNewsService.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2018, Lotto + * 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.service.feed.osrsnews; + +import java.io.IOException; +import java.io.InputStream; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import net.runelite.http.api.RuneLiteAPI; +import net.runelite.http.api.feed.FeedItem; +import net.runelite.http.api.feed.FeedItemType; +import net.runelite.http.service.util.exception.InternalServerErrorException; +import okhttp3.HttpUrl; +import okhttp3.Request; +import okhttp3.Response; +import org.springframework.stereotype.Service; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +@Service +public class OSRSNewsService +{ + private static final HttpUrl RSS_URL = HttpUrl.parse("https://services.runescape.com/m=news/latest_news.rss?oldschool=true"); + private static final SimpleDateFormat PUB_DATE_FORMAT = new SimpleDateFormat("EEE, dd MMM yyyy '00:00:00 GMT'", Locale.US); + + public List getNews() throws IOException + { + Request request = new Request.Builder() + .url(RSS_URL) + .build(); + + try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute()) + { + if (!response.isSuccessful()) + { + throw new IOException("Error getting OSRS news: " + response); + } + + try + { + InputStream in = response.body().byteStream(); + Document document = DocumentBuilderFactory.newInstance() + .newDocumentBuilder() + .parse(in); + + Element documentElement = document.getDocumentElement(); + NodeList documentItems = documentElement.getElementsByTagName("item"); + + List items = new ArrayList<>(); + + for (int i = 0; i < documentItems.getLength(); i++) + { + Node item = documentItems.item(i); + NodeList children = item.getChildNodes(); + + String title = null; + String description = null; + String link = null; + long timestamp = -1; + + for (int j = 0; j < children.getLength(); j++) + { + Node childItem = children.item(j); + String nodeName = childItem.getNodeName(); + + switch (nodeName) + { + case "title": + title = childItem.getTextContent(); + break; + case "description": + description = childItem.getTextContent().replace("\n", "").trim(); + break; + case "link": + link = childItem.getTextContent(); + break; + case "pubDate": + timestamp = PUB_DATE_FORMAT.parse(childItem.getTextContent()).getTime(); + break; + } + } + + if (title == null || description == null || link == null || timestamp == -1) + { + throw new InternalServerErrorException("Failed to find title, description, link and/or timestamp in the OSRS RSS feed"); + } + + items.add(new FeedItem(FeedItemType.OSRS_NEWS, title, description, link, timestamp)); + } + + return items; + } + catch (ParserConfigurationException | SAXException | ParseException e) + { + throw new InternalServerErrorException("Failed to parse OSRS news: " + e.getMessage()); + } + } + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/feed/twitter/TwitterOAuth2TokenResponse.java b/http-service/src/main/java/net/runelite/http/service/feed/twitter/TwitterOAuth2TokenResponse.java new file mode 100644 index 0000000000..24df0b6cf6 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/feed/twitter/TwitterOAuth2TokenResponse.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2018, Lotto + * 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.service.feed.twitter; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; + +@Data +class TwitterOAuth2TokenResponse +{ + @SerializedName("token_type") + private String tokenType; + + @SerializedName("access_token") + private String token; +} diff --git a/http-service/src/main/java/net/runelite/http/service/feed/twitter/TwitterService.java b/http-service/src/main/java/net/runelite/http/service/feed/twitter/TwitterService.java new file mode 100644 index 0000000000..9bb9dc8595 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/feed/twitter/TwitterService.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2018, Lotto + * 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.service.feed.twitter; + +import com.google.gson.reflect.TypeToken; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import net.runelite.http.api.RuneLiteAPI; +import net.runelite.http.api.feed.FeedItem; +import net.runelite.http.api.feed.FeedItemType; +import net.runelite.http.service.util.exception.InternalServerErrorException; +import okhttp3.FormBody; +import okhttp3.HttpUrl; +import okhttp3.Request; +import okhttp3.Response; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; + +@Service +public class TwitterService +{ + private static final HttpUrl AUTH_URL = HttpUrl.parse("https://api.twitter.com/oauth2/token"); + private static final HttpUrl LIST_STATUSES_URL = HttpUrl.parse("https://api.twitter.com/1.1/lists/statuses.json"); + + private final String credentials; + private final String listId; + + private String token; + + @Autowired + public TwitterService( + @Value("${runelite.twitter.consumerkey}") String consumerKey, + @Value("${runelite.twitter.secretkey}") String consumerSecret, + @Value("${runelite.twitter.listid}") String listId + ) + { + this.credentials = consumerKey + ":" + consumerSecret; + this.listId = listId; + } + + public List getTweets() throws IOException + { + return getTweets(false); + } + + private List getTweets(boolean hasRetried) throws IOException + { + if (token == null) + { + updateToken(); + } + + HttpUrl url = LIST_STATUSES_URL.newBuilder() + .addQueryParameter("list_id", listId) + .addQueryParameter("count", "15") + .addQueryParameter("include_entities", "false") + .build(); + + Request request = new Request.Builder() + .url(url) + .header("Authorization", "Bearer " + token) + .build(); + + try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute()) + { + if (!response.isSuccessful()) + { + switch (HttpStatus.valueOf(response.code())) + { + case BAD_REQUEST: + case UNAUTHORIZED: + updateToken(); + if (!hasRetried) + { + return getTweets(true); + } + throw new InternalServerErrorException("Could not auth to Twitter after trying once: " + response); + default: + throw new IOException("Error getting Twitter list: " + response); + } + } + + InputStream in = response.body().byteStream(); + Type listType = new TypeToken>() + { + }.getType(); + List statusesResponse = RuneLiteAPI.GSON + .fromJson(new InputStreamReader(in), listType); + + List items = new ArrayList<>(); + + for (TwitterStatusesResponseItem i : statusesResponse) + { + items.add(new FeedItem(FeedItemType.TWEET, + i.getUser().getProfileImageUrl(), + i.getUser().getScreenName(), + i.getText().replace("\n\n", " ").replaceAll("\n", " "), + "https://twitter.com/" + i.getUser().getScreenName() + "/status/" + i.getId(), + getTimestampFromSnowflake(i.getId()))); + } + + return items; + } + } + + private void updateToken() throws IOException + { + String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes()); + + Request request = new Request.Builder() + .url(AUTH_URL) + .header("Authorization", "Basic " + encodedCredentials) + .post(new FormBody.Builder().add("grant_type", "client_credentials").build()) + .build(); + + try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute()) + { + if (!response.isSuccessful()) + { + throw new IOException("Error authing to Twitter: " + response); + } + + InputStream in = response.body().byteStream(); + TwitterOAuth2TokenResponse tokenResponse = RuneLiteAPI.GSON + .fromJson(new InputStreamReader(in), TwitterOAuth2TokenResponse.class); + + if (!tokenResponse.getTokenType().equals("bearer")) + { + throw new InternalServerErrorException("Returned token was not a bearer token"); + } + + if (tokenResponse.getToken() == null) + { + throw new InternalServerErrorException("Returned token was null"); + } + + token = tokenResponse.getToken(); + } + } + + /** + * Extracts the UTC timestamp from a Twitter snowflake as per + * https://github.com/client9/snowflake2time/blob/master/python/snowflake.py#L24 + */ + private long getTimestampFromSnowflake(long snowflake) + { + return (snowflake >> 22) + 1288834974657L; + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/feed/twitter/TwitterStatusesResponseItem.java b/http-service/src/main/java/net/runelite/http/service/feed/twitter/TwitterStatusesResponseItem.java new file mode 100644 index 0000000000..90b37c5021 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/feed/twitter/TwitterStatusesResponseItem.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2018, Lotto + * 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.service.feed.twitter; + +import lombok.Data; + +@Data +class TwitterStatusesResponseItem +{ + private long id; + private String text; + private TwitterStatusesResponseItemUser user; +} diff --git a/http-service/src/main/java/net/runelite/http/service/feed/twitter/TwitterStatusesResponseItemUser.java b/http-service/src/main/java/net/runelite/http/service/feed/twitter/TwitterStatusesResponseItemUser.java new file mode 100644 index 0000000000..94fe9360f9 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/feed/twitter/TwitterStatusesResponseItemUser.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2018, Lotto + * 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.service.feed.twitter; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; + +@Data +class TwitterStatusesResponseItemUser +{ + @SerializedName("screen_name") + private String screenName; + + @SerializedName("profile_image_url_https") + private String profileImageUrl; +} diff --git a/http-service/src/main/java/net/runelite/http/service/ge/GrandExchangeController.java b/http-service/src/main/java/net/runelite/http/service/ge/GrandExchangeController.java new file mode 100644 index 0000000000..94c759b130 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/ge/GrandExchangeController.java @@ -0,0 +1,111 @@ +/* + * 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.service.ge; + +import java.io.IOException; +import java.util.Collection; +import java.util.stream.Collectors; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import net.runelite.http.api.ge.GrandExchangeTrade; +import net.runelite.http.service.account.AuthFilter; +import net.runelite.http.service.account.beans.SessionEntry; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/ge") +public class GrandExchangeController +{ + private final GrandExchangeService grandExchangeService; + private final AuthFilter authFilter; + + @Autowired + public GrandExchangeController(GrandExchangeService grandExchangeService, AuthFilter authFilter) + { + this.grandExchangeService = grandExchangeService; + this.authFilter = authFilter; + } + + @PostMapping + public void submit(HttpServletRequest request, HttpServletResponse response, @RequestBody GrandExchangeTrade grandExchangeTrade) throws IOException + { + SessionEntry session = authFilter.handle(request, response); + + if (session == null) + { + return; + } + + grandExchangeService.add(session.getUser(), grandExchangeTrade); + } + + @GetMapping + public Collection get(HttpServletRequest request, HttpServletResponse response, + @RequestParam(required = false, defaultValue = "1024") int limit, + @RequestParam(required = false, defaultValue = "0") int offset) throws IOException + { + SessionEntry session = authFilter.handle(request, response); + + if (session == null) + { + return null; + } + + return grandExchangeService.get(session.getUser(), limit, offset).stream() + .map(GrandExchangeController::convert) + .collect(Collectors.toList()); + } + + private static GrandExchangeTrade convert(TradeEntry tradeEntry) + { + GrandExchangeTrade grandExchangeTrade = new GrandExchangeTrade(); + grandExchangeTrade.setBuy(tradeEntry.getAction() == TradeAction.BUY); + grandExchangeTrade.setItemId(tradeEntry.getItem()); + grandExchangeTrade.setQuantity(tradeEntry.getQuantity()); + grandExchangeTrade.setPrice(tradeEntry.getPrice()); + grandExchangeTrade.setTime(tradeEntry.getTime()); + return grandExchangeTrade; + } + + @DeleteMapping + public void delete(HttpServletRequest request, HttpServletResponse response) throws IOException + { + SessionEntry session = authFilter.handle(request, response); + + if (session == null) + { + return; + } + + grandExchangeService.delete(session.getUser()); + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/ge/GrandExchangeService.java b/http-service/src/main/java/net/runelite/http/service/ge/GrandExchangeService.java new file mode 100644 index 0000000000..6456beb85f --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/ge/GrandExchangeService.java @@ -0,0 +1,113 @@ +/* + * 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.service.ge; + +import java.util.Collection; +import net.runelite.http.api.ge.GrandExchangeTrade; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.sql2o.Connection; +import org.sql2o.Sql2o; + +@Service +public class GrandExchangeService +{ + private static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS `ge_trades` (\n" + + " `id` int(11) NOT NULL AUTO_INCREMENT,\n" + + " `user` int(11) NOT NULL,\n" + + " `action` enum('BUY','SELL') NOT NULL,\n" + + " `item` int(11) NOT NULL,\n" + + " `quantity` int(11) NOT NULL,\n" + + " `price` int(11) NOT NULL,\n" + + " `time` timestamp NOT NULL DEFAULT current_timestamp(),\n" + + " PRIMARY KEY (`id`),\n" + + " KEY `user_time` (`user`, `time`),\n" + + " KEY `time` (`time`),\n" + + " CONSTRAINT `ge_trades_ibfk_1` FOREIGN KEY (`user`) REFERENCES `users` (`id`)\n" + + ") ENGINE=InnoDB;"; + + private final Sql2o sql2o; + + @Autowired + public GrandExchangeService(@Qualifier("Runelite SQL2O") Sql2o sql2o) + { + this.sql2o = sql2o; + + // Ensure necessary tables exist + try (Connection con = sql2o.open()) + { + con.createQuery(CREATE_TABLE).executeUpdate(); + } + } + + public void add(int userId, GrandExchangeTrade grandExchangeTrade) + { + try (Connection con = sql2o.open()) + { + con.createQuery("insert into ge_trades (user, action, item, quantity, price) values (:user," + + " :action, :item, :quantity, :price)") + .addParameter("user", userId) + .addParameter("action", grandExchangeTrade.isBuy() ? "BUY" : "SELL") + .addParameter("item", grandExchangeTrade.getItemId()) + .addParameter("quantity", grandExchangeTrade.getQuantity()) + .addParameter("price", grandExchangeTrade.getPrice()) + .executeUpdate(); + } + } + + public Collection get(int userId, int limit, int offset) + { + try (Connection con = sql2o.open()) + { + return con.createQuery("select id, user, action, item, quantity, price, time from ge_trades where user = :user limit :limit offset :offset") + .addParameter("user", userId) + .addParameter("limit", limit) + .addParameter("offset", offset) + .executeAndFetch(TradeEntry.class); + } + } + + public void delete(int userId) + { + try (Connection con = sql2o.open()) + { + con.createQuery("delete from ge_trades where user = :user") + .addParameter("user", userId) + .executeUpdate(); + } + } + + @Scheduled(fixedDelay = 60 * 60 * 1000) + public void expire() + { + try (Connection con = sql2o.open()) + { + con.createQuery("delete from ge_trades where time < current_timestamp - interval 1 month") + .executeUpdate(); + } + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/ge/TradeAction.java b/http-service/src/main/java/net/runelite/http/service/ge/TradeAction.java new file mode 100644 index 0000000000..fcc96d615f --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/ge/TradeAction.java @@ -0,0 +1,31 @@ +/* + * 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.service.ge; + +enum TradeAction +{ + BUY, + SELL; +} diff --git a/http-service/src/main/java/net/runelite/http/service/ge/TradeEntry.java b/http-service/src/main/java/net/runelite/http/service/ge/TradeEntry.java new file mode 100644 index 0000000000..bca3869811 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/ge/TradeEntry.java @@ -0,0 +1,40 @@ +/* + * 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.service.ge; + +import java.time.Instant; +import lombok.Data; + +@Data +class TradeEntry +{ + private int id; + private int user; + private TradeAction action; + private int item; + private int quantity; + private int price; + private Instant time; +} diff --git a/http-service/src/main/java/net/runelite/http/service/hiscore/HiscoreController.java b/http-service/src/main/java/net/runelite/http/service/hiscore/HiscoreController.java new file mode 100644 index 0000000000..25a89e6a8d --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/hiscore/HiscoreController.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2017, 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.service.hiscore; + +import java.util.concurrent.ExecutionException; +import net.runelite.http.api.hiscore.HiscoreEndpoint; +import net.runelite.http.api.hiscore.HiscoreResult; +import net.runelite.http.api.hiscore.HiscoreSkill; +import net.runelite.http.api.hiscore.SingleHiscoreSkillResult; +import net.runelite.http.api.hiscore.Skill; +import net.runelite.http.service.util.HiscoreEndpointEditor; +import net.runelite.http.service.xp.XpTrackerService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/hiscore") +public class HiscoreController +{ + @Autowired + private HiscoreService hiscoreService; + + @Autowired + private XpTrackerService xpTrackerService; + + @GetMapping("/{endpoint}") + public HiscoreResult lookup(@PathVariable HiscoreEndpoint endpoint, @RequestParam String username) throws ExecutionException + { + HiscoreResult result = hiscoreService.lookupUsername(username, endpoint); + + // Submit to xp tracker? + switch (endpoint) + { + case NORMAL: + case IRONMAN: + case ULTIMATE_IRONMAN: + case HARDCORE_IRONMAN: + xpTrackerService.update(username, result); + } + + return result; + } + + @GetMapping("/{endpoint}/{skillName}") + public SingleHiscoreSkillResult singleSkillLookup(@PathVariable HiscoreEndpoint endpoint, @PathVariable String skillName, @RequestParam String username) throws ExecutionException + { + HiscoreSkill skill = HiscoreSkill.valueOf(skillName.toUpperCase()); + + // RS api only supports looking up all stats + HiscoreResult result = hiscoreService.lookupUsername(username, endpoint); + + // Find the skill to return + Skill requested = result.getSkill(skill); + + SingleHiscoreSkillResult skillResult = new SingleHiscoreSkillResult(); + skillResult.setPlayer(username); + skillResult.setSkillName(skillName); + skillResult.setSkill(requested); + + return skillResult; + } + + @InitBinder + public void initBinder(WebDataBinder binder) + { + binder.registerCustomEditor(HiscoreEndpoint.class, new HiscoreEndpointEditor()); + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/hiscore/HiscoreKey.java b/http-service/src/main/java/net/runelite/http/service/hiscore/HiscoreKey.java new file mode 100644 index 0000000000..b15603a224 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/hiscore/HiscoreKey.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2018, 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: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 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 HOLDER 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.service.hiscore; + +import lombok.Value; +import net.runelite.http.api.hiscore.HiscoreEndpoint; + +@Value +class HiscoreKey +{ + String username; + HiscoreEndpoint endpoint; +} diff --git a/http-service/src/main/java/net/runelite/http/service/hiscore/HiscoreService.java b/http-service/src/main/java/net/runelite/http/service/hiscore/HiscoreService.java new file mode 100644 index 0000000000..cc565b4444 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/hiscore/HiscoreService.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2017, 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.service.hiscore; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import net.runelite.http.api.hiscore.HiscoreClient; +import net.runelite.http.api.hiscore.HiscoreEndpoint; +import net.runelite.http.api.hiscore.HiscoreResult; +import okhttp3.HttpUrl; +import org.springframework.stereotype.Service; + +@Service +public class HiscoreService +{ + private final HiscoreClient hiscoreClient = new HiscoreClient(); + private final LoadingCache hiscoreCache = CacheBuilder.newBuilder() + .maximumSize(128) + .expireAfterWrite(1, TimeUnit.MINUTES) + .build( + new CacheLoader() + { + @Override + public HiscoreResult load(HiscoreKey key) throws IOException + { + return hiscoreClient.lookup(key.getUsername(), key.getEndpoint()); + } + }); + + @VisibleForTesting + HiscoreResult lookupUsername(String username, HttpUrl httpUrl) throws IOException + { + return hiscoreClient.lookup(username, httpUrl); + } + + public HiscoreResult lookupUsername(String username, HiscoreEndpoint endpoint) throws ExecutionException + { + return hiscoreCache.get(new HiscoreKey(username, endpoint)); + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/item/ItemController.java b/http-service/src/main/java/net/runelite/http/service/item/ItemController.java new file mode 100644 index 0000000000..9d1ed5710e --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/item/ItemController.java @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2017-2018, 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.service.item; + +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import javax.servlet.http.HttpServletResponse; +import net.runelite.http.api.item.Item; +import net.runelite.http.api.item.ItemPrice; +import net.runelite.http.api.item.SearchResult; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.CacheControl; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/item") +public class ItemController +{ + private static final String RUNELITE_CACHE = "RuneLite-Cache"; + private static final int MAX_BATCH_LOOKUP = 1024; + + private final Cache cachedEmpty = CacheBuilder.newBuilder() + .maximumSize(1024L) + .build(); + + private final ItemService itemService; + + private final Supplier memorizedPrices; + + @Autowired + public ItemController(ItemService itemService) + { + this.itemService = itemService; + + memorizedPrices = Suppliers.memoizeWithExpiration(() -> itemService.fetchPrices().stream() + .map(priceEntry -> + { + ItemPrice itemPrice = new ItemPrice(); + itemPrice.setId(priceEntry.getItem()); + itemPrice.setName(priceEntry.getName()); + itemPrice.setPrice(priceEntry.getPrice()); + itemPrice.setTime(priceEntry.getTime()); + return itemPrice; + }) + .toArray(ItemPrice[]::new), 30, TimeUnit.MINUTES); + } + + @GetMapping("/{itemId}") + public Item getItem(HttpServletResponse response, @PathVariable int itemId) + { + ItemEntry item = itemService.getItem(itemId); + if (item != null) + { + return item.toItem(); + } + + itemService.queueItem(itemId); + return null; + } + + @GetMapping(path = "/{itemId}/icon", produces = "image/gif") + public ResponseEntity getIcon(@PathVariable int itemId) + { + ItemEntry item = itemService.getItem(itemId); + if (item != null && item.getIcon() != null) + { + return ResponseEntity.ok(item.getIcon()); + } + + itemService.queueItem(itemId); + return ResponseEntity.notFound().build(); + } + + @GetMapping(path = "/{itemId}/icon/large", produces = "image/gif") + public ResponseEntity getIconLarge(HttpServletResponse response, @PathVariable int itemId) + { + ItemEntry item = itemService.getItem(itemId); + if (item != null && item.getIcon_large() != null) + { + return ResponseEntity.ok(item.getIcon_large()); + } + + itemService.queueItem(itemId); + return ResponseEntity.notFound().build(); + } + + @GetMapping("/{itemId}/price") + public ResponseEntity itemPrice( + @PathVariable int itemId, + @RequestParam(required = false) Instant time + ) + { + if (cachedEmpty.getIfPresent(itemId) != null) + { + return ResponseEntity.notFound() + .header(RUNELITE_CACHE, "HIT") + .build(); + } + + Instant now = Instant.now(); + + if (time != null && time.isAfter(now)) + { + time = now; + } + + ItemEntry item = itemService.getItem(itemId); + if (item == null) + { + itemService.queueItem(itemId); // queue lookup + cachedEmpty.put(itemId, itemId); // cache empty + return ResponseEntity.notFound() + .header(RUNELITE_CACHE, "MISS") + .build(); + } + + PriceEntry priceEntry = itemService.getPrice(itemId, time); + + if (time != null) + { + if (priceEntry == null) + { + // we maybe can't backfill this + return ResponseEntity.notFound() + .header(RUNELITE_CACHE, "MISS") + .build(); + } + } + else if (priceEntry == null) + { + // Price is unknown + cachedEmpty.put(itemId, itemId); + return ResponseEntity.notFound() + .header(RUNELITE_CACHE, "MISS") + .build(); + } + + ItemPrice itemPrice = new ItemPrice(); + itemPrice.setId(item.getId()); + itemPrice.setName(item.getName()); + itemPrice.setPrice(priceEntry.getPrice()); + itemPrice.setTime(priceEntry.getTime()); + + return ResponseEntity.ok() + .cacheControl(CacheControl.maxAge(30, TimeUnit.MINUTES).cachePublic()) + .body(itemPrice); + } + + @GetMapping("/search") + public SearchResult search(@RequestParam String query) + { + List result = itemService.search(query); + + itemService.queueSearch(query); + + SearchResult searchResult = new SearchResult(); + searchResult.setItems(result.stream() + .map(ItemEntry::toItem) + .collect(Collectors.toList())); + return searchResult; + } + + @GetMapping("/price") + public ItemPrice[] prices(@RequestParam("id") int[] itemIds) + { + if (itemIds.length > MAX_BATCH_LOOKUP) + { + itemIds = Arrays.copyOf(itemIds, MAX_BATCH_LOOKUP); + } + + List prices = itemService.getPrices(itemIds); + + return prices.stream() + .map(priceEntry -> + { + ItemPrice itemPrice = new ItemPrice(); + itemPrice.setId(priceEntry.getItem()); + itemPrice.setName(priceEntry.getName()); + itemPrice.setPrice(priceEntry.getPrice()); + itemPrice.setTime(priceEntry.getTime()); + return itemPrice; + }) + .toArray(ItemPrice[]::new); + } + + @GetMapping("/prices") + public ResponseEntity prices() + { + return ResponseEntity.ok() + .cacheControl(CacheControl.maxAge(30, TimeUnit.MINUTES).cachePublic()) + .body(memorizedPrices.get()); + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/item/ItemEntry.java b/http-service/src/main/java/net/runelite/http/service/item/ItemEntry.java new file mode 100644 index 0000000000..b92f171cc7 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/item/ItemEntry.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2017, 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.service.item; + +import java.time.Instant; +import lombok.Data; +import net.runelite.http.api.item.Item; +import net.runelite.http.api.item.ItemType; + +@Data +public class ItemEntry +{ + private int id; + private String name; + private String description; + private ItemType type; + private byte[] icon; + private byte[] icon_large; + private Instant timestamp; + + public Item toItem() + { + Item item = new Item(); + item.setId(id); + item.setName(name); + item.setDescription(description); + item.setType(type); + return item; + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/item/ItemService.java b/http-service/src/main/java/net/runelite/http/service/item/ItemService.java new file mode 100644 index 0000000000..72a0fe85aa --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/item/ItemService.java @@ -0,0 +1,505 @@ +/* + * Copyright (c) 2017-2018, 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.service.item; + +import com.google.gson.JsonParseException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; +import lombok.extern.slf4j.Slf4j; +import net.runelite.cache.definitions.ItemDefinition; +import net.runelite.http.api.RuneLiteAPI; +import net.runelite.http.api.item.ItemType; +import net.runelite.http.service.cache.CacheService; +import okhttp3.HttpUrl; +import okhttp3.Request; +import okhttp3.Response; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.sql2o.Connection; +import org.sql2o.Query; +import org.sql2o.Sql2o; + +@Service +@Slf4j +public class ItemService +{ + private static final String BASE = "https://services.runescape.com/m=itemdb_oldschool"; + private static final HttpUrl RS_ITEM_URL = HttpUrl.parse(BASE + "/api/catalogue/detail.json"); + private static final HttpUrl RS_PRICE_URL = HttpUrl.parse(BASE + "/api/graph"); + private static final HttpUrl RS_SEARCH_URL = HttpUrl.parse(BASE + "/api/catalogue/items.json?category=1"); + + private static final String CREATE_ITEMS = "CREATE TABLE IF NOT EXISTS `items` (\n" + + " `id` int(11) NOT NULL,\n" + + " `name` tinytext NOT NULL,\n" + + " `description` tinytext NOT NULL,\n" + + " `type` enum('DEFAULT') NOT NULL,\n" + + " `icon` blob,\n" + + " `icon_large` blob,\n" + + " `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n" + + " PRIMARY KEY (`id`),\n" + + " FULLTEXT idx_name (name)\n" + + ") ENGINE=InnoDB"; + + private static final String CREATE_PRICES = "CREATE TABLE IF NOT EXISTS `prices` (\n" + + " `item` int(11) NOT NULL,\n" + + " `price` int(11) NOT NULL,\n" + + " `time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',\n" + + " `fetched_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',\n" + + " UNIQUE KEY `item_time` (`item`,`time`),\n" + + " KEY `item_fetched_time` (`item`,`fetched_time`)\n" + + ") ENGINE=InnoDB"; + + private static final int MAX_PENDING = 512; + + private final Sql2o sql2o; + private final CacheService cacheService; + + private final ConcurrentLinkedQueue pendingLookups = new ConcurrentLinkedQueue(); + private int[] tradeableItems; + private final Random random = new Random(); + + @Autowired + public ItemService(@Qualifier("Runelite SQL2O") Sql2o sql2o, + CacheService cacheService) + { + this.sql2o = sql2o; + this.cacheService = cacheService; + + try (Connection con = sql2o.open()) + { + con.createQuery(CREATE_ITEMS) + .executeUpdate(); + + con.createQuery(CREATE_PRICES) + .executeUpdate(); + } + } + + public ItemEntry getItem(int itemId) + { + try (Connection con = sql2o.open()) + { + ItemEntry item = con.createQuery("select id, name, description, type, icon, icon_large from items where id = :id") + .addParameter("id", itemId) + .executeAndFetchFirst(ItemEntry.class); + + return item; + } + } + + private PriceEntry getPrice(Connection con, int itemId, Instant time) + { + if (time != null) + { + return con.createQuery("select item, name, price, time, fetched_time from prices t1 join items t2 on t1.item=t2.id where item = :item and time <= :time order by time desc limit 1") + .addParameter("item", itemId) + .addParameter("time", time.toString()) + .executeAndFetchFirst(PriceEntry.class); + } + else + { + return con.createQuery("select item, name, price, time, fetched_time from prices t1 join items t2 on t1.item=t2.id where item = :item order by time desc limit 1") + .addParameter("item", itemId) + .executeAndFetchFirst(PriceEntry.class); + } + } + + public PriceEntry getPrice(int itemId, Instant time) + { + try (Connection con = sql2o.open()) + { + return getPrice(con, itemId, time); + } + } + + public List getPrices(int... itemIds) + { + try (Connection con = sql2o.open()) + { + Set seen = new HashSet<>(); + List priceEntries = new ArrayList<>(itemIds.length); + + for (int itemId : itemIds) + { + if (seen.contains(itemId)) + { + continue; + } + seen.add(itemId); + + PriceEntry priceEntry = getPrice(con, itemId, null); + + if (priceEntry == null) + { + continue; + } + + priceEntries.add(priceEntry); + } + + return priceEntries; + } + } + + public List search(String search) + { + try (Connection con = sql2o.open()) + { + return con.createQuery("select id, name, description, type, match (name) against (:search) as score from items " + + "where match (name) against (:search) order by score desc limit 10") + .throwOnMappingFailure(false) // otherwise it tries to map 'score' + .addParameter("search", search) + .executeAndFetch(ItemEntry.class); + } + } + + public ItemEntry fetchItem(int itemId) + { + try + { + RSItem rsItem = fetchRSItem(itemId); + byte[] icon = null, iconLarge = null; + + try + { + icon = fetchImage(rsItem.getIcon()); + } + catch (IOException ex) + { + log.warn("error fetching image", ex); + } + + try + { + iconLarge = fetchImage(rsItem.getIcon_large()); + } + catch (IOException ex) + { + log.warn("error fetching image", ex); + } + + try (Connection con = sql2o.open()) + { + con.createQuery("insert into items (id, name, description, type, icon, icon_large) values (:id," + + " :name, :description, :type, :icon, :icon_large) ON DUPLICATE KEY UPDATE name = :name," + + " description = :description, type = :type, icon = :icon, icon_large = :icon_large") + .addParameter("id", rsItem.getId()) + .addParameter("name", rsItem.getName()) + .addParameter("description", rsItem.getDescription()) + .addParameter("type", rsItem.getType()) + .addParameter("icon", icon) + .addParameter("icon_large", iconLarge) + .executeUpdate(); + } + + ItemEntry item = new ItemEntry(); + item.setId(itemId); + item.setName(rsItem.getName()); + item.setDescription(rsItem.getDescription()); + item.setType(ItemType.of(rsItem.getType())); + item.setIcon(icon); + item.setIcon_large(iconLarge); + return item; + } + catch (IOException ex) + { + log.warn("unable to fetch item {}", itemId, ex); + return null; + } + } + + public List fetchPrice(int itemId) + { + RSPrices rsprice; + try + { + rsprice = fetchRSPrices(itemId); + } + catch (IOException ex) + { + log.warn("unable to fetch price for item {}", itemId, ex); + return null; + } + + try (Connection con = sql2o.beginTransaction()) + { + List entries = new ArrayList<>(); + Instant now = Instant.now(); + + Query query = con.createQuery("insert into prices (item, price, time, fetched_time) values (:item, :price, :time, :fetched_time) " + + "ON DUPLICATE KEY UPDATE price = VALUES(price), fetched_time = VALUES(fetched_time)"); + + for (Map.Entry entry : rsprice.getDaily().entrySet()) + { + long ts = entry.getKey(); // ms since epoch + int price = entry.getValue(); // gp + + Instant time = Instant.ofEpochMilli(ts); + + PriceEntry priceEntry = new PriceEntry(); + priceEntry.setItem(itemId); + priceEntry.setPrice(price); + priceEntry.setTime(time); + priceEntry.setFetched_time(now); + entries.add(priceEntry); + + query + .addParameter("item", itemId) + .addParameter("price", price) + .addParameter("time", time) + .addParameter("fetched_time", now) + .addToBatch(); + } + + query.executeBatch(); + con.commit(false); + + return entries; + } + } + + public List fetchPrices() + { + try (Connection con = sql2o.beginTransaction()) + { + Query query = con.createQuery("select t2.item, t3.name, t2.time, prices.price, prices.fetched_time from (select t1.item as item, max(t1.time) as time from prices t1 group by item) t2 " + + " join prices on t2.item=prices.item and t2.time=prices.time" + + " join items t3 on t2.item=t3.id"); + return query.executeAndFetch(PriceEntry.class); + } + } + + private RSItem fetchRSItem(int itemId) throws IOException + { + HttpUrl itemUrl = RS_ITEM_URL + .newBuilder() + .addQueryParameter("item", "" + itemId) + .build(); + + Request request = new Request.Builder() + .url(itemUrl) + .build(); + + RSItemResponse itemResponse = fetchJson(request, RSItemResponse.class); + return itemResponse.getItem(); + + } + + private RSPrices fetchRSPrices(int itemId) throws IOException + { + HttpUrl priceUrl = RS_PRICE_URL + .newBuilder() + .addPathSegment(itemId + ".json") + .build(); + + Request request = new Request.Builder() + .url(priceUrl) + .build(); + + return fetchJson(request, RSPrices.class); + } + + public RSSearch fetchRSSearch(String query) throws IOException + { + // rs api seems to require lowercase + query = query.toLowerCase(); + + HttpUrl searchUrl = RS_SEARCH_URL + .newBuilder() + .addQueryParameter("alpha", query) + .build(); + + Request request = new Request.Builder() + .url(searchUrl) + .build(); + + return fetchJson(request, RSSearch.class); + } + + private void batchInsertItems(RSSearch search) + { + try (Connection con = sql2o.beginTransaction()) + { + Query q = con.createQuery("insert into items (id, name, description, type) values (:id," + + " :name, :description, :type) ON DUPLICATE KEY UPDATE name = :name," + + " description = :description, type = :type"); + + for (RSItem rsItem : search.getItems()) + { + q.addParameter("id", rsItem.getId()) + .addParameter("name", rsItem.getName()) + .addParameter("description", rsItem.getDescription()) + .addParameter("type", rsItem.getType()) + .addToBatch(); + } + + q.executeBatch(); + con.commit(false); + } + } + + private T fetchJson(Request request, Class clazz) throws IOException + { + try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute()) + { + if (!response.isSuccessful()) + { + throw new IOException("Unsuccessful http response: " + response); + } + + InputStream in = response.body().byteStream(); + return RuneLiteAPI.GSON.fromJson(new InputStreamReader(in), clazz); + } + catch (JsonParseException ex) + { + throw new IOException(ex); + } + } + + private byte[] fetchImage(String url) throws IOException + { + HttpUrl httpUrl = HttpUrl.parse(url); + + Request request = new Request.Builder() + .url(httpUrl) + .build(); + + try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute()) + { + if (!response.isSuccessful()) + { + throw new IOException("Unsuccessful http response: " + response); + } + + return response.body().bytes(); + } + } + + public void queueSearch(String search) + { + if (pendingLookups.size() < MAX_PENDING) + { + pendingLookups.add(new PendingLookup(search, PendingLookup.Type.SEARCH)); + } + else + { + log.debug("Dropping pending search for {}", search); + } + } + + public void queueItem(int itemId) + { + if (pendingLookups.size() < MAX_PENDING) + { + pendingLookups.add(new PendingLookup(itemId, PendingLookup.Type.ITEM)); + } + else + { + log.debug("Dropping pending item lookup for {}", itemId); + } + } + + @Scheduled(fixedDelay = 5000) + public void check() + { + PendingLookup pendingLookup = pendingLookups.poll(); + if (pendingLookup == null) + { + return; + } + + switch (pendingLookup.getType()) + { + case SEARCH: + try + { + RSSearch reSearch = fetchRSSearch(pendingLookup.getSearch()); + + batchInsertItems(reSearch); + } + catch (IOException ex) + { + log.warn("error while searching items", ex); + } + break; + case ITEM: + fetchItem(pendingLookup.getItemId()); + break; + } + } + + @Scheduled(fixedDelay = 20_000) + public void crawlPrices() + { + if (tradeableItems == null || tradeableItems.length == 0) + { + return; + } + + int idx = random.nextInt(tradeableItems.length); + int id = tradeableItems[idx]; + + if (getItem(id) == null) + { + // This is a new item.. + log.debug("Fetching new item {}", id); + queueItem(id); + return; + } + + log.debug("Fetching price for {}", id); + + fetchPrice(id); + } + + @Scheduled(fixedDelay = 1_8000_000) // 30 minutes + public void reloadItems() throws IOException + { + List items = cacheService.getItems(); + if (items.isEmpty()) + { + log.warn("Failed to load any items from cache, item price updating will be disabled"); + } + + tradeableItems = items.stream() + .filter(item -> item.isTradeable) + .mapToInt(item -> item.id) + .toArray(); + + log.debug("Loaded {} tradeable items", tradeableItems.length); + } + +} diff --git a/http-service/src/main/java/net/runelite/http/service/item/PendingLookup.java b/http-service/src/main/java/net/runelite/http/service/item/PendingLookup.java new file mode 100644 index 0000000000..d23ff5c1d9 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/item/PendingLookup.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2018, 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.service.item; + +import lombok.Value; + +@Value +class PendingLookup +{ + enum Type + { + SEARCH, + ITEM; + } + + private final int itemId; + private final String search; + private final Type type; + + public PendingLookup(int itemId, Type type) + { + this.itemId = itemId; + this.search = null; + this.type = type; + } + + public PendingLookup(String search, Type type) + { + this.itemId = -1; + this.search = search; + this.type = type; + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/item/PriceEntry.java b/http-service/src/main/java/net/runelite/http/service/item/PriceEntry.java new file mode 100644 index 0000000000..00f0e6815c --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/item/PriceEntry.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2017, 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.service.item; + +import java.time.Instant; +import lombok.Data; + +@Data +class PriceEntry +{ + private int item; + private String name; + private int price; + private Instant time; + private Instant fetched_time; +} diff --git a/http-service/src/main/java/net/runelite/http/service/item/RSItem.java b/http-service/src/main/java/net/runelite/http/service/item/RSItem.java new file mode 100644 index 0000000000..b7c09260c6 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/item/RSItem.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2017, 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.service.item; + +import lombok.Data; +import net.runelite.http.api.item.Item; +import net.runelite.http.api.item.ItemType; + +@Data +public class RSItem +{ + private int id; + private String name; + private String description; + private String type; + private String icon; + private String icon_large; + + public Item toItem() + { + Item item = new Item(); + item.setId(id); + item.setName(name); + item.setType(ItemType.of(type)); + item.setDescription(description); + return item; + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/item/RSItemResponse.java b/http-service/src/main/java/net/runelite/http/service/item/RSItemResponse.java new file mode 100644 index 0000000000..3fd7b3cef3 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/item/RSItemResponse.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2017, 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.service.item; + +import lombok.Data; + +@Data +public class RSItemResponse +{ + private RSItem item; +} diff --git a/http-service/src/main/java/net/runelite/http/service/item/RSPrices.java b/http-service/src/main/java/net/runelite/http/service/item/RSPrices.java new file mode 100644 index 0000000000..04331d753e --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/item/RSPrices.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2017, 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.service.item; + +import java.util.Map; +import lombok.Data; + +@Data +public class RSPrices +{ + /** + * unix time in ms to price in gp + */ + private Map daily; +} diff --git a/http-service/src/main/java/net/runelite/http/service/item/RSSearch.java b/http-service/src/main/java/net/runelite/http/service/item/RSSearch.java new file mode 100644 index 0000000000..b0edfdf798 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/item/RSSearch.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2017, 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.service.item; + +import java.util.List; +import lombok.Data; + +@Data +public class RSSearch +{ + private List items; +} diff --git a/http-service/src/main/java/net/runelite/http/service/loottracker/LootResult.java b/http-service/src/main/java/net/runelite/http/service/loottracker/LootResult.java new file mode 100644 index 0000000000..5bfd7afee0 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/loottracker/LootResult.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2018, 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.service.loottracker; + +import java.time.Instant; +import lombok.Data; +import net.runelite.http.api.loottracker.LootRecordType; + +@Data +class LootResult +{ + private int killId; + private Instant time; + private LootRecordType type; + private String eventId; + private int itemId; + private int itemQuantity; +} diff --git a/http-service/src/main/java/net/runelite/http/service/loottracker/LootTrackerController.java b/http-service/src/main/java/net/runelite/http/service/loottracker/LootTrackerController.java new file mode 100644 index 0000000000..2c1bd77140 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/loottracker/LootTrackerController.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2018, TheStonedTurtle + * Copyright (c) 2018, 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.service.loottracker; + +import com.google.api.client.http.HttpStatusCodes; +import java.io.IOException; +import java.util.Collection; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import net.runelite.http.api.loottracker.LootRecord; +import net.runelite.http.service.account.AuthFilter; +import net.runelite.http.service.account.beans.SessionEntry; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/loottracker") +public class LootTrackerController +{ + @Autowired + private LootTrackerService service; + + @Autowired + private AuthFilter auth; + + @RequestMapping(method = RequestMethod.POST) + public void storeLootRecord(HttpServletRequest request, HttpServletResponse response, @RequestBody LootRecord record) throws IOException + { + SessionEntry e = auth.handle(request, response); + if (e == null) + { + response.setStatus(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED); + return; + } + + service.store(record, e.getUser()); + response.setStatus(HttpStatusCodes.STATUS_CODE_OK); + } + + @GetMapping + public Collection getLootRecords(HttpServletRequest request, HttpServletResponse response, @RequestParam(value = "count", defaultValue = "1024") int count, @RequestParam(value = "start", defaultValue = "0") int start) throws IOException + { + SessionEntry e = auth.handle(request, response); + if (e == null) + { + response.setStatus(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED); + return null; + } + + return service.get(e.getUser(), count, start); + } + + @DeleteMapping + public void deleteLoot(HttpServletRequest request, HttpServletResponse response, + @RequestParam(required = false) String eventId) throws IOException + { + SessionEntry e = auth.handle(request, response); + if (e == null) + { + response.setStatus(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED); + return; + } + + service.delete(e.getUser(), eventId); + } +} \ No newline at end of file diff --git a/http-service/src/main/java/net/runelite/http/service/loottracker/LootTrackerService.java b/http-service/src/main/java/net/runelite/http/service/loottracker/LootTrackerService.java new file mode 100644 index 0000000000..b999f4eabb --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/loottracker/LootTrackerService.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2018, TheStonedTurtle + * Copyright (c) 2018, 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.service.loottracker; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import net.runelite.http.api.loottracker.GameItem; +import net.runelite.http.api.loottracker.LootRecord; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.sql2o.Connection; +import org.sql2o.Query; +import org.sql2o.Sql2o; + +@Service +public class LootTrackerService +{ + // Table for storing individual LootRecords + private static final String CREATE_KILLS = "CREATE TABLE IF NOT EXISTS `kills` (\n" + + " `id` INT AUTO_INCREMENT UNIQUE,\n" + + " `time` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),\n" + + " `accountId` INT NOT NULL,\n" + + " `type` enum('NPC', 'PLAYER', 'EVENT', 'UNKNOWN') NOT NULL,\n" + + " `eventId` VARCHAR(255) NOT NULL,\n" + + " PRIMARY KEY (id),\n" + + " FOREIGN KEY (accountId) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,\n" + + " INDEX idx_acc (accountId, time)," + + " INDEX idx_time (time)" + + ") ENGINE=InnoDB"; + + // Table for storing Items received as loot for individual LootRecords + private static final String CREATE_DROPS = "CREATE TABLE IF NOT EXISTS `drops` (\n" + + " `killId` INT NOT NULL,\n" + + " `itemId` INT NOT NULL,\n" + + " `itemQuantity` INT NOT NULL,\n" + + " FOREIGN KEY (killId) REFERENCES kills(id) ON DELETE CASCADE\n" + + ") ENGINE=InnoDB"; + + // Queries for inserting kills + private static final String INSERT_KILL_QUERY = "INSERT INTO kills (accountId, type, eventId) VALUES (:accountId, :type, :eventId)"; + private static final String INSERT_DROP_QUERY = "INSERT INTO drops (killId, itemId, itemQuantity) VALUES (LAST_INSERT_ID(), :itemId, :itemQuantity)"; + + private static final String SELECT_LOOT_QUERY = "SELECT killId,time,type,eventId,itemId,itemQuantity FROM kills JOIN drops ON drops.killId = kills.id WHERE accountId = :accountId ORDER BY TIME DESC LIMIT :limit OFFSET :offset"; + + private static final String DELETE_LOOT_ACCOUNT = "DELETE FROM kills WHERE accountId = :accountId"; + private static final String DELETE_LOOT_ACCOUNT_EVENTID = "DELETE FROM kills WHERE accountId = :accountId AND eventId = :eventId"; + + private final Sql2o sql2o; + + @Autowired + public LootTrackerService(@Qualifier("Runelite SQL2O") Sql2o sql2o) + { + this.sql2o = sql2o; + + // Ensure necessary tables exist + try (Connection con = sql2o.open()) + { + con.createQuery(CREATE_KILLS).executeUpdate(); + con.createQuery(CREATE_DROPS).executeUpdate(); + } + } + + /** + * Store LootRecord + * + * @param record LootRecord to store + * @param accountId runelite account id to tie data too + */ + public void store(LootRecord record, int accountId) + { + try (Connection con = sql2o.beginTransaction()) + { + // Kill Entry Query + con.createQuery(INSERT_KILL_QUERY, true) + .addParameter("accountId", accountId) + .addParameter("type", record.getType()) + .addParameter("eventId", record.getEventId()) + .executeUpdate(); + + Query insertDrop = con.createQuery(INSERT_DROP_QUERY); + + // Append all queries for inserting drops + for (GameItem drop : record.getDrops()) + { + insertDrop + .addParameter("itemId", drop.getId()) + .addParameter("itemQuantity", drop.getQty()) + .addToBatch(); + } + + insertDrop.executeBatch(); + con.commit(false); + } + } + + public Collection get(int accountId, int limit, int offset) + { + List lootResults; + + try (Connection con = sql2o.open()) + { + lootResults = con.createQuery(SELECT_LOOT_QUERY) + .addParameter("accountId", accountId) + .addParameter("limit", limit) + .addParameter("offset", offset) + .executeAndFetch(LootResult.class); + } + + LootResult current = null; + List lootRecords = new ArrayList<>(); + List gameItems = new ArrayList<>(); + + for (LootResult lootResult : lootResults) + { + if (current == null || current.getKillId() != lootResult.getKillId()) + { + if (!gameItems.isEmpty()) + { + LootRecord lootRecord = new LootRecord(current.getEventId(), current.getType(), gameItems, current.getTime()); + lootRecords.add(lootRecord); + + gameItems = new ArrayList<>(); + } + + current = lootResult; + } + + GameItem gameItem = new GameItem(lootResult.getItemId(), lootResult.getItemQuantity()); + gameItems.add(gameItem); + } + + if (!gameItems.isEmpty()) + { + LootRecord lootRecord = new LootRecord(current.getEventId(), current.getType(), gameItems, current.getTime()); + lootRecords.add(lootRecord); + } + + return lootRecords; + } + + public void delete(int accountId, String eventId) + { + try (Connection con = sql2o.open()) + { + if (eventId == null) + { + con.createQuery(DELETE_LOOT_ACCOUNT) + .addParameter("accountId", accountId) + .executeUpdate(); + } + else + { + con.createQuery(DELETE_LOOT_ACCOUNT_EVENTID) + .addParameter("accountId", accountId) + .addParameter("eventId", eventId) + .executeUpdate(); + } + } + } + + @Scheduled(fixedDelay = 15 * 60 * 1000) + public void expire() + { + try (Connection con = sql2o.open()) + { + con.createQuery("delete from kills where time < current_timestamp() - interval 30 day") + .executeUpdate(); + } + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/osbuddy/GrandExchangeEntry.java b/http-service/src/main/java/net/runelite/http/service/osbuddy/GrandExchangeEntry.java new file mode 100644 index 0000000000..8ea34c10c9 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/osbuddy/GrandExchangeEntry.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2018, AeonLucid + * 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.service.osbuddy; + +import java.time.Instant; +import lombok.Data; + +@Data +class GrandExchangeEntry +{ + private int item_id; + private int buy_average; + private int sell_average; + private int overall_average; + private Instant last_update; +} diff --git a/http-service/src/main/java/net/runelite/http/service/osbuddy/OSBGrandExchangeController.java b/http-service/src/main/java/net/runelite/http/service/osbuddy/OSBGrandExchangeController.java new file mode 100644 index 0000000000..eaade27c12 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/osbuddy/OSBGrandExchangeController.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2018, AeonLucid + * 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.service.osbuddy; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.CacheControl; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/osb/ge") +public class OSBGrandExchangeController +{ + private final OSBGrandExchangeService grandExchangeService; + + @Autowired + public OSBGrandExchangeController(OSBGrandExchangeService grandExchangeService) + { + this.grandExchangeService = grandExchangeService; + } + + @GetMapping + public ResponseEntity get(@RequestParam("itemId") int itemId) throws ExecutionException + { + GrandExchangeEntry grandExchangeEntry = grandExchangeService.get(itemId); + + return ResponseEntity.ok() + .cacheControl(CacheControl.maxAge(30, TimeUnit.MINUTES).cachePublic()) + .body(grandExchangeEntry); + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/osbuddy/OSBGrandExchangeService.java b/http-service/src/main/java/net/runelite/http/service/osbuddy/OSBGrandExchangeService.java new file mode 100644 index 0000000000..797d7e76e2 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/osbuddy/OSBGrandExchangeService.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2018, AeonLucid + * 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.service.osbuddy; + +import java.io.IOException; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import net.runelite.http.service.osbuddy.beans.OsbuddySummaryItem; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.sql2o.Connection; +import org.sql2o.Query; +import org.sql2o.Sql2o; + +@Service +@Slf4j +public class OSBGrandExchangeService +{ + private static final String CREATE_GRAND_EXCHANGE_PRICES = "CREATE TABLE IF NOT EXISTS `osb_ge` (\n" + + " `item_id` int(11) NOT NULL,\n" + + " `buy_average` int(11) NOT NULL,\n" + + " `sell_average` int(11) NOT NULL,\n" + + " `overall_average` int(11) NOT NULL,\n" + + " `last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n" + + " PRIMARY KEY (`item_id`)\n" + + ") ENGINE=InnoDB"; + + private static final OsbuddyClient CLIENT = new OsbuddyClient(); + + private final Sql2o sql2o; + + @Autowired + public OSBGrandExchangeService(@Qualifier("Runelite SQL2O") Sql2o sql2o) + { + this.sql2o = sql2o; + + try (Connection con = sql2o.open()) + { + con.createQuery(CREATE_GRAND_EXCHANGE_PRICES).executeUpdate(); + } + } + + public GrandExchangeEntry get(int itemId) + { + try (Connection con = sql2o.open()) + { + return con.createQuery("SELECT item_id, buy_average, sell_average, overall_average, last_update" + + " FROM osb_ge WHERE item_id = :itemId") + .addParameter("itemId", itemId) + .executeAndFetchFirst(GrandExchangeEntry.class); + } + } + + @Scheduled(initialDelay = 1000 * 5, fixedDelay = 1000 * 60 * 30) + private void updateDatabase() + { + try + { + Map summary = CLIENT.getSummary(); + + try (Connection con = sql2o.beginTransaction()) + { + Instant updateTime = Instant.now(); + + Query query = con.createQuery("INSERT INTO osb_ge (item_id, buy_average, sell_average, overall_average," + + " last_update) VALUES (:itemId, :buyAverage, :sellAverage, :overallAverage, :lastUpdate)" + + " ON DUPLICATE KEY UPDATE buy_average = VALUES(buy_average), sell_average = VALUES(sell_average)," + + " overall_average = VALUES(overall_average), last_update = VALUES(last_update)"); + + for (Map.Entry entry : summary.entrySet()) + { + Integer itemId = entry.getKey(); + OsbuddySummaryItem item = entry.getValue(); + + query + .addParameter("itemId", itemId) + .addParameter("buyAverage", item.getBuy_average()) + .addParameter("sellAverage", item.getSell_average()) + .addParameter("overallAverage", item.getOverall_average()) + .addParameter("lastUpdate", Timestamp.from(updateTime)) + .addToBatch(); + } + + query.executeBatch(); + con.commit(false); + } + } + catch (IOException e) + { + log.warn("Error while updating the osb grand exchange table", e); + } + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/osbuddy/OsbuddyClient.java b/http-service/src/main/java/net/runelite/http/service/osbuddy/OsbuddyClient.java new file mode 100644 index 0000000000..dadb635a4d --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/osbuddy/OsbuddyClient.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2018, AeonLucid + * 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.service.osbuddy; + +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.Map; +import net.runelite.http.api.RuneLiteAPI; +import net.runelite.http.service.osbuddy.beans.OsbuddySummaryItem; +import okhttp3.HttpUrl; +import okhttp3.Request; +import okhttp3.Response; + +public class OsbuddyClient +{ + private static final String USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36"; + + public Map getSummary() throws IOException + { + HttpUrl httpUrl = new HttpUrl.Builder() + .scheme("https") + .host("rsbuddy.com") + .addPathSegment("exchange") + .addPathSegment("summary.json") + .build(); + + Request request = new Request.Builder() + .url(httpUrl) + .header("User-Agent", USER_AGENT) + .build(); + + try (Response responseOk = RuneLiteAPI.CLIENT.newCall(request).execute()) + { + if (!responseOk.isSuccessful()) + { + throw new IOException("Error retrieving summary from OSBuddy: " + responseOk.message()); + } + + Type type = new TypeToken>() + { + }.getType(); + + return RuneLiteAPI.GSON.fromJson(responseOk.body().string(), type); + } + catch (JsonSyntaxException ex) + { + throw new IOException(ex); + } + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/osbuddy/beans/OsbuddySummaryItem.java b/http-service/src/main/java/net/runelite/http/service/osbuddy/beans/OsbuddySummaryItem.java new file mode 100644 index 0000000000..2f969d9636 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/osbuddy/beans/OsbuddySummaryItem.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2018, AeonLucid + * 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.service.osbuddy.beans; + +import lombok.Data; + +@Data +public class OsbuddySummaryItem +{ + private int id; + private String name; + private boolean members; + private int sp; + private int buy_average; + private int sell_average; + private int overall_average; +} diff --git a/http-service/src/main/java/net/runelite/http/service/sprite/SpriteController.java b/http-service/src/main/java/net/runelite/http/service/sprite/SpriteController.java new file mode 100644 index 0000000000..5afc6299bc --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/sprite/SpriteController.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2018, 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.service.sprite; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/sprite") +public class SpriteController +{ + @Autowired + private SpriteService spriteService; + + private final LoadingCache spriteCache = CacheBuilder.newBuilder() + .maximumSize(1024L) + .expireAfterWrite(10, TimeUnit.MINUTES) + .build(new CacheLoader() + { + @Override + public byte[] load(Integer key) throws Exception + { + byte[] data = spriteService.getImagePng(key >>> 16, key & 0xffff); + return data != null ? data : new byte[0]; + } + }); + + @GetMapping(produces = "image/png") + public ResponseEntity getSprite( + @RequestParam int spriteId, + @RequestParam(defaultValue = "0") int frameId + ) throws IOException + { + byte[] data = spriteCache.getUnchecked(spriteId << 16 | frameId); + if (data == null || data.length == 0) + { + return ResponseEntity.notFound().build(); + } + + return ResponseEntity.ok(data); + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/sprite/SpriteService.java b/http-service/src/main/java/net/runelite/http/service/sprite/SpriteService.java new file mode 100644 index 0000000000..659f1e03e2 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/sprite/SpriteService.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2018, 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.service.sprite; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import javax.imageio.ImageIO; +import net.runelite.cache.IndexType; +import net.runelite.cache.definitions.SpriteDefinition; +import net.runelite.cache.definitions.loaders.SpriteLoader; +import net.runelite.cache.fs.ArchiveFiles; +import net.runelite.cache.fs.FSFile; +import net.runelite.http.service.cache.CacheService; +import net.runelite.http.service.cache.beans.ArchiveEntry; +import net.runelite.http.service.cache.beans.CacheEntry; +import net.runelite.http.service.cache.beans.IndexEntry; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class SpriteService +{ + @Autowired + private CacheService cacheService; + + public SpriteDefinition getSprite(int spriteId, int frameId) throws IOException + { + CacheEntry cache = cacheService.findMostRecent(); + if (cache == null) + { + return null; + } + + IndexEntry index = cacheService.findIndexForCache(cache, IndexType.SPRITES.getNumber()); + if (index == null) + { + return null; + } + + ArchiveEntry archive = cacheService.findArchiveForIndex(index, spriteId); + if (archive == null) + { + return null; + } + + ArchiveFiles files = cacheService.getArchiveFiles(archive); + if (files == null) + { + return null; + } + + FSFile file = files.getFiles().get(0); + byte[] contents = file.getContents(); + SpriteDefinition[] sprite = new SpriteLoader().load(archive.getArchiveId(), contents); + if (frameId < 0 || frameId >= sprite.length) + { + return null; + } + + return sprite[frameId]; + } + + public BufferedImage getImage(int spriteId, int frameId) throws IOException + { + SpriteDefinition sprite = getSprite(spriteId, frameId); + if (sprite == null) + { + return null; + } + + BufferedImage bufferedImage = getSpriteImage(sprite); + return bufferedImage; + } + + public byte[] getImagePng(int spriteId, int frameId) throws IOException + { + BufferedImage image = getImage(spriteId, frameId); + if (image == null) + { + return null; + } + + ByteArrayOutputStream bao = new ByteArrayOutputStream(); + ImageIO.write(image, "png", bao); + return bao.toByteArray(); + } + + private BufferedImage getSpriteImage(SpriteDefinition sprite) + { + BufferedImage image = new BufferedImage(sprite.getWidth(), sprite.getHeight(), BufferedImage.TYPE_INT_ARGB); + image.setRGB(0, 0, sprite.getWidth(), sprite.getHeight(), sprite.getPixels(), 0, sprite.getWidth()); + return image; + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/util/exception/InternalServerErrorException.java b/http-service/src/main/java/net/runelite/http/service/util/exception/InternalServerErrorException.java index 30e0ffda4e..62adc5b6fc 100644 --- a/http-service/src/main/java/net/runelite/http/service/util/exception/InternalServerErrorException.java +++ b/http-service/src/main/java/net/runelite/http/service/util/exception/InternalServerErrorException.java @@ -35,4 +35,4 @@ public class InternalServerErrorException extends RuntimeException { super(message); } -} \ No newline at end of file +} diff --git a/http-service/src/main/java/net/runelite/http/service/util/exception/NotFoundException.java b/http-service/src/main/java/net/runelite/http/service/util/exception/NotFoundException.java index 840c125ff5..83e04ceca6 100644 --- a/http-service/src/main/java/net/runelite/http/service/util/exception/NotFoundException.java +++ b/http-service/src/main/java/net/runelite/http/service/util/exception/NotFoundException.java @@ -32,4 +32,4 @@ import org.springframework.web.bind.annotation.ResponseStatus; public class NotFoundException extends RuntimeException { -} \ No newline at end of file +} diff --git a/http-service/src/main/java/net/runelite/http/service/worlds/ServiceWorldType.java b/http-service/src/main/java/net/runelite/http/service/worlds/ServiceWorldType.java new file mode 100644 index 0000000000..c6aa598321 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/worlds/ServiceWorldType.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2018, Lotto + * 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.service.worlds; + +import net.runelite.http.api.worlds.WorldType; + +enum ServiceWorldType +{ + MEMBERS(WorldType.MEMBERS, 1), + PVP(WorldType.PVP, 1 << 2), + BOUNTY(WorldType.BOUNTY, 1 << 5), + SKILL_TOTAL(WorldType.SKILL_TOTAL, 1 << 7), + HIGH_RISK(WorldType.HIGH_RISK, 1 << 10), + LAST_MAN_STANDING(WorldType.LAST_MAN_STANDING, 1 << 14), + TOURNAMENT(WorldType.TOURNAMENT, 1 << 25), + DEADMAN_TOURNAMENT(WorldType.DEADMAN_TOURNAMENT, 1 << 26), + DEADMAN(WorldType.DEADMAN, 1 << 29), + SEASONAL_DEADMAN(WorldType.SEASONAL_DEADMAN, 1 << 30); + + private final WorldType apiType; + private final int mask; + + ServiceWorldType(WorldType apiType, int mask) + { + this.apiType = apiType; + this.mask = mask; + } + + public WorldType getApiType() + { + return apiType; + } + + public int getMask() + { + return mask; + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/worlds/WorldController.java b/http-service/src/main/java/net/runelite/http/service/worlds/WorldController.java new file mode 100644 index 0000000000..537e252b96 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/worlds/WorldController.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2018, 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.service.worlds; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import net.runelite.http.api.worlds.WorldResult; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.CacheControl; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/worlds") +public class WorldController +{ + @Autowired + private WorldsService worldsService; + + private WorldResult worldResult; + + @GetMapping + public ResponseEntity listWorlds() throws IOException + { + return ResponseEntity.ok() + .cacheControl(CacheControl.maxAge(10, TimeUnit.MINUTES).cachePublic()) + .body(worldResult); + } + + @Scheduled(fixedDelay = 60_000L) + public void refreshWorlds() throws IOException + { + worldResult = worldsService.getWorlds(); + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/worlds/WorldsService.java b/http-service/src/main/java/net/runelite/http/service/worlds/WorldsService.java new file mode 100644 index 0000000000..a3bd010430 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/worlds/WorldsService.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2017, 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.service.worlds; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import net.runelite.http.api.RuneLiteAPI; +import net.runelite.http.api.worlds.World; +import net.runelite.http.api.worlds.WorldResult; +import net.runelite.http.api.worlds.WorldType; +import okhttp3.HttpUrl; +import okhttp3.Request; +import okhttp3.Response; +import org.springframework.stereotype.Service; + +@Service +public class WorldsService +{ + private static final HttpUrl WORLD_URL = HttpUrl.parse("http://www.runescape.com/g=oldscape/slr.ws?order=LPWM"); + + private HttpUrl url = WORLD_URL; + + public WorldResult getWorlds() throws IOException + { + Request okrequest = new Request.Builder() + .url(url) + .build(); + + byte[] b; + + try (Response okresponse = RuneLiteAPI.CLIENT.newCall(okrequest).execute()) + { + b = okresponse.body().bytes(); + } + + List worlds = new ArrayList<>(); + ByteBuffer buf = ByteBuffer.wrap(b); + + int length = buf.getInt(); + buf.limit(length + 4); + + int num = buf.getShort() & 0xFFFF; + + for (int i = 0; i < num; ++i) + { + final World.WorldBuilder worldBuilder = World.builder() + .id(buf.getShort() & 0xFFFF) + .types(getTypes(buf.getInt())) + .address(readString(buf)) + .activity(readString(buf)) + .location(buf.get() & 0xFF) + .players(buf.getShort() & 0xFFFF); + + worlds.add(worldBuilder.build()); + } + + WorldResult result = new WorldResult(); + result.setWorlds(worlds); + return result; + } + + private static EnumSet getTypes(int mask) + { + EnumSet types = EnumSet.noneOf(WorldType.class); + + for (ServiceWorldType type : ServiceWorldType.values()) + { + if ((mask & type.getMask()) != 0) + { + types.add(type.getApiType()); + } + } + + return types; + } + + private static String readString(ByteBuffer buf) + { + byte b; + StringBuilder sb = new StringBuilder(); + + for (;;) + { + b = buf.get(); + + if (b == 0) + { + break; + } + + sb.append((char) b); + } + + return sb.toString(); + } + + public HttpUrl getUrl() + { + return url; + } + + public void setUrl(HttpUrl url) + { + this.url = url; + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/xp/XpMapper.java b/http-service/src/main/java/net/runelite/http/service/xp/XpMapper.java new file mode 100644 index 0000000000..068c6e5438 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/xp/XpMapper.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2018, 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.service.xp; + +import net.runelite.http.api.hiscore.HiscoreResult; +import net.runelite.http.api.xp.XpData; +import net.runelite.http.service.xp.beans.XpEntity; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface XpMapper +{ + XpMapper INSTANCE = Mappers.getMapper(XpMapper.class); + + XpData xpEntityToXpData(XpEntity xpEntity); + + @Mapping(target = "time", ignore = true) + + @Mapping(source = "attack.experience", target = "attack_xp") + @Mapping(source = "defence.experience", target = "defence_xp") + @Mapping(source = "strength.experience", target = "strength_xp") + @Mapping(source = "hitpoints.experience", target = "hitpoints_xp") + @Mapping(source = "ranged.experience", target = "ranged_xp") + @Mapping(source = "prayer.experience", target = "prayer_xp") + @Mapping(source = "magic.experience", target = "magic_xp") + @Mapping(source = "cooking.experience", target = "cooking_xp") + @Mapping(source = "woodcutting.experience", target = "woodcutting_xp") + @Mapping(source = "fletching.experience", target = "fletching_xp") + @Mapping(source = "fishing.experience", target = "fishing_xp") + @Mapping(source = "firemaking.experience", target = "firemaking_xp") + @Mapping(source = "crafting.experience", target = "crafting_xp") + @Mapping(source = "smithing.experience", target = "smithing_xp") + @Mapping(source = "mining.experience", target = "mining_xp") + @Mapping(source = "herblore.experience", target = "herblore_xp") + @Mapping(source = "agility.experience", target = "agility_xp") + @Mapping(source = "thieving.experience", target = "thieving_xp") + @Mapping(source = "slayer.experience", target = "slayer_xp") + @Mapping(source = "farming.experience", target = "farming_xp") + @Mapping(source = "runecraft.experience", target = "runecraft_xp") + @Mapping(source = "hunter.experience", target = "hunter_xp") + @Mapping(source = "construction.experience", target = "construction_xp") + + @Mapping(source = "overall.rank", target = "overall_rank") + @Mapping(source = "attack.rank", target = "attack_rank") + @Mapping(source = "defence.rank", target = "defence_rank") + @Mapping(source = "strength.rank", target = "strength_rank") + @Mapping(source = "hitpoints.rank", target = "hitpoints_rank") + @Mapping(source = "ranged.rank", target = "ranged_rank") + @Mapping(source = "prayer.rank", target = "prayer_rank") + @Mapping(source = "magic.rank", target = "magic_rank") + @Mapping(source = "cooking.rank", target = "cooking_rank") + @Mapping(source = "woodcutting.rank", target = "woodcutting_rank") + @Mapping(source = "fletching.rank", target = "fletching_rank") + @Mapping(source = "fishing.rank", target = "fishing_rank") + @Mapping(source = "firemaking.rank", target = "firemaking_rank") + @Mapping(source = "crafting.rank", target = "crafting_rank") + @Mapping(source = "smithing.rank", target = "smithing_rank") + @Mapping(source = "mining.rank", target = "mining_rank") + @Mapping(source = "herblore.rank", target = "herblore_rank") + @Mapping(source = "agility.rank", target = "agility_rank") + @Mapping(source = "thieving.rank", target = "thieving_rank") + @Mapping(source = "slayer.rank", target = "slayer_rank") + @Mapping(source = "farming.rank", target = "farming_rank") + @Mapping(source = "runecraft.rank", target = "runecraft_rank") + @Mapping(source = "hunter.rank", target = "hunter_rank") + @Mapping(source = "construction.rank", target = "construction_rank") + XpData hiscoreResultToXpData(HiscoreResult hiscoreResult); +} diff --git a/http-service/src/main/java/net/runelite/http/service/xp/XpTrackerController.java b/http-service/src/main/java/net/runelite/http/service/xp/XpTrackerController.java new file mode 100644 index 0000000000..d247d735a4 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/xp/XpTrackerController.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2018, 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.service.xp; + +import java.time.Instant; +import net.runelite.http.api.xp.XpData; +import net.runelite.http.service.xp.beans.XpEntity; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/xp") +public class XpTrackerController +{ + @Autowired + private XpTrackerService xpTrackerService; + + @GetMapping("/update") + public void update(@RequestParam String username) + { + xpTrackerService.tryUpdate(username); + } + + @GetMapping("/get") + public XpData get(@RequestParam String username, @RequestParam(required = false) Instant time) + { + if (time == null) + { + time = Instant.now(); + } + XpEntity xpEntity = xpTrackerService.findXpAtTime(username, time); + return XpMapper.INSTANCE.xpEntityToXpData(xpEntity); + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/xp/XpTrackerService.java b/http-service/src/main/java/net/runelite/http/service/xp/XpTrackerService.java new file mode 100644 index 0000000000..9e8e0a5e06 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/xp/XpTrackerService.java @@ -0,0 +1,307 @@ +/* + * Copyright (c) 2018, 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.service.xp; + +import com.google.common.hash.BloomFilter; +import com.google.common.hash.Funnels; +import java.nio.charset.Charset; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayDeque; +import java.util.Queue; +import java.util.concurrent.ExecutionException; +import lombok.extern.slf4j.Slf4j; +import net.runelite.http.api.hiscore.HiscoreEndpoint; +import net.runelite.http.api.hiscore.HiscoreResult; +import net.runelite.http.api.xp.XpData; +import net.runelite.http.service.hiscore.HiscoreService; +import net.runelite.http.service.xp.beans.PlayerEntity; +import net.runelite.http.service.xp.beans.XpEntity; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.sql2o.Connection; +import org.sql2o.Sql2o; + +@Service +@Slf4j +public class XpTrackerService +{ + private static final int QUEUE_LIMIT = 32768; + private static final int BLOOMFILTER_EXPECTED_INSERTIONS = 100_000; + + @Autowired + @Qualifier("Runelite XP Tracker SQL2O") + private Sql2o sql2o; + + @Autowired + private HiscoreService hiscoreService; + + private final Queue usernameUpdateQueue = new ArrayDeque<>(); + private BloomFilter usernameFilter = createFilter(); + + public void update(String username) throws ExecutionException + { + HiscoreResult hiscoreResult = hiscoreService.lookupUsername(username, HiscoreEndpoint.NORMAL); + update(username, hiscoreResult); + } + + public void tryUpdate(String username) + { + if (usernameFilter.mightContain(username)) + { + return; + } + + try (Connection con = sql2o.open()) + { + PlayerEntity playerEntity = findOrCreatePlayer(con, username); + Duration frequency = updateFrequency(playerEntity); + Instant now = Instant.now(); + Duration timeSinceLastUpdate = Duration.between(playerEntity.getLast_updated(), now); + if (timeSinceLastUpdate.toMillis() < frequency.toMillis()) + { + log.debug("User {} updated too recently", username); + usernameFilter.put(username); + return; + } + + synchronized (usernameUpdateQueue) + { + if (usernameUpdateQueue.size() >= QUEUE_LIMIT) + { + log.warn("Username update queue is full ({})", QUEUE_LIMIT); + return; + } + + usernameUpdateQueue.add(username); + } + } + + usernameFilter.put(username); + } + + public void update(String username, HiscoreResult hiscoreResult) + { + try (Connection con = sql2o.open()) + { + PlayerEntity playerEntity = findOrCreatePlayer(con, username); + + Instant now = Instant.now(); + XpEntity currentXp = findXpAtTime(con, username, now); + if (currentXp != null) + { + XpData hiscoreData = XpMapper.INSTANCE.hiscoreResultToXpData(hiscoreResult); + XpData existingData = XpMapper.INSTANCE.xpEntityToXpData(currentXp); + + if (hiscoreData.equals(existingData)) + { + log.debug("Hiscore for {} already up to date", username); + return; + } + } + + con.createQuery("insert into xp (player,attack_xp,defence_xp,strength_xp,hitpoints_xp,ranged_xp,prayer_xp,magic_xp,cooking_xp,woodcutting_xp," + + "fletching_xp,fishing_xp,firemaking_xp,crafting_xp,smithing_xp,mining_xp,herblore_xp,agility_xp,thieving_xp,slayer_xp,farming_xp," + + "runecraft_xp,hunter_xp,construction_xp,attack_rank,defence_rank,strength_rank,hitpoints_rank,ranged_rank,prayer_rank,magic_rank," + + "cooking_rank,woodcutting_rank,fletching_rank,fishing_rank,firemaking_rank,crafting_rank,smithing_rank,mining_rank,herblore_rank," + + "agility_rank,thieving_rank,slayer_rank,farming_rank,runecraft_rank,hunter_rank,construction_rank,overall_rank) values (:player,:attack_xp,:defence_xp," + + ":strength_xp,:hitpoints_xp,:ranged_xp,:prayer_xp,:magic_xp,:cooking_xp,:woodcutting_xp,:fletching_xp,:fishing_xp,:firemaking_xp," + + ":crafting_xp,:smithing_xp,:mining_xp,:herblore_xp,:agility_xp,:thieving_xp,:slayer_xp,:farming_xp,:runecraft_xp,:hunter_xp," + + ":construction_xp,:attack_rank,:defence_rank,:strength_rank,:hitpoints_rank,:ranged_rank,:prayer_rank,:magic_rank,:cooking_rank," + + ":woodcutting_rank,:fletching_rank,:fishing_rank,:firemaking_rank,:crafting_rank,:smithing_rank,:mining_rank,:herblore_rank," + + ":agility_rank,:thieving_rank,:slayer_rank,:farming_rank,:runecraft_rank,:hunter_rank,:construction_rank,:overall_rank)") + .addParameter("player", playerEntity.getId()) + .addParameter("attack_xp", hiscoreResult.getAttack().getExperience()) + .addParameter("defence_xp", hiscoreResult.getDefence().getExperience()) + .addParameter("strength_xp", hiscoreResult.getStrength().getExperience()) + .addParameter("hitpoints_xp", hiscoreResult.getHitpoints().getExperience()) + .addParameter("ranged_xp", hiscoreResult.getRanged().getExperience()) + .addParameter("prayer_xp", hiscoreResult.getPrayer().getExperience()) + .addParameter("magic_xp", hiscoreResult.getMagic().getExperience()) + .addParameter("cooking_xp", hiscoreResult.getCooking().getExperience()) + .addParameter("woodcutting_xp", hiscoreResult.getWoodcutting().getExperience()) + .addParameter("fletching_xp", hiscoreResult.getFletching().getExperience()) + .addParameter("fishing_xp", hiscoreResult.getFishing().getExperience()) + .addParameter("firemaking_xp", hiscoreResult.getFiremaking().getExperience()) + .addParameter("crafting_xp", hiscoreResult.getCrafting().getExperience()) + .addParameter("smithing_xp", hiscoreResult.getSmithing().getExperience()) + .addParameter("mining_xp", hiscoreResult.getMining().getExperience()) + .addParameter("herblore_xp", hiscoreResult.getHerblore().getExperience()) + .addParameter("agility_xp", hiscoreResult.getAgility().getExperience()) + .addParameter("thieving_xp", hiscoreResult.getThieving().getExperience()) + .addParameter("slayer_xp", hiscoreResult.getSlayer().getExperience()) + .addParameter("farming_xp", hiscoreResult.getFarming().getExperience()) + .addParameter("runecraft_xp", hiscoreResult.getRunecraft().getExperience()) + .addParameter("hunter_xp", hiscoreResult.getHunter().getExperience()) + .addParameter("construction_xp", hiscoreResult.getConstruction().getExperience()) + .addParameter("attack_rank", hiscoreResult.getAttack().getRank()) + .addParameter("defence_rank", hiscoreResult.getDefence().getRank()) + .addParameter("strength_rank", hiscoreResult.getStrength().getRank()) + .addParameter("hitpoints_rank", hiscoreResult.getHitpoints().getRank()) + .addParameter("ranged_rank", hiscoreResult.getRanged().getRank()) + .addParameter("prayer_rank", hiscoreResult.getPrayer().getRank()) + .addParameter("magic_rank", hiscoreResult.getMagic().getRank()) + .addParameter("cooking_rank", hiscoreResult.getCooking().getRank()) + .addParameter("woodcutting_rank", hiscoreResult.getWoodcutting().getRank()) + .addParameter("fletching_rank", hiscoreResult.getFletching().getRank()) + .addParameter("fishing_rank", hiscoreResult.getFishing().getRank()) + .addParameter("firemaking_rank", hiscoreResult.getFiremaking().getRank()) + .addParameter("crafting_rank", hiscoreResult.getCrafting().getRank()) + .addParameter("smithing_rank", hiscoreResult.getSmithing().getRank()) + .addParameter("mining_rank", hiscoreResult.getMining().getRank()) + .addParameter("herblore_rank", hiscoreResult.getHerblore().getRank()) + .addParameter("agility_rank", hiscoreResult.getAgility().getRank()) + .addParameter("thieving_rank", hiscoreResult.getThieving().getRank()) + .addParameter("slayer_rank", hiscoreResult.getSlayer().getRank()) + .addParameter("farming_rank", hiscoreResult.getFarming().getRank()) + .addParameter("runecraft_rank", hiscoreResult.getRunecraft().getRank()) + .addParameter("hunter_rank", hiscoreResult.getHunter().getRank()) + .addParameter("construction_rank", hiscoreResult.getConstruction().getRank()) + .addParameter("overall_rank", hiscoreResult.getOverall().getRank()) + .executeUpdate(); + + con.createQuery("update player set rank = :rank, last_updated = CURRENT_TIMESTAMP where id = :id") + .addParameter("id", playerEntity.getId()) + .addParameter("rank", hiscoreResult.getOverall().getRank()) + .executeUpdate(); + } + } + + private synchronized PlayerEntity findOrCreatePlayer(Connection con, String username) + { + PlayerEntity playerEntity = con.createQuery("select * from player where name = :name") + .addParameter("name", username) + .executeAndFetchFirst(PlayerEntity.class); + if (playerEntity != null) + { + return playerEntity; + } + + Instant now = Instant.now(); + + int id = con.createQuery("insert into player (name, tracked_since) values (:name, :tracked_since)") + .addParameter("name", username) + .addParameter("tracked_since", now) + .executeUpdate() + .getKey(int.class); + + playerEntity = new PlayerEntity(); + playerEntity.setId(id); + playerEntity.setName(username); + playerEntity.setTracked_since(now); + playerEntity.setLast_updated(now); + return playerEntity; + } + + private XpEntity findXpAtTime(Connection con, String username, Instant time) + { + return con.createQuery("select * from xp join player on player.id=xp.player where player.name = :username and time <= :time order by time desc limit 1") + .throwOnMappingFailure(false) + .addParameter("username", username) + .addParameter("time", time) + .executeAndFetchFirst(XpEntity.class); + } + + public XpEntity findXpAtTime(String username, Instant time) + { + try (Connection con = sql2o.open()) + { + return findXpAtTime(con, username, time); + } + } + + @Scheduled(fixedDelay = 1000) + public void update() throws ExecutionException + { + String next; + synchronized (usernameUpdateQueue) + { + next = usernameUpdateQueue.poll(); + } + + if (next == null) + { + return; + } + + update(next); + } + + @Scheduled(fixedDelay = 6 * 60 * 60 * 1000) // 6 hours + public void clearFilter() + { + usernameFilter = createFilter(); + } + + private BloomFilter createFilter() + { + final BloomFilter filter = BloomFilter.create( + Funnels.stringFunnel(Charset.defaultCharset()), + BLOOMFILTER_EXPECTED_INSERTIONS + ); + + synchronized (usernameUpdateQueue) + { + for (String toUpdate : usernameUpdateQueue) + { + filter.put(toUpdate); + } + } + + return filter; + } + + /** + * scale how often to check hiscore updates for players based on their rank + * @param playerEntity + * @return + */ + private static Duration updateFrequency(PlayerEntity playerEntity) + { + Integer rank = playerEntity.getRank(); + if (rank == null || rank == -1) + { + return Duration.ofDays(7); + } + else if (rank < 10_000) + { + return Duration.ofHours(6); + } + else if (rank < 50_000) + { + return Duration.ofDays(2); + } + else if (rank < 100_000) + { + return Duration.ofDays(5); + } + else + { + return Duration.ofDays(7); + } + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/xp/beans/PlayerEntity.java b/http-service/src/main/java/net/runelite/http/service/xp/beans/PlayerEntity.java new file mode 100644 index 0000000000..11bec532d7 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/xp/beans/PlayerEntity.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2018, 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.service.xp.beans; + +import java.time.Instant; +import lombok.Data; + +@Data +public class PlayerEntity +{ + private Integer id; + private String name; + private Instant tracked_since; + private Instant last_updated; + private Integer rank; +} diff --git a/http-service/src/main/java/net/runelite/http/service/xp/beans/XpEntity.java b/http-service/src/main/java/net/runelite/http/service/xp/beans/XpEntity.java new file mode 100644 index 0000000000..acab775873 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/xp/beans/XpEntity.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2018, 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.service.xp.beans; + +import java.time.Instant; +import lombok.Data; + +@Data +public class XpEntity +{ + private Integer id; + private Instant time; + private Integer player; + private int overall_xp; + private int attack_xp; + private int defence_xp; + private int strength_xp; + private int hitpoints_xp; + private int ranged_xp; + private int prayer_xp; + private int magic_xp; + private int cooking_xp; + private int woodcutting_xp; + private int fletching_xp; + private int fishing_xp; + private int firemaking_xp; + private int crafting_xp; + private int smithing_xp; + private int mining_xp; + private int herblore_xp; + private int agility_xp; + private int thieving_xp; + private int slayer_xp; + private int farming_xp; + private int runecraft_xp; + private int hunter_xp; + private int construction_xp; + + private int overall_rank; + private int attack_rank; + private int defence_rank; + private int strength_rank; + private int hitpoints_rank; + private int ranged_rank; + private int prayer_rank; + private int magic_rank; + private int cooking_rank; + private int woodcutting_rank; + private int fletching_rank; + private int fishing_rank; + private int firemaking_rank; + private int crafting_rank; + private int smithing_rank; + private int mining_rank; + private int herblore_rank; + private int agility_rank; + private int thieving_rank; + private int slayer_rank; + private int farming_rank; + private int runecraft_rank; + private int hunter_rank; + private int construction_rank; +} diff --git a/http-service/src/main/java/net/runelite/http/service/xtea/XteaController.java b/http-service/src/main/java/net/runelite/http/service/xtea/XteaController.java index b0c592fc70..3868acb8c6 100644 --- a/http-service/src/main/java/net/runelite/http/service/xtea/XteaController.java +++ b/http-service/src/main/java/net/runelite/http/service/xtea/XteaController.java @@ -42,7 +42,7 @@ import org.springframework.web.bind.annotation.RestController; public class XteaController { @Autowired - private XteaEndpoint xteaService; + private XteaService xteaService; @RequestMapping(method = POST) public void submit(@RequestBody XteaRequest xteaRequest) diff --git a/http-service/src/main/java/net/runelite/http/service/xtea/XteaService.java b/http-service/src/main/java/net/runelite/http/service/xtea/XteaService.java new file mode 100644 index 0000000000..61650b4e51 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/xtea/XteaService.java @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2017, 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.service.xtea; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import java.io.IOException; +import java.util.List; +import net.runelite.cache.IndexType; +import net.runelite.cache.fs.Container; +import net.runelite.cache.util.Djb2; +import net.runelite.http.api.xtea.XteaKey; +import net.runelite.http.api.xtea.XteaRequest; +import net.runelite.http.service.cache.CacheService; +import net.runelite.http.service.cache.beans.ArchiveEntry; +import net.runelite.http.service.cache.beans.CacheEntry; +import net.runelite.http.service.util.exception.InternalServerErrorException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; +import org.sql2o.Connection; +import org.sql2o.Query; +import org.sql2o.Sql2o; + +@Service +public class XteaService +{ + private static final String CREATE_SQL = "CREATE TABLE IF NOT EXISTS `xtea` (\n" + + " `id` int(11) NOT NULL AUTO_INCREMENT,\n" + + " `region` int(11) NOT NULL,\n" + + " `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n" + + " `rev` int(11) NOT NULL,\n" + + " `key1` int(11) NOT NULL,\n" + + " `key2` int(11) NOT NULL,\n" + + " `key3` int(11) NOT NULL,\n" + + " `key4` int(11) NOT NULL,\n" + + " PRIMARY KEY (`id`),\n" + + " KEY `region` (`region`,`time`)\n" + + ") ENGINE=InnoDB"; + + private final Sql2o sql2o; + private final CacheService cacheService; + + private final Cache keyCache = CacheBuilder.newBuilder() + .maximumSize(1024) + .build(); + + @Autowired + public XteaService( + @Qualifier("Runelite SQL2O") Sql2o sql2o, + CacheService cacheService + ) + { + this.sql2o = sql2o; + this.cacheService = cacheService; + + try (Connection con = sql2o.beginTransaction()) + { + con.createQuery(CREATE_SQL) + .executeUpdate(); + } + } + + private XteaEntry findLatestXtea(Connection con, int region) + { + return con.createQuery("select region, time, key1, key2, key3, key4 from xtea " + + "where region = :region " + + "order by time desc " + + "limit 1") + .addParameter("region", region) + .executeAndFetchFirst(XteaEntry.class); + } + + public void submit(XteaRequest xteaRequest) + { + boolean cached = true; + for (XteaKey key : xteaRequest.getKeys()) + { + int region = key.getRegion(); + int[] keys = key.getKeys(); + + XteaCache xteaCache = keyCache.getIfPresent(region); + if (xteaCache == null + || xteaCache.getKey1() != keys[0] + || xteaCache.getKey2() != keys[1] + || xteaCache.getKey3() != keys[2] + || xteaCache.getKey4() != keys[3]) + { + cached = false; + keyCache.put(region, new XteaCache(region, keys[0], keys[1], keys[2], keys[3])); + } + } + + if (cached) + { + return; + } + + try (Connection con = sql2o.beginTransaction()) + { + CacheEntry cache = cacheService.findMostRecent(); + + if (cache == null) + { + throw new InternalServerErrorException("No most recent cache"); + } + + Query query = null; + + for (XteaKey key : xteaRequest.getKeys()) + { + int region = key.getRegion(); + int[] keys = key.getKeys(); + + XteaEntry xteaEntry = findLatestXtea(con, region); + + if (keys.length != 4) + { + throw new IllegalArgumentException("Key length must be 4"); + } + + // already have these? + if (xteaEntry != null + && xteaEntry.getKey1() == keys[0] + && xteaEntry.getKey2() == keys[1] + && xteaEntry.getKey3() == keys[2] + && xteaEntry.getKey4() == keys[3]) + { + continue; + } + + if (!checkKeys(cache, region, keys)) + { + continue; + } + + if (query == null) + { + query = con.createQuery("insert into xtea (region, rev, key1, key2, key3, key4) " + + "values (:region, :rev, :key1, :key2, :key3, :key4)"); + } + + query.addParameter("region", region) + .addParameter("rev", xteaRequest.getRevision()) + .addParameter("key1", keys[0]) + .addParameter("key2", keys[1]) + .addParameter("key3", keys[2]) + .addParameter("key4", keys[3]) + .addToBatch(); + } + + if (query != null) + { + query.executeBatch(); + con.commit(false); + } + } + } + + public List get() + { + try (Connection con = sql2o.open()) + { + return con.createQuery( + "select t1.region, t2.time, t2.rev, t2.key1, t2.key2, t2.key3, t2.key4 from " + + "(select region,max(id) as id from xtea group by region) t1 " + + "join xtea t2 on t1.id = t2.id") + .executeAndFetch(XteaEntry.class); + } + } + + public XteaEntry getRegion(int region) + { + try (Connection con = sql2o.open()) + { + return con.createQuery("select region, time, rev, key1, key2, key3, key4 from xtea " + + "where region = :region order by time desc limit 1") + .addParameter("region", region) + .executeAndFetchFirst(XteaEntry.class); + } + } + + private boolean checkKeys(CacheEntry cache, int regionId, int[] keys) + { + int x = regionId >>> 8; + int y = regionId & 0xFF; + + String archiveName = new StringBuilder() + .append('l') + .append(x) + .append('_') + .append(y) + .toString(); + int archiveNameHash = Djb2.hash(archiveName); + + ArchiveEntry archiveEntry = cacheService.findArchiveForTypeAndName(cache, IndexType.MAPS, archiveNameHash); + if (archiveEntry == null) + { + throw new InternalServerErrorException("Unable to find archive for region"); + } + + byte[] data = cacheService.getArchive(archiveEntry); + if (data == null) + { + throw new InternalServerErrorException("Unable to get archive data"); + } + + try + { + Container.decompress(data, keys); + return true; + } + catch (IOException ex) + { + return false; + } + } +} diff --git a/http-service/src/main/resources/application-dev.yaml b/http-service/src/main/resources/application-dev.yaml index cc2286e9b3..9397951858 100644 --- a/http-service/src/main/resources/application-dev.yaml +++ b/http-service/src/main/resources/application-dev.yaml @@ -26,6 +26,11 @@ datasource: username: runelite password: runelite +# Development mongo +mongo: + jndiName: + host: mongodb://localhost:27017 + # Development oauth callback (without proxy) oauth: callback: http://localhost:8080/account/callback diff --git a/http-service/src/main/resources/application.yaml b/http-service/src/main/resources/application.yaml index 06b5933704..3c6fc451fe 100644 --- a/http-service/src/main/resources/application.yaml +++ b/http-service/src/main/resources/application.yaml @@ -30,6 +30,8 @@ redis: pool.size: 10 host: http://localhost:6379 +mongo: + jndiName: java:comp/env/mongodb/runelite # Twitter client for feed runelite: diff --git a/http-service/src/test/java/net/runelite/http/service/config/ConfigControllerTest.java b/http-service/src/test/java/net/runelite/http/service/config/ConfigControllerTest.java new file mode 100644 index 0000000000..646b19611e --- /dev/null +++ b/http-service/src/test/java/net/runelite/http/service/config/ConfigControllerTest.java @@ -0,0 +1,85 @@ +/* + * 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.service.config; + +import java.io.IOException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import net.runelite.http.service.account.AuthFilter; +import net.runelite.http.service.account.beans.SessionEntry; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringRunner.class) +@WebMvcTest(ConfigController.class) +@ActiveProfiles("test") +public class ConfigControllerTest +{ + @Autowired + private MockMvc mockMvc; + + @MockBean + private ConfigService configService; + + @MockBean + private AuthFilter authFilter; + + @Before + public void before() throws IOException + { + when(authFilter.handle(any(HttpServletRequest.class), any(HttpServletResponse.class))) + .thenReturn(mock(SessionEntry.class)); + + when(configService.setKey(anyInt(), anyString(), anyString())).thenReturn(true); + } + + @Test + public void testSetKey() throws Exception + { + mockMvc.perform(put("/config/key") + .content("value") + .contentType(MediaType.TEXT_PLAIN)) + .andExpect(status().isOk()); + + verify(configService).setKey(anyInt(), eq("key"), eq("value")); + } +} \ No newline at end of file diff --git a/http-service/src/test/java/net/runelite/http/service/config/ConfigServiceTest.java b/http-service/src/test/java/net/runelite/http/service/config/ConfigServiceTest.java new file mode 100644 index 0000000000..64c4ef574c --- /dev/null +++ b/http-service/src/test/java/net/runelite/http/service/config/ConfigServiceTest.java @@ -0,0 +1,55 @@ +/* + * 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.service.config; + +import com.google.common.collect.ImmutableMap; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import org.junit.Test; + +public class ConfigServiceTest +{ + @Test + public void testParseJsonString() + { + assertEquals(1, ConfigService.parseJsonString("1")); + assertEquals(3.14, ConfigService.parseJsonString("3.14")); + assertEquals(1L << 32, ConfigService.parseJsonString("4294967296")); + assertEquals("test", ConfigService.parseJsonString("test")); + assertEquals("test", ConfigService.parseJsonString("\"test\"")); + assertEquals(ImmutableMap.of("key", "value"), ConfigService.parseJsonString("{\"key\": \"value\"}")); + } + + @Test + public void testValidateJson() + { + assertTrue(ConfigService.validateJson("1")); + assertTrue(ConfigService.validateJson("3.14")); + assertTrue(ConfigService.validateJson("test")); + assertTrue(ConfigService.validateJson("\"test\"")); + assertTrue(ConfigService.validateJson("key:value")); + assertTrue(ConfigService.validateJson("{\"key\": \"value\"}")); + } +} \ No newline at end of file diff --git a/http-service/src/test/java/net/runelite/http/service/hiscore/HiscoreServiceTest.java b/http-service/src/test/java/net/runelite/http/service/hiscore/HiscoreServiceTest.java new file mode 100644 index 0000000000..4224205c91 --- /dev/null +++ b/http-service/src/test/java/net/runelite/http/service/hiscore/HiscoreServiceTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2017, 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.service.hiscore; + +import java.io.IOException; +import net.runelite.http.api.hiscore.HiscoreEndpoint; +import net.runelite.http.api.hiscore.HiscoreResult; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class HiscoreServiceTest +{ + private static final String RESPONSE = "654683,705,1304518\n" + + "679419,50,107181\n" + + "550667,48,85764\n" + + "861497,50,101366\n" + + "891591,48,87843\n" + + "-1,1,4\n" + + "840255,27,10073\n" + + "1371912,10,1310\n" + + "432193,56,199795\n" + + "495638,56,198304\n" + + "514466,37,27502\n" + + "456981,54,159727\n" + + "459159,49,93010\n" + + "1028855,8,823\n" + + "862906,29,12749\n" + + "795020,31,16097\n" + + "673591,5,495\n" + + "352676,51,112259\n" + + "428419,40,37235\n" + + "461887,43,51971\n" + + "598582,1,10\n" + + "638177,1,0\n" + + "516239,9,1000\n" + + "492790,1,0\n" + + "-1,-1\n" + + "73,1738\n" + + "-1,-1\n" + + "531,1432\n" + + "324,212\n" + + "8008,131\n" + + "1337,911\n" + + "42,14113\n" + + "1,777\n" + + "254,92\n"; + + private final MockWebServer server = new MockWebServer(); + + @Before + public void before() throws IOException + { + server.enqueue(new MockResponse().setBody(RESPONSE)); + + server.start(); + } + + @After + public void after() throws IOException + { + server.shutdown(); + } + + @Test + public void testNormalLookup() throws Exception + { + HiscoreTestService hiscores = new HiscoreTestService(server.url("/")); + + HiscoreResult result = hiscores.lookupUsername("zezima", HiscoreEndpoint.NORMAL.getHiscoreURL()); + + Assert.assertEquals(50, result.getAttack().getLevel()); + Assert.assertEquals(159727L, result.getFishing().getExperience()); + Assert.assertEquals(492790, result.getConstruction().getRank()); + Assert.assertEquals(1432, result.getClueScrollAll().getLevel()); + Assert.assertEquals(324, result.getClueScrollBeginner().getRank()); + Assert.assertEquals(8008, result.getClueScrollEasy().getRank()); + Assert.assertEquals(911, result.getClueScrollMedium().getLevel()); + Assert.assertEquals(42, result.getClueScrollHard().getRank()); + Assert.assertEquals(777, result.getClueScrollElite().getLevel()); + Assert.assertEquals(254, result.getClueScrollMaster().getRank()); + Assert.assertEquals(-1, result.getLastManStanding().getLevel()); + } + +} diff --git a/http-service/src/test/java/net/runelite/http/service/hiscore/HiscoreTestService.java b/http-service/src/test/java/net/runelite/http/service/hiscore/HiscoreTestService.java new file mode 100644 index 0000000000..bdd59b077a --- /dev/null +++ b/http-service/src/test/java/net/runelite/http/service/hiscore/HiscoreTestService.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2017, 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: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 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 HOLDER 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.service.hiscore; + +import java.io.IOException; +import net.runelite.http.api.hiscore.HiscoreResult; +import okhttp3.HttpUrl; + +class HiscoreTestService extends HiscoreService +{ + private final HttpUrl testUrl; + + HiscoreTestService(HttpUrl testUrl) + { + this.testUrl = testUrl; + } + + @Override + public HiscoreResult lookupUsername(String username, HttpUrl endpoint) throws IOException + { + return super.lookupUsername(username, testUrl); + } +} diff --git a/http-service/src/test/java/net/runelite/http/service/loottracker/LootTrackerControllerTest.java b/http-service/src/test/java/net/runelite/http/service/loottracker/LootTrackerControllerTest.java new file mode 100644 index 0000000000..3c80dd72d4 --- /dev/null +++ b/http-service/src/test/java/net/runelite/http/service/loottracker/LootTrackerControllerTest.java @@ -0,0 +1,92 @@ +/* + * 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.service.loottracker; + +import java.io.IOException; +import java.time.Instant; +import java.util.Collections; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import net.runelite.http.api.RuneLiteAPI; +import net.runelite.http.api.loottracker.GameItem; +import net.runelite.http.api.loottracker.LootRecord; +import net.runelite.http.api.loottracker.LootRecordType; +import net.runelite.http.service.account.AuthFilter; +import net.runelite.http.service.account.beans.SessionEntry; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringRunner.class) +@WebMvcTest(LootTrackerController.class) +@ActiveProfiles("test") +public class LootTrackerControllerTest +{ + @Autowired + private MockMvc mockMvc; + + @MockBean + private LootTrackerService lootTrackerService; + + @MockBean + private AuthFilter authFilter; + + @Before + public void before() throws IOException + { + when(authFilter.handle(any(HttpServletRequest.class), any(HttpServletResponse.class))) + .thenReturn(mock(SessionEntry.class)); + } + + @Test + public void storeLootRecord() throws Exception + { + LootRecord lootRecord = new LootRecord(); + lootRecord.setType(LootRecordType.NPC); + lootRecord.setTime(Instant.now()); + lootRecord.setDrops(Collections.singletonList(new GameItem(4151, 1))); + + String data = RuneLiteAPI.GSON.toJson(lootRecord); + mockMvc.perform(post("/loottracker").content(data).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + verify(lootTrackerService).store(eq(lootRecord), anyInt()); + } +} \ No newline at end of file diff --git a/http-service/src/test/java/net/runelite/http/service/worlds/WorldsServiceTest.java b/http-service/src/test/java/net/runelite/http/service/worlds/WorldsServiceTest.java new file mode 100644 index 0000000000..152a6e4e5a --- /dev/null +++ b/http-service/src/test/java/net/runelite/http/service/worlds/WorldsServiceTest.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2017, 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.service.worlds; + +import java.io.IOException; +import java.io.InputStream; +import net.runelite.http.api.worlds.World; +import net.runelite.http.api.worlds.WorldResult; +import net.runelite.http.api.worlds.WorldType; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okio.Buffer; +import org.junit.After; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import org.junit.Before; +import org.junit.Test; +import org.sql2o.tools.IOUtils; + +public class WorldsServiceTest +{ + + private final MockWebServer server = new MockWebServer(); + + @Before + public void before() throws IOException + { + InputStream in = WorldsServiceTest.class.getResourceAsStream("worldlist"); + byte[] worldData = IOUtils.toByteArray(in); + + Buffer buffer = new Buffer(); + buffer.write(worldData); + + server.enqueue(new MockResponse().setBody(buffer)); + + server.start(); + } + + @After + public void after() throws IOException + { + server.shutdown(); + } + + @Test + public void testListWorlds() throws Exception + { + WorldsService worlds = new WorldsService(); + worlds.setUrl(server.url("/")); + + WorldResult worldResult = worlds.getWorlds(); + assertEquals(82, worldResult.getWorlds().size()); + + World world = worldResult.findWorld(385); + assertNotNull(world); + assertTrue(world.getTypes().contains(WorldType.SKILL_TOTAL)); + } + +} diff --git a/http-service/src/test/java/net/runelite/http/service/xp/XpMapperTest.java b/http-service/src/test/java/net/runelite/http/service/xp/XpMapperTest.java new file mode 100644 index 0000000000..2c24f9cc81 --- /dev/null +++ b/http-service/src/test/java/net/runelite/http/service/xp/XpMapperTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2018, 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.service.xp; + +import net.runelite.http.api.hiscore.HiscoreResult; +import net.runelite.http.api.hiscore.Skill; +import net.runelite.http.api.xp.XpData; +import net.runelite.http.service.xp.beans.XpEntity; +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +public class XpMapperTest +{ + @Test + public void testXpEntityToXpData() + { + XpEntity xpEntity = new XpEntity(); + xpEntity.setAgility_rank(42); + xpEntity.setAgility_xp(9001); + + XpData xpData = XpMapper.INSTANCE.xpEntityToXpData(xpEntity); + assertEquals(42, xpData.getAgility_rank()); + assertEquals(9001, xpData.getAgility_xp()); + } + + @Test + public void testHiscoreResultToXpData() + { + HiscoreResult hiscoreResult = new HiscoreResult(); + hiscoreResult.setAgility(new Skill(42, 9, 9001)); + + XpData xpData = XpMapper.INSTANCE.hiscoreResultToXpData(hiscoreResult); + assertEquals(42, xpData.getAgility_rank()); + assertEquals(9001, xpData.getAgility_xp()); + } + +} diff --git a/http-service/src/test/resources/application-test.yaml b/http-service/src/test/resources/application-test.yaml index 0532963ade..3a8e416a54 100644 --- a/http-service/src/test/resources/application-test.yaml +++ b/http-service/src/test/resources/application-test.yaml @@ -15,3 +15,7 @@ datasource: driverClassName: org.h2.Driver type: org.h2.jdbcx.JdbcDataSource url: jdbc:h2:mem:xptracker + +mongo: + jndiName: + host: mongodb://localhost:27017 \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 65c644bf47..07a276ccbc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -16,6 +16,7 @@ include ':injector-plugin' include ':injected-client' include ':runelite-plugin-archetype' include ':http-service' +include ':http-service-plus' project(':http-api').projectDir = "$rootDir/http-api" as File project(':cache').projectDir = "$rootDir/cache" as File @@ -33,4 +34,5 @@ project(':mixins').projectDir = "$rootDir/runelite-mixins" as File project(':injector-plugin').projectDir = "$rootDir/injector-plugin" as File project(':injected-client').projectDir = "$rootDir/injected-client" as File project(':runelite-plugin-archetype').projectDir = "$rootDir/runelite-plugin-archetype" as File -project(':http-service').projectDir = "$rootDir/http-service" as File \ No newline at end of file +project(':http-service').projectDir = "$rootDir/http-service" as File +project(':http-service-plus').projectDir = "$rootDir/http-service-plus" as File \ No newline at end of file