gilgetter/httpHandlers.go

941 lines
31 KiB
Go

package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"math"
"net/http"
"sort"
"strconv"
"strings"
"time"
inventorytools "code.mashffxiv.deadbeef.codes/MashPotato/gilgetter/inventorytools"
)
// http - GET /
// Serves the home page
func homePageHandler(w http.ResponseWriter, _ *http.Request) {
type PageData struct {
Title string
MarketItems []MarketItem
LastPriceUpdate int
PriceUpdateProgress int
}
pageData := PageData{
Title: "Home",
LastPriceUpdate: int(time.Since(lastFullPriceUpdateTime).Minutes()),
PriceUpdateProgress: priceUpdateProgress,
}
if len(itemSlice) > 200 {
pageData.MarketItems = itemSlice[:200] // truncate to top 200
} else {
pageData.MarketItems = itemSlice
}
err := t.ExecuteTemplate(w, "home.html", pageData)
if err != nil {
log.Fatalf("failed to execute template: %v", err)
}
}
// 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
func staleItemsPageHandler(w http.ResponseWriter, _ *http.Request) {
type PageData struct {
Title string
MarketItems []MarketItem
}
pageData := PageData{Title: "Stale Items", MarketItems: make([]MarketItem, 0)}
for _, item := range items {
if item.MarketBoardListing == nil { // If there are no market board listings, we don't care about it
continue
}
pageData.MarketItems = append(pageData.MarketItems, *(items[item.ID]))
}
// 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
})
// 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
err := t.ExecuteTemplate(w, "stale-items.html", pageData)
if err != nil {
log.Printf("failed to execute template: %v", err)
}
}
// 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" {
type AutoXivItem struct {
Row int
Col int
}
type Retainer struct {
RetainerNumber int
Name string
ItemsToGet []inventorytools.Item
ContainerOne []AutoXivItem
ContainerTwo []AutoXivItem
ContainerThree []AutoXivItem
ContainerFour []AutoXivItem
ContainerFive []AutoXivItem
}
type CraftingItem struct {
ID int
Name string
Job string
Quantity int
}
type PageData struct {
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
}
pageData := PageData{Title: "Crafting Checkout", CraftingItems: make([]CraftingItem, 0), Retainers: make([]Retainer, 0), MarketItems: make([]inventorytools.Item, 0)}
// Build list of crafting items, and how much materials are required
craftingMaterialMap := make(map[int]int) // key item ID, value quantity required
for _, craftingItemID := range craftingCheckoutItemIDs {
craftingItem := CraftingItem{ID: craftingItemID, Name: items[craftingItemID].Name, Job: items[craftingItemID].Job, Quantity: (items[craftingItemID].Velocity / 2) + 1}
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
} else {
// first occurrence
craftingMaterialMap[materialItem.ID] = materialItem.Quantity * craftingItem.Quantity
}
}
pageData.CraftingItems = append(pageData.CraftingItems, craftingItem)
}
// 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
})
// 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
retainerNumber := 0
for _, retainer := range inventories.Retainers {
retainerNumber++
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 {
pageData.Retainers = append(pageData.Retainers, Retainer{Name: retainer.Name, RetainerNumber: retainerNumber, ItemsToGet: itemsToGet})
}
}
// 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)
}
}
}
// 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)
}
/*
for id, quantity := range craftingMaterialMap {
materialItem := TableItem{ID: id, Name: items[id].Name, Quantity: quantity}
pageData.CraftingMaterials = append(pageData.CraftingMaterials, materialItem)
}
*/
err := t.ExecuteTemplate(w, "crafting-checkout.html", pageData)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("failed to execute crafting-checkout.html template: %v", err)
return
}
// 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)
}
}
}
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)
}
} 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
/* // This causes velocity to be scaled up and messes up the crafting material quantity calculation
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()
}()
*/
}
}
// 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")
w.Write(withdrawItemsAhk)
}
// http - GET /full-price-update
// Requests a full price update if criteria is met
func fullPriceUpdateHandler(w http.ResponseWriter, _ *http.Request) {
err := fullPriceRefresh()
if err != nil {
w.WriteHeader(http.StatusTooEarly)
return
}
w.WriteHeader(http.StatusOK)
}
// 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)
}()
}
// http - GET /status
// 5 second polling each client sends to server for updating front end data
func statusHandler(w http.ResponseWriter, _ *http.Request) {
type PageData struct {
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
}
timePerRequest := time.Since(startFullPriceUpdateTime).Seconds() / float64(len(items)-len(priceUpdateQueue))
secondsRemaining := float64(len(priceUpdateQueue)) * timePerRequest
pageData := PageData{
SecondsRemaining: int(secondsRemaining),
PriceUpdateProgress: priceUpdateProgress,
UpdatedMinutesAgo: int(time.Since(lastFullPriceUpdateTime).Minutes()),
}
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
}
jsonBytes, err := json.Marshal(pageData)
if err != nil {
log.Printf("statusHandler failed to marshal json to bytes: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Write(jsonBytes)
}
// http - GET /items-list
// Returns a list of
func itemsListHandler(w http.ResponseWriter, _ *http.Request) {
// 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
}
// use inventory tools to update checkmarks - but universalis code still present as fallback
// expensive?
for i, itemSliceItem := range responseDataItemSlice {
/*
// 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
}
}
}
*/
itemSliceItem.InStock = false // reset it to initial value of false
for _, retainer := range inventories.Retainers {
found := false
// check market listings
for _, marketItem := range retainer.RetainerMarket {
if itemSliceItem.ID == marketItem.ID {
found = true
itemSliceItem.InStock = true
break
}
}
if found {
break
}
// 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
}
}
responseDataItemSlice[i] = itemSliceItem
}
jsonBytes, err := json.Marshal(responseDataItemSlice)
if err != nil {
log.Printf("itemsListHandler failed to marshal json to bytes: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Write(jsonBytes)
}
// http - GET /stale-items-list
// Returns a list of
func staleItemsListHandler(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.MarketBoardListing == nil { // If there are no market board listings, we don't care about it
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
})
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"),
}
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"),
}
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 /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
}
retainerName := strings.ToUpper(r.FormValue("name"))
for _, item := range items {
for _, listing := range item.MarketBoardListing.Listings {
if strings.ToUpper(listing.RetainerName) == retainerName {
w.Write([]byte(item.Name + "\n"))
}
}
}
}
// 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"))
}
}
}
}
// http - GET /inventory
// http - POST /inventory
// When POST, accepts a JSON object containing inventory data from the inventorytools client utility
// 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" {
type ItemToPost struct {
ListingMetric int
InventoryData inventorytools.Item
}
type AutoXivItem struct {
Row int
Col int
}
type Retainer struct {
Name string
RetainerNumber int
ItemsToPost []ItemToPost
ContainerOne []AutoXivItem
ContainerTwo []AutoXivItem
ContainerThree []AutoXivItem
ContainerFour []AutoXivItem
ContainerFive []AutoXivItem
}
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
// 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"
}
action := r.URL.Query().Get("action")
// 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
}
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
}
}
}
// Materials in retainer inventory and subtract
retainerNumber := 0
// Build list of retainers and their items to post to be displayed on the web page
for _, retainer := range inventories.Retainers {
retainerNumber++
if !retainer.MarketItemStorage { // ignore retainers that aren't configured to be market items storage
continue
}
tmpRetainer := Retainer{Name: retainer.Name, RetainerNumber: retainerNumber}
tmpRetainer.ItemsToPost = make([]ItemToPost, 0)
for _, item := range retainer.RetainerBags {
if _, ok := items[item.ID]; !ok {
log.Printf("skipping item '%d' in retainer inventory - not a craftable item?", item.ID)
continue
}
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
itemToPost := ItemToPost{ListingMetric: items[item.ID].ListingMetric / 1000, InventoryData: item} // divide by 1000 to decrease number of digits for human simplicity
tmpRetainer.ItemsToPost = append(tmpRetainer.ItemsToPost, itemToPost)
filteredIDs[item.ID] = true // make it a filtered item to avoid duplicates in future
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)
}
}
}
sort.Slice(tmpRetainer.ItemsToPost, func(i, j int) bool {
return tmpRetainer.ItemsToPost[i].ListingMetric > tmpRetainer.ItemsToPost[j].ListingMetric
})
pageData.Retainers = append(pageData.Retainers, tmpRetainer)
}
if action == "refresh" {
updateRequestWg.Wait()
sortItemSlice() // required to update metrics
}
err := t.ExecuteTemplate(w, "inventory.html", pageData)
if err != nil {
log.Printf("failed to execute template: %v", err)
}
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 {
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++
}
}*/
} else {
log.Printf("retainer '%s' market inventory is nil, assuming no items posted?", retainer.Name)
itemsRequired += 20
}
}
log.Printf("itemsRequired: %d", itemsRequired)
// 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)
}
} 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
}
}
// 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 {
ID int
Name string
ListingMetric int
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)
action := r.URL.Query().Get("action")
// # 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
quantityStored := make(map[int]int, 0) // key is item ID, value is number stored in retainer and character bag
// 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 {
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
}
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
// Count how many of the items we have in character inventory
for _, item := range inventories.CharacterBags {
if item.ID == retainerMarketItem.ID {
if _, ok := items[retainerMarketItem.ID]; ok {
quantityStored[item.ID]++
} else {
quantityStored[item.ID] = 1
}
}
}
// 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 {
if item.ID == retainerMarketItem.ID {
if _, ok := items[retainerMarketItem.ID]; ok {
quantityStored[item.ID]++
} else {
quantityStored[item.ID] = 1
}
}
}
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)
}
tmpRetainer.ItemsListed = append(tmpRetainer.ItemsListed, tmpItem)
}
if action == "refresh" { // if refreshing, block until refresh is done the sort
updateRequestWg.Wait()
sortItemSlice() // required to update metrics
}
// 2. Sort the list of items
sort.Slice(tmpRetainer.ItemsListed, func(i, j int) bool {
return tmpRetainer.ItemsListed[i].ListingMetric > tmpRetainer.ItemsListed[j].ListingMetric
})
pageData.Retainers = append(pageData.Retainers, tmpRetainer)
}
// 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...?
}
}
err := t.ExecuteTemplate(w, "listings.html", pageData)
if err != nil {
log.Printf("failed to execute template: %v", err)
}
}
}