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