2022-03-31 02:51:01 +00:00
package main
import (
2023-01-28 07:42:47 +00:00
"bytes"
2022-04-02 07:11:06 +00:00
"encoding/json"
2022-07-31 18:45:48 +00:00
"fmt"
2022-04-30 05:59:59 +00:00
"io/ioutil"
2022-03-31 02:51:01 +00:00
"log"
2023-01-28 07:42:47 +00:00
"math"
2022-03-31 02:51:01 +00:00
"net/http"
2022-04-03 03:31:17 +00:00
"sort"
2022-04-30 01:00:13 +00:00
"strconv"
2022-09-24 00:15:02 +00:00
"strings"
2022-03-31 02:51:01 +00:00
"time"
2022-07-31 18:45:48 +00:00
2023-11-24 20:42:05 +00:00
inventorytools "code.mashffxiv.deadbeef.codes/MashPotato/gilgetter/inventorytools"
2022-03-31 02:51:01 +00:00
)
// http - GET /
// Serves the home page
2022-07-23 04:40:58 +00:00
func homePageHandler ( w http . ResponseWriter , _ * http . Request ) {
2022-03-31 02:51:01 +00:00
type PageData struct {
Title string
MarketItems [ ] MarketItem
2022-03-31 03:17:36 +00:00
LastPriceUpdate int
2022-03-31 02:51:01 +00:00
PriceUpdateProgress int
}
pageData := PageData {
Title : "Home" ,
2022-04-30 00:26:30 +00:00
LastPriceUpdate : int ( time . Since ( lastFullPriceUpdateTime ) . Minutes ( ) ) ,
2022-03-31 02:51:01 +00:00
PriceUpdateProgress : priceUpdateProgress ,
}
2022-03-31 03:18:45 +00:00
if len ( itemSlice ) > 200 {
pageData . MarketItems = itemSlice [ : 200 ] // truncate to top 200
} else {
pageData . MarketItems = itemSlice
}
2022-03-31 02:51:01 +00:00
err := t . ExecuteTemplate ( w , "home.html" , pageData )
if err != nil {
log . Fatalf ( "failed to execute template: %v" , err )
}
}
2022-04-04 01:46:18 +00:00
// http - GET /stale-items
// Displays a list of items on Universalis that have the most out of date information
// Page is is used to ask users to help contribute data for the items that need it most
2022-07-23 04:40:58 +00:00
func staleItemsPageHandler ( w http . ResponseWriter , _ * http . Request ) {
2022-04-03 00:13:16 +00:00
type PageData struct {
Title string
MarketItems [ ] MarketItem
}
2022-04-04 01:46:18 +00:00
pageData := PageData { Title : "Stale Items" , MarketItems : make ( [ ] MarketItem , 0 ) }
2022-04-03 00:13:16 +00:00
2022-04-03 03:31:17 +00:00
for _ , item := range items {
2022-04-30 05:59:59 +00:00
if item . MarketBoardListing == nil { // If there are no market board listings, we don't care about it
2022-04-03 00:13:16 +00:00
continue
}
2022-04-03 03:31:17 +00:00
pageData . MarketItems = append ( pageData . MarketItems , * ( items [ item . ID ] ) )
2022-04-03 00:13:16 +00:00
}
2022-04-03 03:31:17 +00:00
// Sort the item slice by time last updated
sort . Slice ( pageData . MarketItems , func ( i , j int ) bool {
return pageData . MarketItems [ i ] . MarketBoardListing . LastUploadTime < pageData . MarketItems [ j ] . MarketBoardListing . LastUploadTime
} )
2022-04-03 00:13:16 +00:00
2022-04-03 03:31:17 +00:00
// Truncate the item slice to 200 items
if len ( pageData . MarketItems ) > 200 {
pageData . MarketItems = pageData . MarketItems [ : 200 ]
}
// TBD: could potentially sort by last updated here, however the current data type is a string and that's bad to sort with. Will see if it's needed in practice
2022-04-04 01:46:18 +00:00
err := t . ExecuteTemplate ( w , "stale-items.html" , pageData )
2022-04-03 00:13:16 +00:00
if err != nil {
log . Printf ( "failed to execute template: %v" , err )
}
}
2022-04-30 05:59:59 +00:00
// http - GET /crafting-checkout
// http - POST /crafting-checkout
// When POST, accepts a JSON object detailing what item IDs are selected for crafting, saves to global variable
// When GET, produces a list of the items to crafting, suggested quantities to craft, and materials required
func craftingCheckoutPageHandler ( w http . ResponseWriter , r * http . Request ) {
if r . Method == "GET" {
2022-07-31 23:54:45 +00:00
2023-01-28 07:42:47 +00:00
type AutoXivItem struct {
Row int
Col int
}
2022-07-31 23:54:45 +00:00
type Retainer struct {
2023-01-28 07:42:47 +00:00
RetainerNumber int
Name string
ItemsToGet [ ] inventorytools . Item
ContainerOne [ ] AutoXivItem
ContainerTwo [ ] AutoXivItem
ContainerThree [ ] AutoXivItem
ContainerFour [ ] AutoXivItem
ContainerFive [ ] AutoXivItem
2022-07-31 23:54:45 +00:00
}
type CraftingItem struct {
2022-04-30 05:59:59 +00:00
ID int
Name string
2022-07-23 05:23:26 +00:00
Job string
2022-04-30 05:59:59 +00:00
Quantity int
}
type PageData struct {
2022-07-31 23:54:45 +00:00
Title string
CraftingItems [ ] CraftingItem // The items that will be crafted
Retainers [ ] Retainer // items to get from retainers
MarketItems [ ] inventorytools . Item // items that need to be bought from market
2022-04-30 05:59:59 +00:00
}
2022-07-31 23:54:45 +00:00
pageData := PageData { Title : "Crafting Checkout" , CraftingItems : make ( [ ] CraftingItem , 0 ) , Retainers : make ( [ ] Retainer , 0 ) , MarketItems : make ( [ ] inventorytools . Item , 0 ) }
2022-04-30 05:59:59 +00:00
2022-07-31 23:54:45 +00:00
// Build list of crafting items, and how much materials are required
craftingMaterialMap := make ( map [ int ] int ) // key item ID, value quantity required
2022-04-30 05:59:59 +00:00
for _ , craftingItemID := range craftingCheckoutItemIDs {
2022-07-31 23:54:45 +00:00
craftingItem := CraftingItem { ID : craftingItemID , Name : items [ craftingItemID ] . Name , Job : items [ craftingItemID ] . Job , Quantity : ( items [ craftingItemID ] . Velocity / 2 ) + 1 }
2022-04-30 05:59:59 +00:00
for _ , materialItem := range items [ craftingItemID ] . CraftingMaterials {
if _ , ok := craftingMaterialMap [ materialItem . ID ] ; ok {
// we've seen it before, so we add to it
craftingMaterialMap [ materialItem . ID ] = craftingMaterialMap [ materialItem . ID ] + materialItem . Quantity * craftingItem . Quantity
2022-05-01 02:15:56 +00:00
} else {
// first occurrence
craftingMaterialMap [ materialItem . ID ] = materialItem . Quantity * craftingItem . Quantity
2022-04-30 05:59:59 +00:00
}
}
pageData . CraftingItems = append ( pageData . CraftingItems , craftingItem )
}
2022-12-27 19:02:17 +00:00
// Sort by job to group items to craft together by their job
sort . Slice ( pageData . CraftingItems , func ( i , j int ) bool {
return pageData . CraftingItems [ i ] . Job < pageData . CraftingItems [ j ] . Job
} )
2022-04-30 05:59:59 +00:00
2022-07-31 23:54:45 +00:00
// Subtract materials in character inventory
for _ , item := range inventories . CharacterBags {
if _ , ok := craftingMaterialMap [ item . ID ] ; ok {
craftingMaterialMap [ item . ID ] = craftingMaterialMap [ item . ID ] - item . Quantity
if craftingMaterialMap [ item . ID ] <= 0 {
delete ( craftingMaterialMap , item . ID )
}
}
}
// Materials in retainer inventory and subtract
2023-01-28 07:42:47 +00:00
retainerNumber := 0
2022-07-31 23:54:45 +00:00
for _ , retainer := range inventories . Retainers {
2023-01-28 07:42:47 +00:00
retainerNumber ++
2022-07-31 23:54:45 +00:00
itemsToGet := make ( [ ] inventorytools . Item , 0 )
for _ , item := range retainer . RetainerBags {
if _ , ok := craftingMaterialMap [ item . ID ] ; ok {
// This is an item we need that exists on the retainer
if item . Quantity > craftingMaterialMap [ item . ID ] { // the retainer has more than we need, so adjust quantity down to what we need
item . Quantity = craftingMaterialMap [ item . ID ]
}
// offset quantity and map name from ID
itemToGet := item
itemToGet . Name = items [ itemToGet . ID ] . Name
itemToGet . SortedContainer -= 9999
itemToGet . SortedSlotIndex += 1
itemsToGet = append ( itemsToGet , itemToGet )
craftingMaterialMap [ item . ID ] = craftingMaterialMap [ item . ID ] - item . Quantity
if craftingMaterialMap [ item . ID ] <= 0 {
delete ( craftingMaterialMap , item . ID )
}
}
}
if len ( itemsToGet ) > 0 {
2023-01-28 07:42:47 +00:00
pageData . Retainers = append ( pageData . Retainers , Retainer { Name : retainer . Name , RetainerNumber : retainerNumber , ItemsToGet : itemsToGet } )
2022-07-31 23:54:45 +00:00
}
}
2022-10-03 00:54:32 +00:00
// Subtract crystal bag inventory
for _ , item := range inventories . Crystals {
if _ , ok := craftingMaterialMap [ item . ID ] ; ok {
craftingMaterialMap [ item . ID ] = craftingMaterialMap [ item . ID ] - item . Quantity
if craftingMaterialMap [ item . ID ] <= 0 {
delete ( craftingMaterialMap , item . ID )
}
}
}
2022-07-31 23:54:45 +00:00
// Materials to be bought from market is what's left over
for itemID , itemQuantity := range craftingMaterialMap {
itemToGet := inventorytools . Item { ID : itemID , Name : items [ itemID ] . Name , Quantity : itemQuantity }
pageData . MarketItems = append ( pageData . MarketItems , itemToGet )
2022-04-30 05:59:59 +00:00
}
2022-07-31 23:54:45 +00:00
/ *
for id , quantity := range craftingMaterialMap {
materialItem := TableItem { ID : id , Name : items [ id ] . Name , Quantity : quantity }
pageData . CraftingMaterials = append ( pageData . CraftingMaterials , materialItem )
}
* /
2022-04-30 05:59:59 +00:00
err := t . ExecuteTemplate ( w , "crafting-checkout.html" , pageData )
if err != nil {
2023-01-28 19:22:35 +00:00
w . WriteHeader ( http . StatusInternalServerError )
log . Printf ( "failed to execute crafting-checkout.html template: %v" , err )
return
2022-04-30 05:59:59 +00:00
}
2023-01-28 07:42:47 +00:00
// solve auto-xiv data for template
for i , retainer := range pageData . Retainers {
for _ , item := range retainer . ItemsToGet {
autoXivItem := AutoXivItem { }
autoXivItem . Row = int ( math . Ceil ( float64 ( item . SortedSlotIndex ) / 5 ) )
autoXivItem . Col = item . SortedSlotIndex % 5
if autoXivItem . Col == 0 {
autoXivItem . Col = 5
}
if item . SortedContainer == 1 {
pageData . Retainers [ i ] . ContainerOne = append ( pageData . Retainers [ i ] . ContainerOne , autoXivItem )
} else if item . SortedContainer == 2 {
pageData . Retainers [ i ] . ContainerTwo = append ( pageData . Retainers [ i ] . ContainerTwo , autoXivItem )
} else if item . SortedContainer == 3 {
pageData . Retainers [ i ] . ContainerThree = append ( pageData . Retainers [ i ] . ContainerThree , autoXivItem )
} else if item . SortedContainer == 4 {
pageData . Retainers [ i ] . ContainerFour = append ( pageData . Retainers [ i ] . ContainerFour , autoXivItem )
} else if item . SortedContainer == 5 {
pageData . Retainers [ i ] . ContainerFive = append ( pageData . Retainers [ i ] . ContainerFive , autoXivItem )
}
}
}
2023-01-28 19:22:35 +00:00
buf := bytes . NewBuffer ( nil )
err = t . ExecuteTemplate ( buf , "withdrawitems.ahk" , pageData )
if err != nil {
log . Printf ( "failed to execute withdrawitems.ahk template: %v" , err )
return
}
withdrawItemsAhk , err = ioutil . ReadAll ( buf )
2023-01-28 07:42:47 +00:00
if err != nil {
2023-01-28 19:22:35 +00:00
log . Printf ( "failed to read withdrawitems.ahk processed template buffer to byte slice: %v" , err )
2023-01-28 07:42:47 +00:00
}
2022-04-30 05:59:59 +00:00
} else if r . Method == "POST" {
body , err := ioutil . ReadAll ( r . Body )
if err != nil {
log . Printf ( "failed to read request body on craftingCheckoutPageHandler: %v" , err )
w . WriteHeader ( http . StatusInternalServerError )
return
}
type CraftingCheckoutPageData struct {
ItemIDs [ ] int ` json:"itemids" `
}
craftingCheckoutPageData := CraftingCheckoutPageData { }
err = json . Unmarshal ( body , & craftingCheckoutPageData )
if err != nil {
log . Printf ( "failed to unmarshal craftingCheckoutPageData into struct: %v" , err )
w . WriteHeader ( http . StatusInternalServerError )
return
}
craftingCheckoutItemIDs = make ( [ ] int , 0 )
craftingCheckoutItemIDs = craftingCheckoutPageData . ItemIDs
2022-08-01 17:55:21 +00:00
/ * // This causes velocity to be scaled up and messes up the crafting material quantity calculation
2022-07-31 23:54:45 +00:00
go func ( ) { // queue up datacenter price updates
universalisPriceEndpoint = datacenter
defer func ( ) { universalisPriceEndpoint = world } ( ) // set back to world when done
// build list of crafting supply item IDs to be refreshed for datacenter pricing
itemsToRefresh := make ( map [ int ] bool )
for _ , craftingItemID := range craftingCheckoutItemIDs {
for _ , craftingMaterial := range items [ craftingItemID ] . CraftingMaterials {
itemsToRefresh [ craftingMaterial . ID ] = true
}
}
// queue up datacenter price updates
for itemID := range itemsToRefresh {
priceUpdateQueue <- itemID
updateRequestWg . Add ( 1 )
}
// wait for channel to empty
updateRequestWg . Wait ( )
} ( )
2022-08-01 17:55:21 +00:00
* /
2022-04-30 05:59:59 +00:00
}
}
2023-01-28 07:42:47 +00:00
// http - GET /auto-xiv
//
// Downloads the .ahk file for withdrawing items
func autoXivHandler ( w http . ResponseWriter , _ * http . Request ) {
w . Header ( ) . Set ( "Content-Disposition" , "attachment; filename=withdraw-items.ahk" )
2023-01-28 23:06:54 +00:00
w . Write ( withdrawItemsAhk )
2023-01-28 07:42:47 +00:00
}
2022-04-30 00:33:16 +00:00
// http - GET /full-price-update
// Requests a full price update if criteria is met
2022-07-23 04:40:58 +00:00
func fullPriceUpdateHandler ( w http . ResponseWriter , _ * http . Request ) {
2022-03-31 02:51:01 +00:00
err := fullPriceRefresh ( )
if err != nil {
w . WriteHeader ( http . StatusTooEarly )
return
}
w . WriteHeader ( http . StatusOK )
}
2022-04-30 01:00:13 +00:00
// http - GET /price-update
// Queues a price update for an item after 30 seconds
func singlePriceUpdateHandler ( w http . ResponseWriter , r * http . Request ) {
err := r . ParseForm ( )
if err != nil {
log . Printf ( "failed to parse form in singlePriceUpdateHandler: %v" , err )
w . WriteHeader ( http . StatusInternalServerError )
return
}
itemID , err := strconv . Atoi ( r . FormValue ( "itemid" ) )
if err != nil {
log . Printf ( "failed to convert itemid variable to integer in singlePriceUpdateHandler: %v" , err )
w . WriteHeader ( http . StatusBadRequest )
return
}
w . WriteHeader ( http . StatusAccepted )
// Don't keep the client waiting
go func ( ) {
time . Sleep ( time . Second * 30 )
log . Printf ( "dynamic price update for %d triggered" , itemID )
priceUpdateQueue <- itemID
updateRequestWg . Add ( 1 )
} ( )
}
2022-04-03 06:20:25 +00:00
// http - GET /status
// 5 second polling each client sends to server for updating front end data
2022-07-23 04:40:58 +00:00
func statusHandler ( w http . ResponseWriter , _ * http . Request ) {
2022-04-02 07:11:06 +00:00
type PageData struct {
2022-04-30 00:27:43 +00:00
SecondsRemaining int ` json:"secondsremaining" ` // ETA for how long an ongoing update will take
PriceUpdateProgress int ` json:"priceupdateprogress" ` // if progress is 100 that means there is no price update ongoing
UpdatedMinutesAgo int ` json:"updatedminutesago" ` // How many minutes since the last full price refresh
NewSortAvailable bool ` json:"newsortavailable" ` // True if an updated metric sort is available
2022-04-02 07:11:06 +00:00
}
2022-04-30 00:26:30 +00:00
timePerRequest := time . Since ( startFullPriceUpdateTime ) . Seconds ( ) / float64 ( len ( items ) - len ( priceUpdateQueue ) )
2022-04-02 07:11:06 +00:00
secondsRemaining := float64 ( len ( priceUpdateQueue ) ) * timePerRequest
2022-04-03 06:31:27 +00:00
pageData := PageData {
SecondsRemaining : int ( secondsRemaining ) ,
2022-04-02 07:11:06 +00:00
PriceUpdateProgress : priceUpdateProgress ,
2022-04-30 00:26:30 +00:00
UpdatedMinutesAgo : int ( time . Since ( lastFullPriceUpdateTime ) . Minutes ( ) ) ,
2022-05-18 17:24:18 +00:00
}
if pageData . UpdatedMinutesAgo < 1 {
pageData . NewSortAvailable = false
} else {
pageData . NewSortAvailable = time . Now ( ) . Before ( lastSortTime . Add ( time . Second * 6 ) ) // true if last sort was within 6 seconds
2022-04-02 07:11:06 +00:00
}
2022-04-30 00:33:16 +00:00
2022-04-02 07:11:06 +00:00
jsonBytes , err := json . Marshal ( pageData )
if err != nil {
2022-04-03 06:20:25 +00:00
log . Printf ( "statusHandler failed to marshal json to bytes: %v" , err )
w . WriteHeader ( http . StatusInternalServerError )
return
2022-04-02 07:11:06 +00:00
}
w . Write ( jsonBytes )
2022-03-31 02:51:01 +00:00
}
2022-04-03 22:04:43 +00:00
// http - GET /items-list
// Returns a list of
2022-07-23 04:40:58 +00:00
func itemsListHandler ( w http . ResponseWriter , _ * http . Request ) {
2022-04-03 22:04:43 +00:00
// Remap our data into a smaller object, we don't need full data for the front end
var responseDataItemSlice [ ] MarketItem
if len ( itemSlice ) > 200 {
responseDataItemSlice = itemSlice [ : 200 ] // truncate to top 200
} else {
responseDataItemSlice = itemSlice
}
2022-07-31 21:22:16 +00:00
// use inventory tools to update checkmarks - but universalis code still present as fallback
// expensive?
for i , itemSliceItem := range responseDataItemSlice {
2022-08-20 05:09:17 +00:00
/ *
// Universalis only implementation - requires searching each item on market board to
// properly refresh universalis with up to date data first from the price uploader
// Depracated when inventory tools integration was added (below)
for _ , listing := range itemSliceItem . MarketBoardListing . Listings {
for _ , retainer := range retainers {
if listing . RetainerName == retainer {
itemSliceItem . InStock = true
}
}
}
* /
2022-08-21 00:29:53 +00:00
itemSliceItem . InStock = false // reset it to initial value of false
2022-07-31 21:22:16 +00:00
for _ , retainer := range inventories . Retainers {
found := false
2022-08-11 21:33:46 +00:00
// check market listings
2022-07-31 21:22:16 +00:00
for _ , marketItem := range retainer . RetainerMarket {
if itemSliceItem . ID == marketItem . ID {
found = true
2022-08-11 21:33:46 +00:00
itemSliceItem . InStock = true
2022-07-31 21:22:16 +00:00
break
}
}
if found {
break
}
2022-08-11 21:33:46 +00:00
// check retainer bags
for _ , marketItem := range retainer . RetainerBags {
if itemSliceItem . ID == marketItem . ID {
found = true
itemSliceItem . InStock = true
break
}
}
if found {
break
}
}
// check player bags
for _ , marketItem := range inventories . CharacterBags {
if itemSliceItem . ID == marketItem . ID {
itemSliceItem . InStock = true
break
}
2022-07-31 21:22:16 +00:00
}
responseDataItemSlice [ i ] = itemSliceItem
}
2022-07-23 05:23:26 +00:00
jsonBytes , err := json . Marshal ( responseDataItemSlice )
2022-04-03 22:04:43 +00:00
if err != nil {
log . Printf ( "itemsListHandler failed to marshal json to bytes: %v" , err )
w . WriteHeader ( http . StatusInternalServerError )
return
}
w . Write ( jsonBytes )
}
2022-04-03 23:10:45 +00:00
// http - GET /stale-items-list
// Returns a list of
2022-07-23 04:40:58 +00:00
func staleItemsListHandler ( w http . ResponseWriter , _ * http . Request ) {
2022-04-03 23:10:45 +00:00
// Remap our data into a smaller object, we don't need full data for the front end
type ResponseDataItem struct {
2023-03-25 03:20:13 +00:00
ID int ` json:"id" `
Name string ` json:"name" `
Data string ` json:"data" `
2022-04-03 23:10:45 +00:00
}
var responseDataItemSlice [ ] MarketItem
for _ , item := range items {
2022-04-30 05:59:59 +00:00
if item . MarketBoardListing == nil { // If there are no market board listings, we don't care about it
2022-04-03 23:10:45 +00:00
continue
}
if item . MarketBoardListing . LastUploadTime == 0 {
continue
}
responseDataItemSlice = append ( responseDataItemSlice , * ( items [ item . ID ] ) )
}
// Sort the item slice by time last updated
sort . Slice ( responseDataItemSlice , func ( i , j int ) bool {
return responseDataItemSlice [ i ] . MarketBoardListing . LastUploadTime < responseDataItemSlice [ j ] . MarketBoardListing . LastUploadTime
} )
2023-02-05 07:51:13 +00:00
if len ( responseDataItemSlice ) > 600 {
responseDataItemSlice = responseDataItemSlice [ : 600 ] // truncate to top 600
2022-04-03 23:10:45 +00:00
}
responseData := make ( [ ] ResponseDataItem , 0 )
for _ , item := range responseDataItemSlice {
responseDataItem := ResponseDataItem {
2023-03-25 03:20:13 +00:00
ID : item . ID ,
Name : item . Name ,
Data : time . Unix ( item . MarketBoardListing . LastUploadTime / 1000 , 0 ) . Format ( "2006-01-02" ) ,
}
responseData = append ( responseData , responseDataItem )
}
jsonBytes , err := json . Marshal ( responseData )
if err != nil {
log . Printf ( "itemsListHandler failed to marshal json to bytes: %v" , err )
w . WriteHeader ( http . StatusInternalServerError )
return
}
w . Write ( jsonBytes )
}
// http - GET /bugged-metric-items-list
// Returns a list of
func buggedMetricItemsListHandler ( w http . ResponseWriter , _ * http . Request ) {
// Remap our data into a smaller object, we don't need full data for the front end
type ResponseDataItem struct {
ID int ` json:"id" `
Name string ` json:"name" `
Data string ` json:"data" `
}
var responseDataItemSlice [ ] MarketItem
for _ , item := range items {
if item . ListingMetric != 0 {
continue
}
responseDataItemSlice = append ( responseDataItemSlice , * ( items [ item . ID ] ) )
}
// Sort the item slice by time last updated
sort . Slice ( responseDataItemSlice , func ( i , j int ) bool {
return responseDataItemSlice [ i ] . MarketBoardListing . LastUploadTime < responseDataItemSlice [ j ] . MarketBoardListing . LastUploadTime
} )
if len ( responseDataItemSlice ) > 600 {
responseDataItemSlice = responseDataItemSlice [ : 600 ] // truncate to top 600
}
responseData := make ( [ ] ResponseDataItem , 0 )
for _ , item := range responseDataItemSlice {
responseDataItem := ResponseDataItem {
ID : item . ID ,
Name : item . Name ,
Data : time . Unix ( item . MarketBoardListing . LastUploadTime / 1000 , 0 ) . Format ( "2006-01-02" ) ,
2022-04-03 23:10:45 +00:00
}
responseData = append ( responseData , responseDataItem )
}
jsonBytes , err := json . Marshal ( responseData )
if err != nil {
log . Printf ( "itemsListHandler failed to marshal json to bytes: %v" , err )
w . WriteHeader ( http . StatusInternalServerError )
return
}
w . Write ( jsonBytes )
}
2022-07-31 18:45:48 +00:00
2022-09-24 00:15:02 +00:00
// http - GET /search-listing-retainer
// Returns a list of listings on Universalis posted by a particular retainer name
func searchListingRetainerHandler ( w http . ResponseWriter , r * http . Request ) {
err := r . ParseForm ( )
if err != nil {
log . Printf ( "failed to parse form in searchListingRetainerHandler: %v" , err )
w . WriteHeader ( http . StatusInternalServerError )
return
}
2022-09-24 00:20:57 +00:00
retainerName := strings . ToUpper ( r . FormValue ( "name" ) )
2022-09-24 00:15:02 +00:00
for _ , item := range items {
for _ , listing := range item . MarketBoardListing . Listings {
if strings . ToUpper ( listing . RetainerName ) == retainerName {
w . Write ( [ ] byte ( item . Name + "\n" ) )
}
}
}
}
2022-09-24 00:20:57 +00:00
// http - GET /search-listing-craftedby
// Returns a list of listings on Universalis posted by a particular retainer name
func searchListingCraftedbyHandler ( w http . ResponseWriter , r * http . Request ) {
err := r . ParseForm ( )
if err != nil {
log . Printf ( "failed to parse form in searchListingCraftedbyHandler: %v" , err )
w . WriteHeader ( http . StatusInternalServerError )
return
}
craftedbyName := strings . ToUpper ( r . FormValue ( "name" ) )
for _ , item := range items {
for _ , listing := range item . MarketBoardListing . Listings {
if strings . ToUpper ( listing . CreatorName ) == craftedbyName {
w . Write ( [ ] byte ( item . Name + "\n" ) )
}
}
}
}
// http - GET /search-listing-price
// Returns a list of listings on Universalis posted by a particular listing price
func searchListingPriceHandler ( w http . ResponseWriter , r * http . Request ) {
err := r . ParseForm ( )
if err != nil {
log . Printf ( "failed to parse form in searchListingPriceHandler: %v" , err )
w . WriteHeader ( http . StatusInternalServerError )
return
}
price , err := strconv . Atoi ( r . FormValue ( "price" ) )
if err != nil {
log . Printf ( "failed to convert '%s' to an integer in searchListingPriceHandler" , r . FormValue ( "price" ) )
w . WriteHeader ( http . StatusInternalServerError )
return
}
for _ , item := range items {
for _ , listing := range item . MarketBoardListing . Listings {
if listing . PricePerUnit == price {
w . Write ( [ ] byte ( item . Name + "\n" ) )
}
}
}
}
2022-07-31 18:45:48 +00:00
// http - GET /inventory
// http - POST /inventory
2022-07-31 19:43:08 +00:00
// When POST, accepts a JSON object containing inventory data from the inventorytools client utility
2022-07-31 18:45:48 +00:00
// When GET, produces a list of the items in retainer inventory to take for listing on market
func inventoryPageHandler ( w http . ResponseWriter , r * http . Request ) {
if r . Method == "GET" {
2022-07-31 19:43:08 +00:00
2022-12-09 21:50:40 +00:00
type ItemToPost struct {
ListingMetric int
InventoryData inventorytools . Item
}
2023-01-28 23:21:36 +00:00
type AutoXivItem struct {
Row int
Col int
}
2022-07-31 19:43:08 +00:00
type Retainer struct {
2023-01-28 23:21:36 +00:00
Name string
RetainerNumber int
ItemsToPost [ ] ItemToPost
ContainerOne [ ] AutoXivItem
ContainerTwo [ ] AutoXivItem
ContainerThree [ ] AutoXivItem
ContainerFour [ ] AutoXivItem
ContainerFive [ ] AutoXivItem
2022-07-31 19:43:08 +00:00
}
type PageData struct {
Title string
Retainers [ ] Retainer
}
pageData := PageData { Title : "Inventory" }
pageData . Retainers = make ( [ ] Retainer , 0 )
// # The plan
// 1. Create map named filteredIDs
// 2. Add all itemIDs in character inventory
// 3. Add all itemIDs in retainerMarket inventories
// 4. Loop through retainers that have the MarketItemStorage set to true
// 5. Make new slice per retainer itemsToPost
// 6. Add everything in retainer bag that doesn't match filteredIDs to the itemsToPost slice
// 7. Display a segment per retainer containing retainer name, their items to post, sorted container and sorted slot index
2022-12-10 19:54:48 +00:00
// Filter can be several modes
// * filterListed - filters out items which are in the character inventory and that are already listed on market for retainer. Useful for finding crafted items stored on retainers that can be posted.
// * none - no filter, lists all items stored on retainers
filter := r . URL . Query ( ) . Get ( "filter" )
if filter == "" { // by default (empty string) it "filterListed" mode.
filter = "filterListed"
}
2022-12-11 02:14:45 +00:00
action := r . URL . Query ( ) . Get ( "action" )
2022-07-31 19:43:08 +00:00
// Build a list of IDs that are ineligible to pull from retainer bags for posting
filteredIDs := make ( map [ int ] bool )
for _ , item := range inventories . CharacterBags { // ignore everything in character inventory
filteredIDs [ item . ID ] = true
}
2022-12-10 19:54:48 +00:00
if filter == "filterListed" {
for _ , retainer := range inventories . Retainers { // go through each retainer
for _ , item := range retainer . RetainerMarket { // ignore everything retainer already have posted to market
filteredIDs [ item . ID ] = true
}
2022-07-31 19:43:08 +00:00
}
}
2023-01-28 23:51:12 +00:00
// Materials in retainer inventory and subtract
retainerNumber := 0
2022-07-31 19:43:08 +00:00
// Build list of retainers and their items to post to be displayed on the web page
2023-01-28 23:58:25 +00:00
for _ , retainer := range inventories . Retainers {
2023-01-28 23:51:12 +00:00
retainerNumber ++
2022-07-31 19:43:08 +00:00
if ! retainer . MarketItemStorage { // ignore retainers that aren't configured to be market items storage
continue
}
2023-01-28 23:51:12 +00:00
tmpRetainer := Retainer { Name : retainer . Name , RetainerNumber : retainerNumber }
2022-12-09 21:50:40 +00:00
tmpRetainer . ItemsToPost = make ( [ ] ItemToPost , 0 )
2022-07-31 19:43:08 +00:00
for _ , item := range retainer . RetainerBags {
2023-02-04 21:09:30 +00:00
if _ , ok := items [ item . ID ] ; ! ok {
log . Printf ( "skipping item '%d' in retainer inventory - not a craftable item?" , item . ID )
continue
}
2022-07-31 19:43:08 +00:00
if _ , ok := filteredIDs [ item . ID ] ; ! ok { // if it's not a filtered item then we can display it on the page
if _ , ok := items [ item . ID ] ; ok {
item . Name = items [ item . ID ] . Name // we have to set the name by cross referencing
} else {
item . Name = fmt . Sprintf ( "Unknown name - ID: %d" , item . ID ) // this might happen if a market item storage container is holding an item that isn't loaded from the crafting-recipes directory
}
item . SortedContainer = item . SortedContainer - 9999 // human friendly offsets
item . SortedSlotIndex = item . SortedSlotIndex + 1 // human friendly offsets
2022-12-09 21:50:40 +00:00
2022-12-27 03:56:14 +00:00
itemToPost := ItemToPost { ListingMetric : items [ item . ID ] . ListingMetric / 1000 , InventoryData : item } // divide by 1000 to decrease number of digits for human simplicity
2022-12-09 21:50:40 +00:00
tmpRetainer . ItemsToPost = append ( tmpRetainer . ItemsToPost , itemToPost )
2022-07-31 19:43:08 +00:00
filteredIDs [ item . ID ] = true // make it a filtered item to avoid duplicates in future
2022-12-11 02:14:45 +00:00
if action == "refresh" { // If it's a refresh action, we queue it for an update
log . Printf ( "dynamic price update for %d triggered" , item . ID )
priceUpdateQueue <- item . ID
updateRequestWg . Add ( 1 )
}
2022-07-31 19:43:08 +00:00
}
}
2022-12-27 03:56:14 +00:00
sort . Slice ( tmpRetainer . ItemsToPost , func ( i , j int ) bool {
return tmpRetainer . ItemsToPost [ i ] . ListingMetric > tmpRetainer . ItemsToPost [ j ] . ListingMetric
} )
2022-07-31 19:43:08 +00:00
pageData . Retainers = append ( pageData . Retainers , tmpRetainer )
}
2022-12-11 02:14:45 +00:00
if action == "refresh" {
updateRequestWg . Wait ( )
2022-12-11 04:51:15 +00:00
sortItemSlice ( ) // required to update metrics
2022-12-11 02:14:45 +00:00
}
2022-07-31 19:43:08 +00:00
err := t . ExecuteTemplate ( w , "inventory.html" , pageData )
if err != nil {
log . Printf ( "failed to execute template: %v" , err )
}
2022-07-31 18:45:48 +00:00
2023-01-28 23:21:36 +00:00
itemsRequired := 0 // number of items required to withdraw to have exactly 20/20 items posted on each retainer
for _ , retainer := range inventories . Retainers {
if retainer . RetainerMarket != nil {
2023-01-29 01:10:01 +00:00
2023-11-25 02:22:47 +00:00
itemsRequired += 20 - len ( retainer . RetainerMarket )
// Update since conversion to CSV - no longer do we have nil entries
/ *
for _ , item := range retainer . RetainerMarket {
fmt . Printf ( "Item ID: %d\n" , item . ID )
if item . ID == 0 {
itemsRequired ++
}
} * /
2023-01-29 01:10:01 +00:00
2023-01-28 23:21:36 +00:00
} else {
2023-01-29 00:19:08 +00:00
log . Printf ( "retainer '%s' market inventory is nil, assuming no items posted?" , retainer . Name )
2023-01-28 23:21:36 +00:00
itemsRequired += 20
}
}
2023-01-29 00:19:08 +00:00
log . Printf ( "itemsRequired: %d" , itemsRequired )
2023-01-28 23:21:36 +00:00
// second pass
for i , _ := range pageData . Retainers {
if itemsRequired <= 0 {
break
}
for _ , item := range pageData . Retainers [ i ] . ItemsToPost {
if itemsRequired <= 0 {
break
}
itemsRequired --
autoXivItem := AutoXivItem { }
autoXivItem . Row = int ( math . Ceil ( float64 ( item . InventoryData . SortedSlotIndex ) / 5 ) )
autoXivItem . Col = item . InventoryData . SortedSlotIndex % 5
if autoXivItem . Col == 0 {
autoXivItem . Col = 5
}
if item . InventoryData . SortedContainer == 1 {
pageData . Retainers [ i ] . ContainerOne = append ( pageData . Retainers [ i ] . ContainerOne , autoXivItem )
} else if item . InventoryData . SortedContainer == 2 {
pageData . Retainers [ i ] . ContainerTwo = append ( pageData . Retainers [ i ] . ContainerTwo , autoXivItem )
} else if item . InventoryData . SortedContainer == 3 {
pageData . Retainers [ i ] . ContainerThree = append ( pageData . Retainers [ i ] . ContainerThree , autoXivItem )
} else if item . InventoryData . SortedContainer == 4 {
pageData . Retainers [ i ] . ContainerFour = append ( pageData . Retainers [ i ] . ContainerFour , autoXivItem )
} else if item . InventoryData . SortedContainer == 5 {
pageData . Retainers [ i ] . ContainerFive = append ( pageData . Retainers [ i ] . ContainerFive , autoXivItem )
}
}
}
buf := bytes . NewBuffer ( nil )
err = t . ExecuteTemplate ( buf , "withdrawitems.ahk" , pageData )
if err != nil {
log . Printf ( "failed to execute withdrawitems.ahk template: %v" , err )
return
}
withdrawItemsAhk , err = ioutil . ReadAll ( buf )
if err != nil {
log . Printf ( "failed to read withdrawitems.ahk processed template buffer to byte slice: %v" , err )
}
2022-07-31 18:45:48 +00:00
} else if r . Method == "POST" {
body , err := ioutil . ReadAll ( r . Body )
if err != nil {
log . Printf ( "failed to read request body on inventoryPageHandler: %v" , err )
w . WriteHeader ( http . StatusInternalServerError )
return
}
inventories = inventorytools . Inventories { }
err = json . Unmarshal ( body , & inventories )
if err != nil {
log . Printf ( "failed to unmarshal request body json to inventorytools.Inventories struct: %v" , err )
w . WriteHeader ( http . StatusInternalServerError )
return
}
log . Printf ( "processed inventory refresh" )
return
}
}
2022-10-30 05:21:46 +00:00
// http - GET /listings
// When GET, produces a list of the items the retainers have listed on the market, sorted by their metric
func listingsPageHandler ( w http . ResponseWriter , r * http . Request ) {
if r . Method == "GET" {
type ListedItem struct {
2022-12-11 18:49:19 +00:00
ID int
2022-10-30 05:21:46 +00:00
Name string
2022-12-06 00:45:12 +00:00
ListingMetric int
2022-10-30 05:21:46 +00:00
QuantityStored int
}
type Retainer struct {
Name string
ItemsListed [ ] ListedItem
}
type PageData struct {
Title string
Retainers [ ] Retainer
}
pageData := PageData { Title : "Listing" }
pageData . Retainers = make ( [ ] Retainer , 0 )
2022-12-11 05:01:01 +00:00
action := r . URL . Query ( ) . Get ( "action" )
2022-10-30 05:21:46 +00:00
// # The plan
// 1. Loop through retainers and build a list of listed items, their metric and number stored
// i. Loop through market listings
// ii. Get the metric from the item map
// iii. Check character and retainer bags for item and increment counter each time it's found
// 2. Sort the list of items by metric
2022-12-11 18:49:19 +00:00
quantityStored := make ( map [ int ] int , 0 ) // key is item ID, value is number stored in retainer and character bag
2022-10-30 05:21:46 +00:00
// 1. Loop through retainers and build a list of listed items, their metric and number stored
for _ , retainer := range inventories . Retainers {
tmpRetainer := Retainer { Name : retainer . Name }
tmpRetainer . ItemsListed = make ( [ ] ListedItem , 0 )
for _ , retainerMarketItem := range retainer . RetainerMarket {
2022-12-06 05:13:59 +00:00
if _ , ok := items [ retainerMarketItem . ID ] ; ! ok { // if we skip this check, it results in invalid memory address error
log . Printf ( "skipping item with ID '%d' on listing page, as it doesn't exist in the items hash map. Is this a crafted item?" , retainerMarketItem . ID ) // can happen if retainers list items for sale that aren't crafted items
continue
}
2022-12-27 03:56:14 +00:00
tmpItem := ListedItem { ID : items [ retainerMarketItem . ID ] . ID , Name : items [ retainerMarketItem . ID ] . Name , ListingMetric : items [ retainerMarketItem . ID ] . ListingMetric / 1000 } // divide by 1000 to decrease number of digits for human simplicity
2022-10-30 05:21:46 +00:00
2022-12-11 18:49:19 +00:00
// Count how many of the items we have in character inventory
for _ , item := range inventories . CharacterBags {
2022-10-30 05:21:46 +00:00
if item . ID == retainerMarketItem . ID {
2022-12-11 18:49:19 +00:00
if _ , ok := items [ retainerMarketItem . ID ] ; ok {
quantityStored [ item . ID ] ++
} else {
quantityStored [ item . ID ] = 1
}
2022-10-30 05:21:46 +00:00
}
}
2022-12-11 18:49:19 +00:00
// Count how many of the items we have in THIS retainer inventories/storage
// A second pass later is required to set the quantity stored
for _ , item := range retainer . RetainerBags {
2022-10-30 05:21:46 +00:00
if item . ID == retainerMarketItem . ID {
2022-12-11 18:49:19 +00:00
if _ , ok := items [ retainerMarketItem . ID ] ; ok {
quantityStored [ item . ID ] ++
} else {
quantityStored [ item . ID ] = 1
}
2022-10-30 05:21:46 +00:00
}
}
2022-12-11 05:01:01 +00:00
if action == "refresh" { // If it's a refresh action, we queue it for an update
log . Printf ( "dynamic price update for %d triggered" , retainerMarketItem . ID )
priceUpdateQueue <- retainerMarketItem . ID
updateRequestWg . Add ( 1 )
}
2022-10-30 05:21:46 +00:00
tmpRetainer . ItemsListed = append ( tmpRetainer . ItemsListed , tmpItem )
}
2022-12-11 05:01:01 +00:00
if action == "refresh" { // if refreshing, block until refresh is done the sort
updateRequestWg . Wait ( )
sortItemSlice ( ) // required to update metrics
}
2022-10-30 05:21:46 +00:00
// 2. Sort the list of items
sort . Slice ( tmpRetainer . ItemsListed , func ( i , j int ) bool {
2022-12-27 03:56:14 +00:00
return tmpRetainer . ItemsListed [ i ] . ListingMetric > tmpRetainer . ItemsListed [ j ] . ListingMetric
2022-10-30 05:21:46 +00:00
} )
pageData . Retainers = append ( pageData . Retainers , tmpRetainer )
}
2022-12-11 18:49:19 +00:00
// Update quantity stored
for _ , retainer := range pageData . Retainers {
for _ , itemListed := range retainer . ItemsListed {
itemListed . QuantityStored = quantityStored [ itemListed . ID ] // shouldn't need to check for existence of key in map as all these items should be in the map already...?
}
}
2022-10-30 05:21:46 +00:00
err := t . ExecuteTemplate ( w , "listings.html" , pageData )
if err != nil {
log . Printf ( "failed to execute template: %v" , err )
}
}
}