http-services(-pro): Add back http-service and move custom http-service

This commit is contained in:
Owain van Brakel
2019-08-09 05:22:26 +02:00
parent f85b1254cd
commit 3a92e3b008
109 changed files with 8699 additions and 112 deletions

View File

@@ -0,0 +1,143 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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);
}
});
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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;
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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<ConfigEntry> config = new ArrayList<>();
public Configuration(List<ConfigEntry> config)
{
this.config = config;
}
public List<ConfigEntry> getConfig()
{
return config;
}
}

View File

@@ -48,6 +48,7 @@ public class DiscordMessage
String avatarUrl;
@SerializedName("tts")
boolean textToSpeech;
@Builder.Default
List<DiscordEmbed> embeds = new ArrayList<>();
public DiscordMessage()

View File

@@ -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;

View File

@@ -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'
}

View File

@@ -0,0 +1,203 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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<Class, Converter> 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);
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* 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<HttpMessageConverter<?>> 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);
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* 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);
}
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* 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<String> 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);
}
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* 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;
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* 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<XteaKey> 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;
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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;
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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}}
<details>
<summary><b>Table Of Contents</b></summary>
[toc]
</details>
# 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}}
## <a name="/definitions/{{key}}">{{@key}}</a>
<table>
<tr>
<th>name</th>
<th>type</th>
<th>required</th>
<th>description</th>
<th>example</th>
</tr>
{{#each this.properties}}
<tr>
<td>{{@key}}</td>
<td>
{{#ifeq type "array"}}
{{#items.$ref}}
{{type}}[<a href="{{items.$ref}}">{{basename items.$ref}}</a>]
{{/items.$ref}}
{{^items.$ref}}{{type}}[{{items.type}}]{{/items.$ref}}
{{else}}
{{#$ref}}<a href="{{$ref}}">{{basename $ref}}</a>{{/$ref}}
{{^$ref}}{{type}}{{#format}} ({{format}}){{/format}}{{/$ref}}
{{/ifeq}}
</td>
<td>{{#required}}required{{/required}}{{^required}}optional{{/required}}</td>
<td>{{#description}}{{{description}}}{{/description}}{{^description}}-{{/description}}</td>
<td>{{example}}</td>
</tr>
{{/each}}
</table>
{{/each}}

View File

@@ -0,0 +1,71 @@
{{#deprecated}}-deprecated-{{/deprecated}}
<a id="{{operationId}}">{{summary}}</a>
{{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}}
<table>
<tr>
<th>Name</th>
<th>Located in</th>
<th>Required</th>
<th>Description</th>
<th>Default</th>
<th>Schema</th>
</tr>
{{/if}}
{{#parameters}}
<tr>
<th>{{name}}</th>
<td>{{in}}</td>
<td>{{#if required}}yes{{else}}no{{/if}}</td>
<td>{{description}}{{#if pattern}} (**Pattern**: `{{pattern}}`){{/if}}</td>
<td> - </td>
{{#ifeq in "body"}}
<td>
{{#ifeq schema.type "array"}}Array[<a href="{{schema.items.$ref}}">{{basename schema.items.$ref}}</a>]{{/ifeq}}
{{#schema.$ref}}<a href="{{schema.$ref}}">{{basename schema.$ref}}</a> {{/schema.$ref}}
</td>
{{else}}
{{#ifeq type "array"}}
<td>Array[{{items.type}}] ({{collectionFormat}})</td>
{{else}}
<td>{{type}} {{#format}}({{format}}){{/format}}</td>
{{/ifeq}}
{{/ifeq}}
</tr>
{{/parameters}}
{{#if parameters}}
</table>
{{/if}}
#### Response
{{#if produces}}__Content-Type:__ {{join produces ", "}}{{/if}}
| Status Code | Reason | Response Model |
|-------------|-------------|----------------|
{{#each responses}}| {{@key}} | {{description}} | {{#schema.$ref}}<a href="{{schema.$ref}}">{{basename schema.$ref}}</a>{{/schema.$ref}}{{#ifeq schema.type "array"}}Array[<a href="{{schema.items.$ref}}">{{basename schema.items.$ref}}</a>]{{/ifeq}}{{^schema}} - {{/schema}}|
{{/each}}

View File

@@ -0,0 +1,88 @@
{{#each securityDefinitions}}
### {{@key}}
{{#this}}
{{#ifeq type "oauth2"}}
<table>
<tr>
<th>type</th>
<th colspan="2">{{type}}</th>
</tr>
{{#if description}}
<tr>
<th>description</th>
<th colspan="2">{{description}}</th>
</tr>
{{/if}}
{{#if authorizationUrl}}
<tr>
<th>authorizationUrl</th>
<th colspan="2">{{authorizationUrl}}</th>
</tr>
{{/if}}
{{#if flow}}
<tr>
<th>flow</th>
<th colspan="2">{{flow}}</th>
</tr>
{{/if}}
{{#if tokenUrl}}
<tr>
<th>tokenUrl</th>
<th colspan="2">{{tokenUrl}}</th>
</tr>
{{/if}}
{{#if scopes}}
<tr>
<td rowspan="3">scopes</td>
{{#each scopes}}
<td>{{@key}}</td>
<td>{{this}}</td>
</tr>
<tr>
{{/each}}
</tr>
{{/if}}
</table>
{{/ifeq}}
{{#ifeq type "apiKey"}}
<table>
<tr>
<th>type</th>
<th colspan="2">{{type}}</th>
</tr>
{{#if description}}
<tr>
<th>description</th>
<th colspan="2">{{description}}</th>
</tr>
{{/if}}
{{#if name}}
<tr>
<th>name</th>
<th colspan="2">{{name}}</th>
</tr>
{{/if}}
{{#if in}}
<tr>
<th>in</th>
<th colspan="2">{{in}}</th>
</tr>
{{/if}}
</table>
{{/ifeq}}
{{#ifeq type "basic"}}
<table>
<tr>
<th>type</th>
<th colspan="2">{{type}}</th>
</tr>
{{#if description}}
<tr>
<th>description</th>
<th colspan="2">{{description}}</th>
</tr>
{{/if}}
</table>
{{/ifeq}}
{{/this}}
{{/each}}

View File

@@ -0,0 +1,49 @@
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/3.0.1/github-markdown.min.css"/>
<style>
.markdown-body {
box-sizing: border-box;
min-width: 200px;
max-width: 980px;
margin: 0 auto;
padding: 45px;
}
@media (max-width: 767px) {
.markdown-body {
padding: 15px;
}
}
.markdown-body table {
display: table;
}
</style>
<!-- JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.0/showdown.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/JanLoebel/showdown-toc/src/showdown-toc.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
var markdown = document.querySelector('noscript').innerText
var converter = new showdown.Converter({emoji: true, extensions: ['toc']})
converter.setFlavor('github')
var html = converter.makeHtml(markdown)
document.body.innerHTML = html
})
</script>
<title>{{info.title}} {{info.version}}</title>
</head>
<body class="markdown-body">
<noscript>{{>markdown}}</noscript>
</body>
</html>

View File

@@ -0,0 +1,32 @@
<!--
Copyright (c) 2017, Adam <Adam@sigterm.info>
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.
-->
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<display-name>RuneLite API</display-name>
</web-app>

View File

@@ -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

View File

@@ -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')
}

View File

@@ -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()))

View File

@@ -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

View File

@@ -0,0 +1,278 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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<String, String> 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);
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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;
}
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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;
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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;
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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;
}
}

View File

@@ -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<Object>
public class UserEntry
{
private int id;
private String username;
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType)
public String toString()
{
return true;
return "UserEntry{" + "id=" + id + ", username=" + username + '}';
}
@Override
public Object beforeBodyWrite(
Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> 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;
}
}
public void setId(int id)
{
this.id = id;
}
public String getUsername()
{
return username;
}
public void setUsername(String username)
{
this.username = username;
}
}

View File

@@ -0,0 +1,362 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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<Cache> listCaches()
{
return cacheService.listCaches().stream()
.map(entry -> new Cache(entry.getId(), entry.getRevision(), entry.getDate()))
.collect(Collectors.toList());
}
@GetMapping("{cacheId}")
public List<CacheIndex> listIndexes(@PathVariable int cacheId)
{
CacheEntry cache = cacheService.findCache(cacheId);
if (cache == null)
{
throw new NotFoundException();
}
List<IndexEntry> indexes = cacheService.findIndexesForCache(cache);
return indexes.stream()
.map(entry -> new CacheIndex(entry.getIndexId(), entry.getRevision()))
.collect(Collectors.toList());
}
@GetMapping("{cacheId}/{indexId}")
public List<CacheArchive> 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<ArchiveEntry> 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<byte[]> 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;
}
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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<CacheEntry> 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<IndexEntry> 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<ArchiveEntry> 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<FileEntry> 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);
}
}

View File

@@ -0,0 +1,254 @@
/*
* Copyright (c) 2017-2018, Adam <Adam@sigterm.info>
* 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<FileEntry> 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<CacheEntry> 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<IndexEntry> 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<ArchiveEntry> findArchivesForIndex(IndexEntry indexEntry)
{
try (Connection con = sql2o.open())
{
CacheDAO cacheDao = new CacheDAO();
ResultSetIterable<ArchiveEntry> archiveEntries = cacheDao.findArchivesForIndex(con, indexEntry);
List<ArchiveEntry> 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<ItemDefinition> 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<ItemDefinition> result = new ArrayList<>(archiveFiles.getFiles().size());
for (FSFile file : archiveFiles.getFiles())
{
ItemDefinition itemDef = itemLoader.load(file.getFileId(), file.getContents());
result.add(itemDef);
}
return result;
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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;
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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;
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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;
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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;
}

View File

@@ -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<KillCountKey, Integer> 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;
}
}
@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;
}
}

View File

@@ -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<String> json;
String key = "hosts.w" + Integer.toString(world) + "." + location;
Map<String, String> 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<String, String> taskMap = ImmutableMap.<String, String>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());
}
}
}
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<String, String> 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<String, String> duelsMap = ImmutableMap.<String, String>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());
}
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* 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;
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright (c) 2019, Adam <Adam@sigterm.info>
* 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);
}
}
}

View File

@@ -0,0 +1,287 @@
/*
* Copyright (c) 2017-2019, Adam <Adam@sigterm.info>
* 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<Document> mongoCollection;
@Autowired
public ConfigService(
MongoClient mongoClient
)
{
MongoDatabase database = mongoClient.getDatabase("config");
MongoCollection<Document> 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<String, Object> configMap = getConfig(userId);
if (configMap == null || configMap.isEmpty())
{
return new Configuration(Collections.emptyList());
}
List<ConfigEntry> config = new ArrayList<>();
for (String group : configMap.keySet())
{
// Reserved keys
if (group.startsWith("_") || group.startsWith("$"))
{
continue;
}
Map<String, Object> groupMap = (Map) configMap.get(group);
for (Map.Entry<String, Object> 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<String, JsonElement> 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;
}
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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);
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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;
}
}

View File

@@ -0,0 +1,94 @@
/*
* Copyright (c) 2019, Adam <Adam@sigterm.info>
* 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();
}
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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;
}

View File

@@ -0,0 +1,112 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* 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<FeedItem> 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<FeedResult> getFeed()
{
if (feedResult == null)
{
return ResponseEntity.notFound()
.build();
}
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(10, TimeUnit.MINUTES).cachePublic())
.body(feedResult);
}
}

View File

@@ -0,0 +1,129 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* 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<FeedItem> 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<FeedItem> 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());
}
}
}
}

View File

@@ -0,0 +1,129 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* 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<FeedItem> 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<FeedItem> 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());
}
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* 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;
}

View File

@@ -0,0 +1,178 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* 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<FeedItem> getTweets() throws IOException
{
return getTweets(false);
}
private List<FeedItem> 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<List<TwitterStatusesResponseItem>>()
{
}.getType();
List<TwitterStatusesResponseItem> statusesResponse = RuneLiteAPI.GSON
.fromJson(new InputStreamReader(in), listType);
List<FeedItem> 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;
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* 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;
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* 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;
}

View File

@@ -0,0 +1,111 @@
/*
* Copyright (c) 2019, Adam <Adam@sigterm.info>
* 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<GrandExchangeTrade> 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());
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright (c) 2019, Adam <Adam@sigterm.info>
* 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<TradeEntry> 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();
}
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright (c) 2019, Adam <Adam@sigterm.info>
* 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;
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright (c) 2019, Adam <Adam@sigterm.info>
* 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;
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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());
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* 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;
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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<HiscoreKey, HiscoreResult> hiscoreCache = CacheBuilder.newBuilder()
.maximumSize(128)
.expireAfterWrite(1, TimeUnit.MINUTES)
.build(
new CacheLoader<HiscoreKey, HiscoreResult>()
{
@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));
}
}

View File

@@ -0,0 +1,227 @@
/*
* Copyright (c) 2017-2018, Adam <Adam@sigterm.info>
* 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<Integer, Integer> cachedEmpty = CacheBuilder.newBuilder()
.maximumSize(1024L)
.build();
private final ItemService itemService;
private final Supplier<ItemPrice[]> 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<byte[]> 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<byte[]> 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> 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<ItemEntry> 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<PriceEntry> 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<ItemPrice[]> prices()
{
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.MINUTES).cachePublic())
.body(memorizedPrices.get());
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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;
}
}

View File

@@ -0,0 +1,505 @@
/*
* Copyright (c) 2017-2018, Adam <Adam@sigterm.info>
* 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<PendingLookup> pendingLookups = new ConcurrentLinkedQueue<PendingLookup>();
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<PriceEntry> getPrices(int... itemIds)
{
try (Connection con = sql2o.open())
{
Set<Integer> seen = new HashSet<>();
List<PriceEntry> 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<ItemEntry> 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<PriceEntry> 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<PriceEntry> 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<Long, Integer> 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<PriceEntry> 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> T fetchJson(Request request, Class<T> 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<ItemDefinition> 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);
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* 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;
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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;
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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;
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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;
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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<Long, Integer> daily;
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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<RSItem> items;
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* 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;
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright (c) 2018, TheStonedTurtle <https://github.com/TheStonedTurtle>
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* 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<LootRecord> 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);
}
}

View File

@@ -0,0 +1,196 @@
/*
* Copyright (c) 2018, TheStonedTurtle <https://github.com/TheStonedTurtle>
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* 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<LootRecord> get(int accountId, int limit, int offset)
{
List<LootResult> 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<LootRecord> lootRecords = new ArrayList<>();
List<GameItem> 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();
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (c) 2018, AeonLucid <https://github.com/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;
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (c) 2018, AeonLucid <https://github.com/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<GrandExchangeEntry> get(@RequestParam("itemId") int itemId) throws ExecutionException
{
GrandExchangeEntry grandExchangeEntry = grandExchangeService.get(itemId);
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.MINUTES).cachePublic())
.body(grandExchangeEntry);
}
}

View File

@@ -0,0 +1,119 @@
/*
* Copyright (c) 2018, AeonLucid <https://github.com/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<Integer, OsbuddySummaryItem> 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<Integer, OsbuddySummaryItem> 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);
}
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (c) 2018, AeonLucid <https://github.com/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<Integer, OsbuddySummaryItem> 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<Map<Integer, OsbuddySummaryItem>>()
{
}.getType();
return RuneLiteAPI.GSON.fromJson(responseOk.body().string(), type);
}
catch (JsonSyntaxException ex)
{
throw new IOException(ex);
}
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (c) 2018, AeonLucid <https://github.com/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;
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* 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<Integer, byte[]> spriteCache = CacheBuilder.newBuilder()
.maximumSize(1024L)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(new CacheLoader<Integer, byte[]>()
{
@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<byte[]> 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);
}
}

View File

@@ -0,0 +1,117 @@
/*
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* 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;
}
}

View File

@@ -35,4 +35,4 @@ public class InternalServerErrorException extends RuntimeException
{
super(message);
}
}
}

View File

@@ -32,4 +32,4 @@ import org.springframework.web.bind.annotation.ResponseStatus;
public class NotFoundException extends RuntimeException
{
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* 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;
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* 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<WorldResult> 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();
}
}

View File

@@ -0,0 +1,131 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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<World> 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<WorldType> getTypes(int mask)
{
EnumSet<WorldType> 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;
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* 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);
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* 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);
}
}

View File

@@ -0,0 +1,307 @@
/*
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* 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<String> usernameUpdateQueue = new ArrayDeque<>();
private BloomFilter<String> 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<String> createFilter()
{
final BloomFilter<String> 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);
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* 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;
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* 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;
}

View File

@@ -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)

View File

@@ -0,0 +1,240 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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<Integer, XteaCache> 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<XteaEntry> 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;
}
}
}

View File

@@ -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

View File

@@ -30,6 +30,8 @@ redis:
pool.size: 10
host: http://localhost:6379
mongo:
jndiName: java:comp/env/mongodb/runelite
# Twitter client for feed
runelite:

Some files were not shown because too many files have changed in this diff Show More