initial commit of functioning opening tracking

This commit is contained in:
Sheldan
2024-01-06 23:29:25 +01:00
parent b72c68dfe5
commit 45e7982330
176 changed files with 37635 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>dev.sheldan.gw2.tools</groupId>
<artifactId>gw2-tools</artifactId>
<version>0.0.9-SNAPSHOT</version>
</parent>
<artifactId>rest-api</artifactId>
<build>
<sourceDirectory>src/main/kotlin</sourceDirectory>
<testSourceDirectory>src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${maven-failsafe-plugin.version}</version>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
</dependency>
<dependency>
<groupId>dev.sheldan.gw2.tools</groupId>
<artifactId>gw2-api-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>dev.sheldan.gw2.tools</groupId>
<artifactId>database</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.github.microutils</groupId>
<artifactId>kotlin-logging-jvm</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,4 @@
package dev.sheldan.gw2.tools
class ItemNotFoundException(message: String): Throwable(message) {
}

View File

@@ -0,0 +1,33 @@
package dev.sheldan.gw2.tools.api
import dev.sheldan.gw2.tools.loader.AccountLoader
import dev.sheldan.gw2.tools.models.AccountMaterialView
import dev.sheldan.gw2.tools.models.AccountVaultView
import dev.sheldan.gw2.tools.models.AccountWalletView
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.context.annotation.RequestScope
@RestController
@RequestScope
class AccountController(var accountLoader: AccountLoader) {
@GetMapping("/characters")
fun inventory(): List<String>? {
return accountLoader.getCharacters()
}
@GetMapping("/wallet")
fun wallet(): AccountWalletView {
return accountLoader.getWallet()
}
@GetMapping("/bank")
fun bank(): AccountVaultView {
return accountLoader.getBank()
}
@GetMapping("/materials")
fun materials(): AccountMaterialView {
return accountLoader.getMaterials()
}
}

View File

@@ -0,0 +1,15 @@
package dev.sheldan.gw2.tools.api
import dev.sheldan.gw2.tools.service.CacheService
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RestController
@RestController
class CurrencyController(val cacheService: CacheService) {
@PostMapping("/currency-cache")
fun updateCurrencyCache() {
cacheService.reloadCurrencyCache()
}
}

View File

@@ -0,0 +1,12 @@
package dev.sheldan.gw2.tools.api
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
@RestController
class Health {
@GetMapping("/health-check")
fun healCheck(): String {
return "yes"
}
}

View File

@@ -0,0 +1,31 @@
package dev.sheldan.gw2.tools.api
import dev.sheldan.gw2.tools.loader.InventoryLoader
import dev.sheldan.gw2.tools.models.AccountInventory
import dev.sheldan.gw2.tools.models.AccountInventoryView
import dev.sheldan.gw2.tools.models.CharacterInventory
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.context.annotation.RequestScope
@RestController
@RequestScope
class InventoryController(var inventoryLoader: InventoryLoader) {
@GetMapping("/inventory/{name}")
fun characterInventory(@PathVariable("name") characterName: String): CharacterInventory {
return inventoryLoader.getFullCharacterInventory(characterName)
}
@GetMapping("/inventory")
fun completeCharacterInventory(): AccountInventoryView {
return inventoryLoader.getAccountInventory()
}
@GetMapping("/sharedInventory")
fun accountInventory(): AccountInventory {
return inventoryLoader.getSharedInventory()
}
}

View File

@@ -0,0 +1,15 @@
package dev.sheldan.gw2.tools.api
import dev.sheldan.gw2.tools.service.CacheService
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RestController
@RestController
class ItemController(val cacheService: CacheService) {
@PostMapping("/item-cache")
fun updateItemCache() {
cacheService.reloadItemCache()
}
}

View File

