diff --git a/spec/System/TestMercenaryEquipmentImport_spec.lua b/spec/System/TestMercenaryEquipmentImport_spec.lua new file mode 100644 index 0000000000..429b3e431d --- /dev/null +++ b/spec/System/TestMercenaryEquipmentImport_spec.lua @@ -0,0 +1,232 @@ +describe("Mercenary equipment import workflows", function() + local json = require("dkjson") + local originalAPI, originalDownload + local originalRealm, originalLeague, originalCharacter + local character, roster, requests, leagueResponse, mercenaryAccess + + local function equipment(base, slot, id) + return { id = string.rep(id, 64), frameType = 0, name = "", typeLine = base, + inventoryId = slot, ilvl = 70, properties = { }, explicitMods = { } } + end + + local function respond(callback, body) + callback({ body = json.encode(body), header = "HTTP/1.1 200 OK\r\n" }) + end + + local function download(_, url, callback) + local suffix = character.realm == "pc" and "" or "/" .. character.realm + local path = url:gsub("^https://api%.pathofexile%.com", "") + table.insert(requests, path) + if path == "/character" .. suffix then + local listed = copyTable(character) + listed.equipment = nil + respond(callback, { characters = { listed } }) + elseif path == "/character" .. suffix .. "/" .. character.name then + respond(callback, { character = character }) + elseif path == "/league-account" .. suffix .. "/" .. urlEncode(character.league) then + if not mercenaryAccess then + callback(nil, "Response code: 403") + elseif leagueResponse then + leagueResponse(callback) + else + respond(callback, { league_account = { mercenaries = roster } }) + end + else + error("Unexpected API request: " .. url) + end + end + + local function fetchMercenaries() + build.importTab.controls.charImportMercenaryEquipment:Click() + local popup = assert(main.popups[1], "The Mercenary selection dialog did not open") + assert.equals("Import Mercenary Equipment", popup.title) + return popup.controls + end + + local function existingSet() + local items = build.itemsTab + local set = items:NewItemSet() + set.title = "Mercenary equipment" + table.insert(items.itemSetOrderList, set.id) + items:AddUndoState() + return set + end + + before_each(function() + originalAPI, originalDownload = main.api, launch.DownloadPage + originalRealm, originalLeague, originalCharacter = main.lastRealm, main.lastLeague, main.lastCharacterHash + main.lastRealm, main.lastLeague, main.lastCharacterHash = "PC", nil, nil + newBuild() + while main.popups[1] do main:ClosePopup() end + + local helmet = equipment("Iron Hat", "Helm", "a") + helmet.sockets = { { group = 0, sColour = "B" } } + helmet.socketedItems = { { typeLine = "Fireball", socket = 0, support = false, + properties = { { name = "Level", values = { { "20", 0 } } } } } } + character = { id = string.rep("b", 64), name = "Player", realm = "pc", class = "Witch", + league = "Standard", level = 80, active_mercenary_index = 1, equipment = { helmet } } + roster = { + { index = 0, name = "Ruktara, the Bullseye", level = 80, items = { equipment("Leather Cap", "Helm", "c") } }, + { index = 1, name = "Vorneka Azrus", level = 82, items = { equipment("Cone Helmet", "Helm", "d") } }, + } + build.importTab:ImportItemsAndSkills(character, true, true, false) + end) + + after_each(function() + main.api, launch.DownloadPage = originalAPI, originalDownload + main.lastRealm, main.lastLeague, main.lastCharacterHash = originalRealm, originalLeague, originalCharacter + while main.popups[1] do main:ClosePopup() end + build.abortSave = true + assert.is_nil(launch.promptMsg) + end) + + it("replaces the active set without importing socketed gem skills", function() + local items = build.itemsTab + local skills = build.skillsTab:CreateUndoState() + items.activeItemSet.useSecondWeaponSet = true + local helm = roster[2].items[1] + helm.sockets = { { group = 0, sColour = "B" } } + helm.socketedItems = { { typeLine = "Arc", socket = 0, support = false, properties = { } } } + build.importTab:ImportMercenaryEquipment(roster[2], items.activeItemSetId) + assert.equals(helm.id, items.items[items.slots.Helmet.selItemId].uniqueID) + assert.equals(items.slots.Helmet.selItemId, items.activeItemSet.Helmet.selItemId) + assert.is_false(items.activeItemSet.useSecondWeaponSet) + assert.same(skills, build.skillsTab:CreateUndoState()) + end) + + it("imports a Stygian Vise and its socketed Abyss jewel into the chosen set", function() + local set = existingSet() + local belt = equipment("Stygian Vise", "Belt", "e") + belt.implicitMods = { "Has 1 Abyssal Socket" } + belt.sockets = { { group = 0, sColour = "A" } } + local jewel = equipment("Murderous Eye Jewel", nil, "f") + jewel.abyssJewel = true + belt.socketedItems = { jewel } + table.insert(roster[2].items, belt) + build.importTab:ImportMercenaryEquipment(roster[2], set.id) + local items = build.itemsTab + assert.equals(belt.id, items.items[set.Belt.selItemId].uniqueID) + assert.equals(jewel.id, items.items[set["Belt Abyssal Socket 1"].selItemId].uniqueID) + assert.equals(0, items.activeItemSet.Belt.selItemId) + assert.equals(0, items.activeItemSet["Belt Abyssal Socket 1"].selItemId) + end) + + describe("API and UI", function() + before_each(function() + build.controls.modeImport:Click() + main.api = new("PoEAPI"):PoEAPI("fixture-token", "fixture-refresh", os.time() + 3600) + main.api.UpdateMain = function() end + requests, leagueResponse, mercenaryAccess = { }, nil, true + launch.DownloadPage = download + build.importTab.controls.accountRealmFetchButton:Click() + end) + + it("imports the selected Mercenary into a new set while retaining the player's build", function() + local items = build.itemsTab + local playerSet, playerHelmet = items.activeItemSetId, items.slots.Helmet.selItemId + local skills = build.skillsTab:CreateUndoState() + local controls = fetchMercenaries() + assert.equals("Vorneka Azrus (Active)", controls.hire:GetSelValue().label) + controls.hire:SetSel(1) + assert.equals("Ruktara, the Bullseye", controls.hire:GetSelValue().label) + assert.equals(0, controls.destination:GetSelValue().id) + controls.import:Click() + + local imported = items.itemSets[items.itemSetOrderList[#items.itemSetOrderList]] + assert.equals("Ruktara, the Bullseye (Mercenary)", imported.title) + assert.equals(roster[1].items[1].id, items.items[imported.Helmet.selItemId].uniqueID) + assert.equals(playerSet, items.activeItemSetId) + assert.equals(playerHelmet, items.slots.Helmet.selItemId) + assert.equals(80, build.characterLevel) + assert.same(skills, build.skillsTab:CreateUndoState()) + assert.is_nil(main.popups[1]) + local tab = build.importTab + local expected = { "PC", "Standard", common.sha1("Player") } + assert.same(expected, { main.lastRealm, main.lastLeague, main.lastCharacterHash }) + assert.same(expected, { tab.lastRealm, tab.lastLeague, tab.lastCharacterHash }) + end) + + it("reimports changed equipment into the chosen existing set without duplicates", function() + local items, set = build.itemsTab, existingSet() + local playerHelmet = items.slots.Helmet.selItemId + table.insert(roster[2].items, equipment("Iron Greaves", "Boots", "e")) + local controls = fetchMercenaries() + controls.destination:SelByValue(set.id, "id") + controls.import:Click() + local helmetId, count = set.Helmet.selItemId, #items.itemOrderList + assert.is_true(set.Boots.selItemId > 0) + roster[2].items[1].properties = { { name = "Quality", values = { { "+20%", 1 } } } } + table.remove(roster[2].items, 2) + + controls = fetchMercenaries() + controls.destination:SelByValue(set.id, "id") + controls.import:Click() + assert.equals(helmetId, set.Helmet.selItemId) + assert.equals(20, items.items[helmetId].quality) + assert.equals(count, #items.itemOrderList) + assert.equals(0, set.Boots.selItemId) + assert.equals(playerHelmet, items.slots.Helmet.selItemId) + items:Undo() + assert.equals(0, items.items[helmetId].quality) + assert.is_true(items.itemSets[set.id].Boots.selItemId > 0) + items:Undo() + assert.equals(0, items.itemSets[set.id].Helmet.selItemId) + assert.equals(0, items.itemSets[set.id].Boots.selItemId) + end) + + for _, action in ipairs({ "cancel", "logout" }) do + it("ignores an arriving roster after the user clicks " .. action, function() + local pending + leagueResponse = function(callback) pending = callback end + local tab, before = build.importTab, build.itemsTab:CreateUndoState() + tab.controls.charImportMercenaryEquipment:Click() + assert.is_function(pending) + if action == "cancel" then tab.controls.cancelMercenaryImport:Click() + else tab.controls.logoutApiButton:Click() end + respond(pending, { league_account = { mercenaries = roster } }) + assert.is_nil(main.popups[1]) + assert.is_false(tab.oauthLoading) + assert.same(before, build.itemsTab:CreateUndoState()) + end) + end + + it("reports a league with no hired Mercenaries without changing equipment", function() + roster = { } + local before = build.itemsTab:CreateUndoState() + build.importTab.controls.charImportMercenaryEquipment:Click() + assert.matches("No Mercenaries", build.importTab.oauthErrCode) + assert.is_nil(main.popups[1]) + assert.same(before, build.itemsTab:CreateUndoState()) + end) + + it("disables further import requests while the API rate limit is active", function() + local before = build.itemsTab:CreateUndoState() + leagueResponse = function(callback) + callback({ header = "HTTP/1.1 429 Too Many Requests\r\nX-Rate-Limit-Policy: league-account-request-limit\r\nRetry-After: 30\r\n" }, "Response code: 429") + end + local button = build.importTab.controls.charImportMercenaryEquipment + button:Click() + assert.is_true(build.importTab.rateLimitEndTime >= os.time() + 29) + local count = #requests + assert.is_false(button:IsEnabled()) + button:Click() + assert.equals(count, #requests) + assert.same(before, build.itemsTab:CreateUndoState()) + end) + + for _, realm in ipairs({ { "XBOX", "xbox" }, { "SONY", "sony" } }) do + it("fetches and imports a Mercenary from the " .. realm[1] .. " realm", function() + character.realm = realm[2] + character.name = "ConsolePlayer" + character.league = "Hardcore" + build.importTab.controls.accountRealm:SelByValue(realm[1], "id") + build.importTab.controls.accountRealmFetchButton:Click() + fetchMercenaries().import:Click() + assert.equals("/league-account/" .. realm[2] .. "/Hardcore", requests[#requests]) + local items = build.itemsTab + local set = items.itemSets[items.itemSetOrderList[#items.itemSetOrderList]] + assert.equals(roster[2].items[1].id, items.items[set.Helmet.selItemId].uniqueID) + end) + end + end) +end) diff --git a/src/Classes/ImportTab.lua b/src/Classes/ImportTab.lua index a44f91bc0a..9408f9cfbf 100644 --- a/src/Classes/ImportTab.lua +++ b/src/Classes/ImportTab.lua @@ -83,6 +83,8 @@ local function addOAuthControls(self) self.controls.logoutApiButton = new("ButtonControl"):ButtonControl({ "TOPLEFT", self.controls.charImportStatusLabel, "TOPRIGHT" }, { labelSpacing, 0, 170, 16 }, "^7Logout from Path of Exile API", function() + self:CancelMercenaryImport() + self.mercenaryAuthorizationRequired = false main.api:ResetDetails() main:SaveSettings() end) @@ -91,7 +93,7 @@ local function addOAuthControls(self) self.controls.characterImportAnchor = new("Control"):Control({ "TOPLEFT", self.controls.sectionOauthCharImport, "TOPLEFT" }, { labelSpacing, 40, 200, 16 }) self.controls.sectionOauthCharImport.height = function() - return self.isAuthorized() and 200 or 60 + return self.isAuthorized() and 225 or 60 end -- realm select @@ -163,6 +165,7 @@ local function addOAuthControls(self) self.controls.authenticateButton = new("ButtonControl"):ButtonControl({ "TOPLEFT", self.controls.characterImportAnchor, "TOPLEFT" }, { 0, 0, 200, 16 }, "^7Authorize with Path of Exile", function() + self:CancelMercenaryImport() main.api:FetchAuthToken(function(errCode) if errCode then self.oauthErrCode = errCode @@ -198,6 +201,7 @@ local function addOAuthControls(self) self.controls.accountRealm = new("DropDownControl"):DropDownControl({ "TOPLEFT", self.controls.charSelectHeader, "BOTTOMLEFT" }, { 0, rowSpacing, 60, 20 }, realmList, function() + self:CancelMercenaryImport() setLeaguesFromCharList() end) self.controls.accountRealm:SelByValue(main.lastRealm or "PC", "id") @@ -216,6 +220,7 @@ local function addOAuthControls(self) -- league select --- @param newLeague string local function onLeagueChange(_, newLeague) + self:CancelMercenaryImport() local realm = self.controls.accountRealm:GetSelValue().realmCode if newLeague == "Any" then self:BuildCharacterList(realm, nil, self.characterList[realm], self.controls.charSelect) @@ -230,7 +235,7 @@ local function addOAuthControls(self) { labelSpacing, 0, 150, 18 }, nil, onLeagueChange) -- character select self.controls.charSelect = new("DropDownControl"):DropDownControl({ "TOPLEFT", self.controls.charSelectLeagueLabel, "BOTTOMLEFT" }, - { 0, rowSpacing, 400, 18 }, nil) + { 0, rowSpacing, 400, 18 }, nil, function() self:CancelMercenaryImport() end) self.controls.charSelect.enabled = function() return self.usingOauth and self.isAuthorized() end @@ -279,7 +284,7 @@ local function addOAuthControls(self) end end) self.controls.charImportTree.enabled = function() - return self.usingOauth and self.isAuthorized() and self.controls.charSelect:GetSelValue() + return self.usingOauth and self.isAuthorized() and self.controls.charSelect:GetSelValue() and not self.mercenaryImportPending end self.controls.charImportTreeClearJewels = new("CheckBoxControl"):CheckBoxControl({ "LEFT", self.controls.charImportTree, "RIGHT" }, { 90, 0, 18 }, "Delete jewels:", nil, "Delete all equipped jewels when importing.", true) @@ -310,7 +315,7 @@ local function addOAuthControls(self) end) end) self.controls.charImportItems.enabled = function() - return self.usingOauth and self.isAuthorized() and self.controls.charSelect:GetSelValue() + return self.usingOauth and self.isAuthorized() and self.controls.charSelect:GetSelValue() and not self.mercenaryImportPending end self.controls.charImportItemsClearSkills = new("CheckBoxControl"):CheckBoxControl({ "LEFT", self.controls.charImportItems, "RIGHT" }, { 85, 0, 18 }, "Delete skills:", nil, "Delete all existing skills when importing.", true) @@ -318,6 +323,26 @@ local function addOAuthControls(self) { 220, 0, 18 }, "Delete equipment:", nil, "Delete all equipped items when importing.", true) self.controls.charImportItemsIgnoreWeaponSwap = new("CheckBoxControl"):CheckBoxControl({ "LEFT", self.controls.charImportItems, "RIGHT" }, { 380, 0, 18 }, "Ignore weapon swap:", nil, "Ignore items and skills in weapon swap.", false) + self.controls.charImportMercenaryEquipment = new("ButtonControl"):ButtonControl({ "TOPLEFT", self.controls.charImportItems, "BOTTOMLEFT" }, + { 0, rowSpacing, 210, 20 }, function() + return self.mercenaryAuthorizationRequired and "Authorize Mercenary access" or "Mercenary Equipment..." + end, function() + if self.mercenaryAuthorizationRequired then + self.characterList = { } + self.controls.logoutApiButton.onClick() + self.controls.authenticateButton.onClick() + else + saveDetails(self.controls.accountRealm:GetSelValue().id, self.controls.charSelectLeague:GetSelValue(), self.controls.charSelect:GetSelValue().label) + self:DownloadMercenaryEquipment() + end + end) + self.controls.charImportMercenaryEquipment.shown = function() return self.usingOauth and self.isAuthorized() end + self.controls.charImportMercenaryEquipment.enabled = function() + return self.controls.charSelect:GetSelValue() and not self.oauthLoading and not self.oauthTimer and m_max(main.api.rateLimiter:NextRequestTime("character-request-limit"), main.api.rateLimiter:NextRequestTime("league-account-request-limit")) <= os.time() + end + self.controls.cancelMercenaryImport = new("ButtonControl"):ButtonControl({ "LEFT", self.controls.charImportMercenaryEquipment, "RIGHT" }, + { 6, 0, 80, 20 }, "Cancel", function() self:CancelMercenaryImport() end) + self.controls.cancelMercenaryImport.shown = function() return self.mercenaryImportPending end end local function addAccountNameControls(self) self.charImportMode = "GETACCOUNTNAME" @@ -1494,7 +1519,7 @@ local rarityMap = { [0] = "NORMAL", "MAGIC", "RARE", "UNIQUE", [9] = "RELIC", [1 local slotMap = { ["Weapon"] = "Weapon 1", ["Offhand"] = "Weapon 2", ["Weapon2"] = "Weapon 1 Swap", ["Offhand2"] = "Weapon 2 Swap", ["Helm"] = "Helmet", ["BodyArmour"] = "Body Armour", ["Gloves"] = "Gloves", ["Boots"] = "Boots", ["Amulet"] = "Amulet", ["Ring"] = "Ring 1", ["Ring2"] = "Ring 2", ["Ring3"] = "Ring 3", ["Belt"] = "Belt", ["BrequelGrafts"] = "Graft 1", ["BrequelGrafts2"] = "Graft 2", } -function ImportTabClass:ImportItem(itemData, slotName, ignoreWeaponSwap, itemSetId) +function ImportTabClass:ImportItem(itemData, slotName, ignoreWeaponSwap, itemSetId, equipmentOnly) if not slotName then if itemData.inventoryId == "PassiveJewels" then slotName = "Jewel "..self.build.latestTree.jewelSlots[itemData.x + 1] @@ -1656,7 +1681,7 @@ function ImportTabClass:ImportItem(itemData, slotName, ignoreWeaponSwap, itemSet end end if itemData.socketedItems then - self:ImportSocketedItems(item, itemData.socketedItems, slotName) + self:ImportSocketedItems(item, itemData.socketedItems, slotName, itemSetId, equipmentOnly) end if itemData.requirements and (not itemData.socketedItems or not itemData.socketedItems[1]) then -- Requirements cannot be trusted if there are socketed gems, as they may override the item's natural requirements @@ -1820,15 +1845,15 @@ function ImportTabClass:ImportItem(itemData, slotName, ignoreWeaponSwap, itemSet end end -function ImportTabClass:ImportSocketedItems(item, socketedItems, slotName) +function ImportTabClass:ImportSocketedItems(item, socketedItems, slotName, itemSetId, equipmentOnly) -- Build socket group list local itemSocketGroupList = { } local abyssalSocketId = 1 for _, socketedItem in ipairs(socketedItems) do if socketedItem.abyssJewel then - self:ImportItem(socketedItem, slotName .. " Abyssal Socket "..abyssalSocketId) + self:ImportItem(socketedItem, slotName .. " Abyssal Socket "..abyssalSocketId, false, itemSetId, equipmentOnly) abyssalSocketId = abyssalSocketId + 1 - else + elseif not equipmentOnly then local normalizedBasename = sanitiseText(socketedItem.typeLine) local gemId = self.build.data.gemForBaseName[normalizedBasename:lower()] if socketedItem.hybrid then @@ -1897,6 +1922,123 @@ function ImportTabClass:ImportSocketedItems(item, socketedItems, slotName) end end +function ImportTabClass:CancelMercenaryImport() + self.mercenaryImportGeneration = (self.mercenaryImportGeneration or 0) + 1 + if self.mercenaryImportPending then + self.oauthLoading = false + end + self.mercenaryImportPending = false +end + +function ImportTabClass:IsMercenaryImportCurrent(generation, spec, realm, character) + return self.mercenaryImportGeneration == generation and self.build.importTab == self and self.build.spec == spec + and main.mode == "BUILD" + and self.controls.accountRealm:GetSelValue().realmCode == realm and self.controls.charSelect:GetSelValue() == character +end + +function ImportTabClass:DownloadMercenaryEquipment() + self:CancelMercenaryImport() + local api, spec = main.api, self.build.spec + local generation = self.mercenaryImportGeneration + local realm = self.controls.accountRealm:GetSelValue().realmCode + local character = self.controls.charSelect:GetSelValue() + local function current() + if self:IsMercenaryImportCurrent(generation, spec, realm, character) then return true end + if self.mercenaryImportGeneration == generation then self:CancelMercenaryImport() end + return false + end + local function finish(err, timeNext) + self.mercenaryImportPending = false + self.oauthLoading = false + self.oauthErrCode = err + self.rateLimitEndTime = timeNext + end + self.mercenaryImportPending = true + self.oauthLoading = true + self.oauthErrCode = nil + api:DownloadCharacter(realm, character.label, function(body, err, timeNext) + if not current() then return end + local selected = body and body.character + if err or not selected or not selected.league then + finish(err or "Selected character has no league", timeNext) + return + end + api:DownloadLeagueAccount(realm, selected.league, function(body, err, timeNext) + if not current() then return end + if err then + self.mercenaryAuthorizationRequired = err:match("403") ~= nil + finish(self.mercenaryAuthorizationRequired and "Authorize Mercenary access to import equipment." or err, timeNext) + return + end + local hires = body.league_account.mercenaries + if #hires == 0 then + finish("No Mercenaries found in this league.") + return + end + finish() + self:OpenMercenaryEquipmentPopup(hires, selected.active_mercenary_index) + end) + end) +end + +function ImportTabClass:OpenMercenaryEquipmentPopup(hires, activeIndex) + local controls, choices = { }, { } + local itemsTab = self.build.itemsTab + local selectedIndex = 1 + for index, hire in ipairs(hires) do + local active = hire.index == activeIndex + t_insert(choices, { hire = hire, label = hire.name .. (active and " (Active)" or "") }) + if active then selectedIndex = index end + end + local destinations = { { id = 0, label = "Create new item set" } } + for _, id in ipairs(itemsTab.itemSetOrderList) do + local set = itemsTab.itemSets[id] + t_insert(destinations, { id = id, label = set.title or "Default" }) + end + controls.hireLabel = new("LabelControl"):LabelControl({ "TOPLEFT", nil, "TOPLEFT" }, { 16, 16, 0, 16 }, "Mercenary:") + controls.hire = new("DropDownControl"):DropDownControl(nil, { 0, 36, 468, 20 }, choices) + controls.hire:SetSel(selectedIndex) + controls.destinationLabel = new("LabelControl"):LabelControl({ "TOPLEFT", nil, "TOPLEFT" }, { 16, 64, 0, 16 }, "Destination item set:") + controls.destination = new("DropDownControl"):DropDownControl(nil, { 0, 84, 468, 20 }, destinations) + controls.notice = new("LabelControl"):LabelControl({ "TOPLEFT", nil, "TOPLEFT" }, { 16, 112, 0, 16 }, "Replaces equipment in the selected set.") + controls.import = new("ButtonControl"):ButtonControl(nil, { -45, 154, 80, 20 }, "Import", function() + self:ImportMercenaryEquipment(controls.hire:GetSelValue().hire, controls.destination:GetSelValue().id) + main:ClosePopup() + end) + controls.cancel = new("ButtonControl"):ButtonControl(nil, { 45, 154, 80, 20 }, "Cancel", function() + self:CancelMercenaryImport() + main:ClosePopup() + end) + main:OpenPopup(500, 190, "Import Mercenary Equipment", controls, "import", "hire", "cancel") +end + +function ImportTabClass:ImportMercenaryEquipment(hire, itemSetId) + local itemsTab = self.build.itemsTab + local itemSet = itemsTab.itemSets[itemSetId] + if itemSetId == 0 then + itemSet = itemsTab:NewItemSet() + itemSet.title = hire.name .. " (Mercenary)" + t_insert(itemsTab.itemSetOrderList, itemSet.id) + self.build:SyncLoadouts() + end + itemSet.useSecondWeaponSet = false + for _, slot in ipairs(itemsTab.orderedSlots) do + if not slot.nodeId then + if itemSet == itemsTab.activeItemSet then + slot:SetSelItemId(0) + else + itemSet[slot.slotName].selItemId = 0 + end + end + end + for _, itemData in ipairs(hire.items) do + self:ImportItem(copyTable(itemData), nil, false, itemSet.id, true) + end + itemsTab:PopulateSlots() + itemsTab:AddUndoState() + self.build.buildFlag = true +end + -- Return the index of the group with the most gems function ImportTabClass:GuessMainSocketGroup() local largestGroupSize = 0 diff --git a/src/Classes/PoEAPI.lua b/src/Classes/PoEAPI.lua index 7a9031df05..c68c9d451a 100644 --- a/src/Classes/PoEAPI.lua +++ b/src/Classes/PoEAPI.lua @@ -6,6 +6,7 @@ local scopesOAuth = { "account:profile", "account:leagues", "account:characters", + "account:league_accounts", "account:trade" } @@ -202,15 +203,15 @@ function PoEAPIClass:DownloadWithRateLimit(policy, url, callback) self.rateLimiter:FinishRequest(policy, requestId) local header = response and response.header or "" self.rateLimiter:UpdateFromHeader(header, policy) - if errMsg then - callback(response, errMsg, nil) - return - end if header:match("HTTP/[%d%.]+ (%d+)") == "429" then timeNext = self.rateLimiter:NextRequestTime(policy, now) callback(nil, "Response code: 429", timeNext) return end + if errMsg then + callback(response, errMsg, nil) + return + end local responseLua = dkjson.decode(response.body) callback(responseLua, errMsg, nil) end @@ -236,3 +237,12 @@ function PoEAPIClass:DownloadCharacter(realm, name, callback) self:DownloadWithRateLimit("character-request-limit", "/character" .. (realm == "pc" and "" or "/" .. realm) .. "/" .. name, callback) end + +---Fetches the authorized account's Mercenaries and other league-specific data. +---@param realm string Realm to fetch the league account from +---@param league string League name +---@param callback DownloadCallback +function PoEAPIClass:DownloadLeagueAccount(realm, league, callback) + self:DownloadWithRateLimit("league-account-request-limit", + "/league-account" .. (realm == "pc" and "" or "/" .. realm) .. "/" .. urlEncode(league), callback) +end diff --git a/src/Classes/TradeQueryRateLimiter.lua b/src/Classes/TradeQueryRateLimiter.lua index f98c59f669..f512f613d2 100644 --- a/src/Classes/TradeQueryRateLimiter.lua +++ b/src/Classes/TradeQueryRateLimiter.lua @@ -56,7 +56,8 @@ function TradeQueryRateLimiterClass:TradeQueryRateLimiter() ["trade-search-request-limit"] = {}, ["trade-fetch-request-limit"] = {}, ["character-list-request-limit"] = {}, - ["character-request-limit"] = {} + ["character-request-limit"] = {}, + ["league-account-request-limit"] = {} } return self end