gilgetter/httpHandlers.go

941 lines
31 KiB
Go
Raw Normal View History

package main
import (
2023-01-28 07:42:47 +00:00
"bytes"
2022-04-02 07:11:06 +00:00
"encoding/json"
"fmt"
2022-04-30 05:59:59 +00:00
"io/ioutil"
"log"
2023-01-28 07:42:47 +00:00
"math"
"net/http"
"sort"
"strconv"
"strings"
"time"
2023-11-24 20:42:05 +00:00
inventorytools "code.mashffxiv.deadbeef.codes/MashPotato/gilgetter/inventorytools"
)
// http - GET /
// Serves the home page
2022-07-23 04:40:58 +00:00
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,
}
2022-03-31 03:18:45 +00:00
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)
}
}
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
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
}
pageData.MarketItems = append(pageData.MarketItems, *(items[item.ID]))
2022-04-03 00:13:16 +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
// 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
} else {
// first occurrence
craftingMaterialMap[materialItem.ID] = materialItem.Quantity * craftingItem.Quantity
2022-04-30 05:59:59 +00:00
}
}
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
})
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
/* // 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-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")
w.Write(withdrawItemsAhk)
2023-01-28 07:42:47 +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) {
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
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
}
timePerRequest := time.Since(startFullPriceUpdateTime).Seconds() / float64(len(items)-len(priceUpdateQueue))
2022-04-02 07:11:06 +00:00
secondsRemaining := float64(len(priceUpdateQueue)) * timePerRequest
pageData := PageData{
SecondsRemaining: int(secondsRemaining),
2022-04-02 07:11:06 +00:00
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
2022-04-02 07:11:06 +00:00
}
2022-04-02 07:11:06 +00:00
jsonBytes, err := json.Marshal(pageData)
if err != nil {
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)
}
// http - GET /items-list
// Returns a list of
2022-07-23 04:40:58 +00:00
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
}
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
}
}
}
*/
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
// check market listings
2022-07-31 21:22:16 +00:00
for _, marketItem := range retainer.RetainerMarket {
if itemSliceItem.ID == marketItem.ID {
found = true
itemSliceItem.InStock = true
2022-07-31 21:22:16 +00:00
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
}
2022-07-31 21:22:16 +00:00
}
responseDataItemSlice[i] = itemSliceItem
}
2022-07-23 05:23:26 +00:00
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)
}
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 {
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{
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)
}
// 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"))
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"))
}
}
}
}
// 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" {
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
}
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
}
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"
}
2022-12-11 02:14:45 +00:00
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
}
}
}
2023-01-28 23:51:12 +00:00
// Materials in retainer inventory and subtract
retainerNumber := 0
// 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++
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)
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
2022-12-09 21:50:40 +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)
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)
}
}
}
sort.Slice(tmpRetainer.ItemsToPost, func(i, j int) bool {
return tmpRetainer.ItemsToPost[i].ListingMetric > tmpRetainer.ItemsToPost[j].ListingMetric
})
pageData.Retainers = append(pageData.Retainers, tmpRetainer)
}
2022-12-11 02:14:45 +00:00
if action == "refresh" {
updateRequestWg.Wait()
sortItemSlice() // required to update metrics
2022-12-11 02:14:45 +00:00
}
err := t.ExecuteTemplate(w, "inventory.html", pageData)
if err != nil {
log.Printf("failed to execute template: %v", err)
}
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
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 {
log.Printf("retainer '%s' market inventory is nil, assuming no items posted?", retainer.Name)
2023-01-28 23:21:36 +00:00
itemsRequired += 20
}
}
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)
}
} 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)
2022-12-11 05:01:01 +00:00
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
}
}
}
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)
}
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
}
// 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)
}
}
}