@@ -0,0 +1,46 @@
package dev.sheldan.gw2.tools.api
import dev.sheldan.gw2.tools.config.ApiKeyInterceptor
import dev.sheldan.gw2.tools.model.ItemRates
import dev.sheldan.gw2.tools.model.OpeningRequest
import dev.sheldan.gw2.tools.model.OpeningsView
import dev.sheldan.gw2.tools.service.OpeningService
import jakarta.servlet.http.HttpServletRequest
import mu.KotlinLogging
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.*
import org.springframework.web.context.annotation.RequestScope
@RestController
@RequestScope
class OpeningController(val openingService: OpeningService) {
private val logger = KotlinLogging.logger {}
@PostMapping("/openings")
@ResponseStatus(HttpStatus.CREATED)
fun createOpenings(request: HttpServletRequest, @RequestBody openingRequest: OpeningRequest) {
val apiKey: String = request.getHeader(ApiKeyInterceptor.API_KEY_HEADER_NAME) ?: throw IllegalArgumentException("API key not provided.")
openingService.createOpening(openingRequest, apiKey)
}
@GetMapping("/openings")
fun getOpenings(request: HttpServletRequest, @RequestParam("showOwnOnly") ownOnly: String?): OpeningsView {
val apiKey: String? = request.getHeader(ApiKeyInterceptor.API_KEY_HEADER_NAME)
val showOnOnly = ownOnly.toBoolean()
logger.info { "Retrieving openings" }
val loadedOpenings = openingService.loadOpenings(apiKey, showOnOnly)
logger.info { "Loaded ${loadedOpenings.openings.size} openings" }
return loadedOpenings
}
@DeleteMapping("/openings/{id}")
fun deleteOpening(request: HttpServletRequest, @PathVariable("id") openingId: Int) {
val apiKey: String = request.getHeader(ApiKeyInterceptor.API_KEY_HEADER_NAME) ?: throw IllegalArgumentException("API key not provided.")
openingService.deleteOpening(apiKey, openingId)
}
@GetMapping("/itemRates")
fun getItemRates(): ItemRates {
return openingService.loadItemRates()
}
}

View File

@@ -0,0 +1,23 @@
package dev.sheldan.gw2.tools.api
import dev.sheldan.gw2.tools.ItemNotFoundException
import dev.sheldan.gw2.tools.service.SubmissionTemplateService
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
@RestController
class SubmissionTemplateController(
val submissionTemplateService: SubmissionTemplateService
) {
@GetMapping("/submissionTemplates")
@ResponseBody
fun getSubmissionTemplatesForItem(@RequestParam("itemId") itemId: Int?): ResponseEntity<Any> {
try {
val responseObj = itemId?.let { submissionTemplateService.getSubmissionTemplateForItem(itemId) } ?: submissionTemplateService.getSubmissionTemplates()
return ResponseEntity(responseObj, HttpStatus.OK)
} catch (e: ItemNotFoundException) {
return ResponseEntity(e.message, HttpStatus.BAD_REQUEST)
}
}
}

View File

@@ -0,0 +1,12 @@
package dev.sheldan.gw2.tools.config
open class ApiKeyContainer : ApiKey {
var apiKeyValue: String = "";
override fun getApiKey(): String {
return apiKeyValue
}
override fun setApiKey(key: String) {
this.apiKeyValue = key
}
}

View File

@@ -0,0 +1,18 @@
package dev.sheldan.gw2.tools.config
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.web.servlet.HandlerInterceptor
class ApiKeyInterceptor(private val apiKeyContainer: ApiKey) : HandlerInterceptor {
companion object {
const val API_KEY_HEADER_NAME = "api-key"
}
override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
val apiKey: String? = request.getHeader(API_KEY_HEADER_NAME)
apiKey?.let { apiKeyContainer.setApiKey(it) }
return true
}
}

View File

@@ -0,0 +1,26 @@
package dev.sheldan.gw2.tools.config
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.context.annotation.RequestScope
import org.springframework.web.servlet.config.annotation.InterceptorRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
@Configuration
class HeaderInterceptorConfig : WebMvcConfigurer {
override fun addInterceptors(registry: InterceptorRegistry) {
registry.addInterceptor(apiKeyInterceptor())
}
@Bean
fun apiKeyInterceptor(): ApiKeyInterceptor {
return ApiKeyInterceptor(apiContainer())
}
@Bean
@RequestScope
fun apiContainer(): ApiKeyContainer {
return ApiKeyContainer()
}
}

View File

@@ -0,0 +1,5 @@
package dev.sheldan.gw2.tools.model
data class DisplayOpeningItem(val itemId: Int, val change: Int, val itemType: OpeningItemType) {
}

View File

@@ -0,0 +1,10 @@
package dev.sheldan.gw2.tools.model
class ItemDisplay(
val id: Int,
val name: String,
val iconUrl: String,
val description: String,
val rarity: String
) {
}

View File

@@ -0,0 +1,7 @@
package dev.sheldan.gw2.tools.model
import dev.sheldan.gw2.tools.models.EnrichedCurrency
import dev.sheldan.gw2.tools.models.EnrichedItem
class ItemRate(val item: EnrichedItem, val receivedItems: List<EnrichedItem>, val receivedCurrencies: List<EnrichedCurrency>) {
}

View File

@@ -0,0 +1,4 @@
package dev.sheldan.gw2.tools.model
class ItemRates(val itemRates: List<ItemRate>) {
}

View File

@@ -0,0 +1,7 @@
package dev.sheldan.gw2.tools.model
class ItemSubmissionTemplate(
val submissionTemplates: List<SubmissionTemplateDisplay>,
val item: ItemDisplay
) {
}

View File

@@ -0,0 +1,6 @@
package dev.sheldan.gw2.tools.model
class ItemSubmissionTemplateList(
val itemSubmissionTemplates: List<ItemSubmissionTemplate>
) {
}

View File

@@ -0,0 +1,5 @@
package dev.sheldan.gw2.tools.model
enum class OpeningItemType {
CURRENCY, ITEM
}

View File

@@ -0,0 +1,7 @@
package dev.sheldan.gw2.tools.model
data class OpeningRequest(
val items: List<DisplayOpeningItem>,
val description: String?=null,
) {
}

View File

@@ -0,0 +1,13 @@
package dev.sheldan.gw2.tools.model
import dev.sheldan.gw2.tools.models.EnrichedCurrency
import dev.sheldan.gw2.tools.models.EnrichedItem
import java.time.Instant
class OpeningView(
val itemChanges: List<EnrichedItem>,
val currencyChanges: List<EnrichedCurrency>,
val openingId: Int,
val description: String?,
val openingDate: Instant) {
}

View File

@@ -0,0 +1,4 @@
package dev.sheldan.gw2.tools.model
class OpeningsView(val openings: List<OpeningView>) {
}

View File

@@ -0,0 +1,8 @@
package dev.sheldan.gw2.tools.model
class SubmissionTemplateDisplay(
val name: String,
val description: String,
val templateText: String
) {
}

View File

@@ -0,0 +1,62 @@
package dev.sheldan.gw2.tools.service
import dev.sheldan.gw2.tools.loader.CurrencyLoader
import dev.sheldan.gw2.tools.loader.ItemLoader
import mu.KotlinLogging
import org.springframework.stereotype.Component
@Component
class CacheService(
val itemLoader: ItemLoader,
val itemManagement: ItemManagement,
val currencyLoader: CurrencyLoader,
val currencyManagement: CurrencyManagement
) {
private val logger = KotlinLogging.logger {}
fun reloadItemCache() {
val allItemsApi = itemLoader.getAllItems().associateBy { it.id }
logger.info { "Loaded ${allItemsApi.size} items from API" }
val allItemsDb = itemManagement.getItems().associateBy { it.id }
logger.info { "Loaded ${allItemsDb.size} items from DB" }
val missingItems = allItemsApi.keys.minus(allItemsDb.keys)
val newDbItems = missingItems.mapNotNull { itemId ->
val item = allItemsApi[itemId]
item?.let {
itemManagement.createItem(
it.id,
item.name!!,
item.description!!,
item.iconUrl!!,
it.type!!.name,
item.rarity!!.name
)
}
}.toList()
logger.info { "Creating ${newDbItems.size} new items" }
itemManagement.saveItems(newDbItems)
}
fun reloadCurrencyCache() {
val allCurrenciesApi = currencyLoader.getAllCurrencies().associateBy { it.id }
logger.info { "Loaded ${allCurrenciesApi.size} currencies from API" }
val allCurrenciesDb = currencyManagement.getCurrencies().associateBy { it.id }
logger.info { "Loaded ${allCurrenciesDb.size} currencies from DB" }
val missingCurrencies = allCurrenciesApi.keys.minus(allCurrenciesDb.keys)
val newDbCurrencies = missingCurrencies.mapNotNull {
itemId ->
val item = allCurrenciesApi[itemId]
item?.let {
currencyManagement.createCurrency(
it.id,
item.name,
item.description,
item.iconUrl
)
}
}
logger.info { "Creating ${newDbCurrencies.size} new items" }
currencyManagement.saveCurrencies(newDbCurrencies)
}
}

View File

@@ -0,0 +1,138 @@
package dev.sheldan.gw2.tools.service
import dev.sheldan.gw2.tools.entity.Currency
import dev.sheldan.gw2.tools.entity.Item
import dev.sheldan.gw2.tools.entity.Opening
import dev.sheldan.gw2.tools.entity.OpeningItem
import dev.sheldan.gw2.tools.model.*
import dev.sheldan.gw2.tools.models.EnrichedCurrency
import dev.sheldan.gw2.tools.models.EnrichedItem
import dev.sheldan.gw2.tools.models.Gw2TItemType
import org.springframework.stereotype.Component
@Component
class OpeningService(
val userManagement: UserManagement,
val openingManagement: OpeningManagement,
val itemManagement: ItemManagement,
val currencyManagement: CurrencyManagement
) {
fun createOpening(openingRequest: OpeningRequest, token: String) {
val user = userManagement.getOrCreateUser(token)
val itemsWithIds = openingRequest.items.filter { it.itemType == OpeningItemType.ITEM }.associateBy { it.itemId }
val fullItems =
itemManagement.getItems(itemsWithIds.keys.toList()).associateWith { itemsWithIds[it.id]!!.change }
val currenciesWithIds =
openingRequest.items.filter { it.itemType == OpeningItemType.CURRENCY }.associateBy { it.itemId }
val fullCurrencies = currencyManagement.getCurrencies(currenciesWithIds.keys.toList())
.associateWith { currenciesWithIds[it.id]!!.change }
openingManagement.createOpening(user, fullItems, fullCurrencies, openingRequest.description)
}
fun loadOpenings(token: String?, ownOnly: Boolean): OpeningsView {
val openings: List<Opening>;
if (ownOnly && token != null) {
val user = userManagement.getOrCreateUser(token)
openings = openingManagement.getOpeningsByUser(user)
} else {
openings = openingManagement.getAllOpenings()
}
val items = openings.filter { it.items != null }.flatMap { it.items!! }
val itemIds = items.map { it.item.id }
val loadedItemsMap = itemManagement.getItemsAsMap(itemIds)
val currencies = openings.filter { it.currencies != null }.flatMap { it.currencies!! }
val currencyIds = currencies.map { it.currency.id }
val loadedCurrenciesMapMap = currencyManagement.getCurrenciesAsMap(currencyIds)
val openingViews = openings.map { convertOpening(it, loadedItemsMap, loadedCurrenciesMapMap) }
return OpeningsView(openingViews)
}
fun deleteOpening(token: String, openingId: Int) {
val user = userManagement.getOrCreateUser(token)
val opening = openingManagement.getOpening(openingId)
opening.map {
if (user.id == it.user.id) {
openingManagement.deleteOpening(it)
}
}
}
fun convertOpening(opening: Opening, itemMap: Map<Int, Item>, currencyMap: Map<Int, Currency>): OpeningView {
val items = mutableListOf<EnrichedItem>()
opening.items?.mapNotNull {
val enrichedItem = EnrichedItem(it.item.id, it.count)
itemMap[it.item.id]?.let { item ->
enrichedItem.setValuesFromDbItem(item)
return@mapNotNull enrichedItem
}
}?.let { items.addAll(it) }
val currencies = mutableListOf<EnrichedCurrency>()
opening.currencies?.mapNotNull {
currencyMap[it.currency.id]?.let { currency ->
EnrichedCurrency(currency.id, it.amount, currency.name, currency.description, currency.iconUrl)
}
}?.let { currencies.addAll(it) }
return OpeningView(items, currencies, opening.id!!, opening.description, opening.creationDate!!)
}
fun loadItemRates(): ItemRates {
val openings = openingManagement.getAllOpenings()
val presentItems = openings.filter { it.items != null }.flatMap { it.items!! }.map { it.item }
val presentCurrencies = openings.filter { it.currencies != null }.flatMap { it.currencies!! }.map { it.currency }
val itemIds = presentItems.map { it.id }
val currencyIds = presentCurrencies.map { it.id }
val loadedItemsMap = itemManagement.getItemsAsMap(itemIds)
val loadedCurrenciesMap = currencyManagement.getCurrenciesAsMap(currencyIds)
val allItems = openings.mapNotNull { opening ->
val containers = getListOfContainersFromOpening(opening)
if(containers.size == 1) {
val containerOpeningItem = containers[0]
val enrichedContainerItem = EnrichedItem(containerOpeningItem.item.id, containerOpeningItem.count)
loadedItemsMap[containerOpeningItem.item.id]?.let { item ->
enrichedContainerItem.setValuesFromDbItem(item)
}
val resultingItems = opening.items?.filter { it.count > 0 }?.mapNotNull itemMap@ {
val enrichedItem = EnrichedItem(it.item.id, it.count)
loadedItemsMap[it.item.id]?.let { item ->
enrichedItem.setValuesFromDbItem(item)
return@itemMap enrichedItem
}
}?: listOf()
val resultingCurrencies = opening.currencies?.filter { it.amount > 0 }?.mapNotNull currencyMap@ {
loadedCurrenciesMap[it.currency.id]?.let { currency ->
val enrichedCurrency = EnrichedCurrency(it.currency.id, it.amount, currency.name, currency.description, currency.iconUrl)
return@currencyMap enrichedCurrency
}
}?: listOf()
return@mapNotNull ItemRate(enrichedContainerItem, resultingItems, resultingCurrencies)
} else {
null
}
}
val itemRateMap = mutableMapOf<Int, ItemRate>()
allItems.forEach{ item ->
itemRateMap[item.item.id]?.let { itemRate ->
val existingEnrichedItems = itemRate.receivedItems.associateBy { receivedItem -> receivedItem.id }
item.receivedItems.forEach{receivedItem ->
existingEnrichedItems[receivedItem.id]?.let{
it.count += receivedItem.count
}?: itemRate.receivedItems.addLast(receivedItem)
}
val existingEnrichedCurrencies = itemRate.receivedCurrencies.associateBy { receivedCurrency -> receivedCurrency.id }
item.receivedCurrencies.forEach{receivedCurrency ->
existingEnrichedCurrencies[receivedCurrency.id]?.let{
it.amount += receivedCurrency.amount
}?: itemRate.receivedCurrencies.addLast(receivedCurrency)
}
itemRate.item.count += item.item.count
}?: itemRateMap.put(item.item.id, item)
}
return ItemRates(itemRateMap.values.toList())
}
private fun getListOfContainersFromOpening(opening: Opening): List<OpeningItem> {
return opening.items?.filter { item -> item.count < 0 && item.item.type == Gw2TItemType.CONTAINER.name } ?: listOf()
}
}

View File

@@ -0,0 +1,38 @@
package dev.sheldan.gw2.tools.service
import dev.sheldan.gw2.tools.ItemNotFoundException
import dev.sheldan.gw2.tools.model.ItemDisplay
import dev.sheldan.gw2.tools.model.SubmissionTemplateDisplay
import dev.sheldan.gw2.tools.model.ItemSubmissionTemplate
import dev.sheldan.gw2.tools.model.ItemSubmissionTemplateList
import org.springframework.stereotype.Component
@Component
class SubmissionTemplateService(
val submissionTemplateManagement: SubmissionTemplateManagement,
val itemManagement: ItemManagement
) {
fun getSubmissionTemplateForItem(itemId: Int): ItemSubmissionTemplate {
val item = itemManagement.getItem(itemId)?: throw ItemNotFoundException("Item not found.")
val submissionTemplates = submissionTemplateManagement.getSubmissionTemplatesForItem(item)
val itemDisplay = ItemDisplay(item.id, item.name, item.iconUrl, item.description, item.rarity)
val templateDisplays = submissionTemplates.map {
SubmissionTemplateDisplay(it.name, it.description, it.templateText)
}
return ItemSubmissionTemplate(templateDisplays, itemDisplay)
}
fun getSubmissionTemplates(): ItemSubmissionTemplateList {
val submissionTemplates = submissionTemplateManagement.getAllSubmissionTemplates()
val items = submissionTemplates.associateBy { it.item }.keys
val itemSubmissionTemplates = items.map { item ->
val itemDisplay = ItemDisplay(item.id, item.name, item.iconUrl, item.description, item.rarity)
val submissionTemplatesOfItem = submissionTemplates.filter { it.item.id == item.id }
val templateDisplays = submissionTemplatesOfItem.map {
SubmissionTemplateDisplay(it.name, it.description, it.templateText)
}
ItemSubmissionTemplate(templateDisplays, itemDisplay)
}
return ItemSubmissionTemplateList(itemSubmissionTemplates)
}
}