From 571152798be47c16a997df1bfbf30453a9b97937 Mon Sep 17 00:00:00 2001 From: Michael Flegler Date: Wed, 3 Sep 2025 23:11:05 +0200 Subject: [PATCH 1/3] Feat(trade): Implement direct in-game whisper and teleport --- src/Classes/TradeQuery.lua | 134 ++++++++++++++++++++++++-- src/Classes/TradeQueryRateLimiter.lua | 32 +++--- src/Classes/TradeQueryRequests.lua | 105 +++++++++++++++++--- 3 files changed, 236 insertions(+), 35 deletions(-) diff --git a/src/Classes/TradeQuery.lua b/src/Classes/TradeQuery.lua index 8d54637e9a..671d310080 100644 --- a/src/Classes/TradeQuery.lua +++ b/src/Classes/TradeQuery.lua @@ -27,6 +27,7 @@ local TradeQueryClass = newClass("TradeQuery", function(self, itemsTab) self.resultTbl = { } self.sortedResultTbl = { } self.itemIndexTbl = { } + self.queryIdTbl = { } -- tooltip acceleration tables self.onlyWeightedBaseOutput = { } self.lastComparedWeightList = { } @@ -359,6 +360,20 @@ Highest Weight - Displays the order retrieved from trade]] -- self:PullPoENinjaCurrencyConversion(self.pbLeague) end) self.controls.pbNotice = new("LabelControl", {"BOTTOMRIGHT", nil, "BOTTOMRIGHT"}, {-row_height - pane_margins_vertical - row_vertical_padding, -pane_margins_vertical - row_height - row_vertical_padding, 300, row_height}, "") + + -- Add Trade Mode dropdown to the bottom right + self.tradeModeList = { + "Instant Buyout and In Person Trade", + "Instant Buyout Only", + "In Person Trade Only", + "Any" + } + self.pbTradeModeSelectionIndex = 3 -- Default to "In Person Trade Only" + self.controls.tradeModeSelection = new("DropDownControl", {"BOTTOMRIGHT", nil, "BOTTOMRIGHT"}, {-pane_margins_horizontal, -pane_margins_vertical, 220, row_height}, self.tradeModeList, function(index, value) + self.pbTradeModeSelectionIndex = index + end) + self.controls.tradeModeSelection:SetSel(self.pbTradeModeSelectionIndex) + self.controls.tradeModeSelection.enableDroppedWidth = true -- Realm selection self.controls.realmLabel = new("LabelControl", {"LEFT", self.controls.setSelect, "RIGHT"}, {18, 0, 20, row_height - 4}, "^7Realm:") @@ -737,6 +752,7 @@ function TradeQueryClass:UpdateControlsWithItems(row_idx) self.sortedResultTbl[row_idx] = sortedItems local pb_index = self.sortedResultTbl[row_idx][1].index self.itemIndexTbl[row_idx] = pb_index + self:UpdateSelectedItemTokens(row_idx) self.controls["priceButton".. row_idx].tooltipText = "Sorted by " .. self.itemSortSelectionList[self.pbItemSortSelectionIndex] self.totalPrice[row_idx] = { currency = self.resultTbl[row_idx][pb_index].currency, @@ -844,7 +860,7 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro local nameColor = slotTbl.unique and colorCodes.UNIQUE or "^7" controls["name"..row_idx] = new("LabelControl", top_pane_alignment_ref, {0, row_idx*(row_height + row_vertical_padding), 100, row_height - 4}, nameColor..slotTbl.slotName) controls["bestButton"..row_idx] = new("ButtonControl", { "LEFT", controls["name"..row_idx], "LEFT"}, {100 + 8, 0, 80, row_height}, "Find best", function() - self.tradeQueryGenerator:RequestQuery(activeSlot, { slotTbl = slotTbl, controls = controls, row_idx = row_idx }, self.statSortSelectionList, function(context, query, errMsg) + self.tradeQueryGenerator:RequestQuery(activeSlot, { slotTbl = slotTbl, controls = controls, row_idx = row_idx }, self.statSortSelectionList, self.pbTradeModeSelectionIndex, function(context, query, errMsg) if errMsg then self:SetNotice(context.controls.pbNotice, colorCodes.NEGATIVE .. errMsg) return @@ -873,8 +889,14 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro end, { callbackQueryId = function(queryId) - local url = self.tradeQueryRequests:buildUrl(self.hostName .. "trade2/search", self.pbRealm, self.pbLeague, queryId) - controls["uri"..context.row_idx]:SetText(url, true) + -- ConPrintf("Setting queryId for 'Find best' row %d: %s", context.row_idx, tostring(queryId)) + if queryId then + self.queryIdTbl[context.row_idx] = queryId + local url = self.tradeQueryRequests:buildUrl(self.hostName .. "trade2/search", self.pbRealm, self.pbLeague, queryId) + controls["uri"..context.row_idx]:SetText(url, true) + else + -- ConPrintf("Warning: callbackQueryId called with nil queryId for 'Find best' row %d", context.row_idx) + end end } ) @@ -922,7 +944,17 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro self:UpdateControlsWithItems(row_idx) end controls["priceButton"..row_idx].label = "Price Item" - end) + end, + { + callbackQueryId = function(queryId) + -- ConPrintf("Setting queryId for 'Price Item' row %d: %s", row_idx, tostring(queryId)) + if queryId then + self.queryIdTbl[row_idx] = queryId + else + -- ConPrintf("Warning: callbackQueryId called with nil queryId for 'Price Item' row %d", row_idx) + end + end + }) end) controls["priceButton"..row_idx].enabled = function() local poesessidAvailable = main.POESESSID and main.POESESSID ~= "" @@ -956,6 +988,7 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro end controls["resultDropdown"..row_idx] = new("DropDownControl", { "TOPLEFT", controls["changeButton"..row_idx], "TOPRIGHT"}, {8, 0, 325, row_height}, dropdownLabels, function(index) self.itemIndexTbl[row_idx] = self.sortedResultTbl[row_idx][index].index + self:UpdateSelectedItemTokens(row_idx) self:SetFetchResultReturn(row_idx, self.itemIndexTbl[row_idx]) end) local function addCompareTooltip(tooltip, result_index, dbMode) @@ -999,20 +1032,74 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro controls["importButton"..row_idx].enabled = function() return self.itemIndexTbl[row_idx] and self.resultTbl[row_idx][self.itemIndexTbl[row_idx]].item_string ~= nil end - -- Whisper so we can copy to clipboard +-- Whisper so we can copy to clipboard controls["whisperButton"..row_idx] = new("ButtonControl", { "TOPLEFT", controls["importButton"..row_idx], "TOPRIGHT"}, {8, 0, 185, row_height}, function() - return self.totalPrice[row_idx] and "Whisper for " .. self.totalPrice[row_idx].amount .. " " .. self.totalPrice[row_idx].currency or "Whisper" + if not self.itemIndexTbl[row_idx] then return "Whisper" end + local result = self.resultTbl[row_idx][self.itemIndexTbl[row_idx]] + local priceStr = self.totalPrice[row_idx] and " for " .. self.totalPrice[row_idx].amount .. " " .. self.totalPrice[row_idx].currency or "" + if result.hideout_token then + return "Teleport" .. priceStr + elseif result.whisper_token then + return "Whisper" .. priceStr + else + return "Copy Whisper" .. priceStr + end end, function() - Copy(self.resultTbl[row_idx][self.itemIndexTbl[row_idx]].whisper) + local result = self.resultTbl[row_idx][self.itemIndexTbl[row_idx]] + local token = result.hideout_token or result.whisper_token + + if token then + local queryId = self.queryIdTbl and self.queryIdTbl[row_idx] + local refererUrl = queryId and self.tradeQueryRequests:buildUrl(self.hostName .. "trade2/search", self.pbRealm, self.pbLeague, queryId) + + if not refererUrl then + -- ConPrintf("Error: Could not construct referer URL for whisper.") + if result.whisper then Copy(result.whisper) end + return + end + + self.tradeQueryRequests:SendWhisper(token, refererUrl, function(response, errMsg) + local action = result.hideout_token and "Teleport" or "Whisper" + if errMsg or (response and response.error) then + local errorMsgStr = "Error: " .. (errMsg or (response.error and response.error.message) or "Unknown error") + -- ConPrintf("Action '%s' failed: %s", action, errorMsgStr) + self:SetNotice(self.controls.pbNotice, errorMsgStr) + if result.whisper then + -- ConPrintf("Falling back to clipboard copy.") + Copy(result.whisper) + end + else + -- ConPrintf("'%s' action successful!", action) + self:SetNotice(self.controls.pbNotice, action .. " sent!") + end + end) + elseif result.whisper then + -- ConPrintf("No token found. Falling back to clipboard copy.") + Copy(result.whisper) + else + -- ConPrintf("No token and no whisper text found. Cannot perform action.") + end end) controls["whisperButton"..row_idx].enabled = function() - return self.itemIndexTbl[row_idx] and self.resultTbl[row_idx][self.itemIndexTbl[row_idx]].whisper ~= nil + if not self.itemIndexTbl[row_idx] then return false end + local result = self.resultTbl[row_idx][self.itemIndexTbl[row_idx]] + return result and (result.whisper or result.whisper_token or result.hideout_token) end controls["whisperButton"..row_idx].tooltipFunc = function(tooltip) tooltip:Clear() - if self.itemIndexTbl[row_idx] and self.resultTbl[row_idx][self.itemIndexTbl[row_idx]].item_string then + if not self.itemIndexTbl[row_idx] then return end + local result = self.resultTbl[row_idx][self.itemIndexTbl[row_idx]] + + if result and result.item_string then tooltip.center = true - tooltip:AddLine(16, "Copies the item purchase whisper to the clipboard") + if result.hideout_token then + tooltip:AddLine(16, "Teleports you to the player's hideout.") + elseif result.whisper_token then + tooltip:AddLine(16, "Sends the whisper message directly in-game.") + else + tooltip:AddLine(16, "Copies the item purchase whisper to the clipboard.") + end + tooltip:AddLine(16, "If the primary action fails, it will fall back to copying the whisper.") end end end @@ -1074,3 +1161,30 @@ function TradeQueryClass:UpdateRealms() end end) end + +function TradeQueryClass:UpdateSelectedItemTokens(row_idx) + local index = self.itemIndexTbl[row_idx] + if not index then return end + + local selectedItem = self.resultTbl[row_idx][index] + local queryId = self.queryIdTbl and self.queryIdTbl[row_idx] + + if selectedItem and queryId then + -- ConPrintf("Fetching tokens for item %s in row %d", selectedItem.id, row_idx) + self.tradeQueryRequests:FetchSingleItem(selectedItem.id, queryId, function(newItemData, errMsg) + if errMsg then + -- ConPrintf("Error fetching single item token: %s", errMsg) + selectedItem.whisper_token = nil + selectedItem.hideout_token = nil + return + end + if newItemData then + selectedItem.whisper_token = newItemData.whisper_token + selectedItem.hideout_token = newItemData.hideout_token + -- ConPrintf("Successfully updated tokens for item %s", selectedItem.id) + end + end) + else + -- ConPrintf("Missing item data or queryId for row %d, cannot fetch fresh token.", row_idx) + end +end \ No newline at end of file diff --git a/src/Classes/TradeQueryRateLimiter.lua b/src/Classes/TradeQueryRateLimiter.lua index 28960762ca..f55a3b9da6 100644 --- a/src/Classes/TradeQueryRateLimiter.lua +++ b/src/Classes/TradeQueryRateLimiter.lua @@ -42,7 +42,8 @@ local TradeQueryRateLimiterClass = newClass("TradeQueryRateLimiter", function(se -- convenient name lookup, can be extended self.policyNames = { ["search"] = "trade-search-request-limit", - ["fetch"] = "trade-fetch-request-limit" + ["fetch"] = "trade-fetch-request-limit", + ["whisper"] = "trade-whisper-request-limit" } self.delayCache = {} self.requestId = 0 @@ -53,6 +54,7 @@ local TradeQueryRateLimiterClass = newClass("TradeQueryRateLimiter", function(se self.pendingRequests = { ["trade-search-request-limit"] = {}, ["trade-fetch-request-limit"] = {}, + ["trade-whisper-request-limit"] = {}, ["character-list-request-limit-poe2"] = {}, ["character-request-limit-poe2"] = {} } @@ -75,13 +77,19 @@ function TradeQueryRateLimiterClass:ParsePolicy(headerString) local policies = {} local headers = self:ParseHeader(headerString) local policyName = headers["x-rate-limit-policy"] + if not policyName then return policies end + policies[policyName] = {} local retryAfter = headers["retry-after"] if retryAfter then - policies[policyName].retryAfter = os.time() + retryAfter + policies[policyName].retryAfter = os.time() + tonumber(retryAfter) end + + local rulesHeader = headers["x-rate-limit-rules"] + if not rulesHeader then return policies end + local ruleNames = {} - for match in headers["x-rate-limit-rules"]:gmatch("[^,]+") do + for match in rulesHeader:gmatch("[^,]+") do ruleNames[#ruleNames+1] = match:lower() end for _, ruleName in pairs(ruleNames) do @@ -93,14 +101,16 @@ function TradeQueryRateLimiterClass:ParsePolicy(headerString) for key, headerKey in pairs(properties) do policies[policyName][ruleName][key] = {} local headerValue = headers[headerKey] - for bucket in headerValue:gmatch("[^,]+") do -- example 8:10:60,15:60:120,60:300:1800 - local next = bucket:gmatch("[^:]+") -- example 8:10:60 - local request, window, timeout = tonumber(next()), tonumber(next()), tonumber(next()) - policies[policyName][ruleName][key][window] = { - ["request"] = request, - ["timeout"] = timeout - } - end + if headerValue then + for bucket in headerValue:gmatch("[^,]+") do -- example 8:10:60,15:60:120,60:300:1800 + local next = bucket:gmatch("[^:]+") -- example 8:10:60 + local request, window, timeout = tonumber(next()), tonumber(next()), tonumber(next()) + policies[policyName][ruleName][key][window] = { + ["request"] = request, + ["timeout"] = timeout + } + end + end end end return policies diff --git a/src/Classes/TradeQueryRequests.lua b/src/Classes/TradeQueryRequests.lua index 92f4f23d8d..1593b8bbfe 100644 --- a/src/Classes/TradeQueryRequests.lua +++ b/src/Classes/TradeQueryRequests.lua @@ -14,6 +14,7 @@ local TradeQueryRequestsClass = newClass("TradeQueryRequests", function(self, ra self.requestQueue = { ["search"] = {}, ["fetch"] = {}, + ["whisper"] = {}, } self.hostName = "https://www.pathofexile.com/" end) @@ -30,13 +31,15 @@ function TradeQueryRequestsClass:ProcessQueue() local requestId = self.rateLimiter:InsertRequest(policy) local onComplete = function(response, errMsg) self.rateLimiter:FinishRequest(policy, requestId) - self.rateLimiter:UpdateFromHeader(response.header) - if response.header:match("HTTP/[%d%.]+ (%d+)") == "429" then + if response and response.header then + self.rateLimiter:UpdateFromHeader(response.header) + end + if response and response.header and response.header:match("HTTP/[%d%.]+ (%d+)") == "429" then table.insert(queue, 1, request) return end -- if limit rules don't return account then the POESESSID is invalid. - if response.header:match("X%-Rate%-Limit%-Rules: (.-)\n"):match("Account") == nil and main.POESESSID ~= "" then + if response and response.header and response.header:match("X%-Rate%-Limit%-Rules: (.-)\n") and response.header:match("X%-Rate%-Limit%-Rules: (.-)\n"):match("Account") == nil and main.POESESSID ~= "" then main.POESESSID = "" if errMsg then errMsg = errMsg .. "\nPOESESSID is invalid. Please Re-Log and reset" @@ -44,17 +47,23 @@ function TradeQueryRequestsClass:ProcessQueue() errMsg = "POESESSID is invalid. Please Re-Log and reset" end end - request.callback(response.body, errMsg, unpack(request.callbackParams or {})) + request.callback(response and response.body or nil, errMsg, unpack(request.callbackParams or {})) end - -- self:SendRequest(request.url , onComplete, {body = request.body, poesessid = main.POESESSID}) - local header = "Content-Type: application/json" + + local header = request.headers or "Content-Type: application/json\nUser-Agent: Path of Building Community" if main.POESESSID ~= "" then header = header .. "\nCookie: POESESSID=" .. main.POESESSID end - launch:DownloadPage(request.url, onComplete, { + + local downloadOptions = { header = header, - body = request.body, - }) + body = request.body + } + if request.body then + downloadOptions.post = "raw" + end + + launch:DownloadPage(request.url, onComplete, downloadOptions) else break end @@ -216,7 +225,7 @@ function TradeQueryRequestsClass:PerformSearch(realm, league, query, callback) errMsg = "[ " .. response.error.code .. ": " .. response.error.message .. " ]" end else - ConPrintf("Found 0 results for " .. self.hostName .. "api/trade2/search/" .. league .. "/" .. response.id) + -- ConPrintf("Found 0 results for " .. self.hostName .. "api/trade2/search/" .. league .. "/" .. response.id) errMsg = "No Matching Results Found" end return callback(response, errMsg) @@ -409,14 +418,18 @@ function TradeQueryRequestsClass:FetchResultBlock(url, callback) end - table.insert(items, { + local itemData = { amount = trade_entry.listing.price.amount, currency = trade_entry.listing.price.currency, item_string = table.concat(rawLines, "\n"), whisper = trade_entry.listing.whisper, + whisper_token = trade_entry.listing.whisper_token, + hideout_token = trade_entry.listing.hideout_token, weight = trade_entry.item.pseudoMods and trade_entry.item.pseudoMods[1]:match("Sum: (.+)") or "0", id = trade_entry.id - }) + } + -- ConPrintf("Fetched item [%s]: whisper_token: %s, hideout_token: %s", itemData.id, tostring(itemData.whisper_token), tostring(itemData.hideout_token)) + table.insert(items, itemData) end return callback(items) end @@ -424,7 +437,8 @@ function TradeQueryRequestsClass:FetchResultBlock(url, callback) end ---@param callback fun(items:table, errMsg:string) -function TradeQueryRequestsClass:SearchWithURL(url, callback) +function TradeQueryRequestsClass:SearchWithURL(url, callback, params) + params = params or {} local subpath = url:match(self.hostName .. "trade2/search/(.+)$") local paths = {} for path in subpath:gmatch("[^/]+") do @@ -439,6 +453,11 @@ function TradeQueryRequestsClass:SearchWithURL(url, callback) end league = paths[#paths-1] queryId = paths[#paths] + + if params.callbackQueryId then + params.callbackQueryId(queryId) + end + self:FetchSearchQuery(realm, league, queryId, function(query, errMsg) if errMsg then return callback(nil, errMsg) @@ -457,7 +476,7 @@ function TradeQueryRequestsClass:SearchWithURL(url, callback) end query = dkjson.encode(json_data) - self:SearchWithQuery(realm, league, query, callback) + self:SearchWithQuery(realm, league, query, callback, params) end) end @@ -525,3 +544,61 @@ function TradeQueryRequestsClass:buildUrl(root, realm, league, queryId) end return result end + +---Fetch item details for a single itemHash to refresh its data (e.g., tokens) +---@param itemId string +---@param queryId string +---@param callback fun(item:table, errMsg:string) +function TradeQueryRequestsClass:FetchSingleItem(itemId, queryId, callback) + -- ConPrintf("Fetching single item data for ID: %s", itemId) + local fetch_url = self.hostName .. "api/trade2/fetch/"..itemId.."?query="..queryId + self:FetchResultBlock(fetch_url, function(itemBlock, errMsg) + if errMsg then + return callback(nil, errMsg) + end + if itemBlock and #itemBlock > 0 then + local singleItemData = itemBlock[1] + -- ConPrintf("Successfully fetched single item. hideout_token: %s, whisper_token: %s", tostring(singleItemData.hideout_token), tostring(singleItemData.whisper_token)) + callback(singleItemData) + else + callback(nil, "No item data found in single fetch response") + end + end) +end + +---@param callback fun(items:table, errMsg:string) +function TradeQueryRequestsClass:SendWhisper(token, refererUrl, callback) + -- ConPrintf("Attempting to send whisper with token: " .. tostring(token)) + + -- Manually construct the JSON string to ensure a space after the colon + local requestBody = '{"token": "' .. token .. '"}' + + -- Construct the full headers required by the whisper API + local headers = { + "Content-Type: application/json", + "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36", + "X-Requested-With: XMLHttpRequest" + } + if refererUrl then + table.insert(headers, "Referer: " .. refererUrl) + end + + table.insert(self.requestQueue["whisper"], { + url = self.hostName .. "api/trade2/whisper", + body = requestBody, + headers = table.concat(headers, "\n"), + callback = function(responseBody, errMsg) + -- ConPrintf("Whisper API Response Body: " .. tostring(responseBody)) + -- ConPrintf("Whisper API Error Message: " .. tostring(errMsg)) + if errMsg then + return callback(nil, errMsg) + end + local response, jsonErr = dkjson.decode(responseBody) + if not response then + errMsg = "Failed to decode Whisper JSON response: " .. (jsonErr or "Empty response") + return callback(nil, errMsg) + end + callback(response, errMsg) + end, + }) +end \ No newline at end of file From 2c7b021e925ea755d7faee728e2217dc2b8d0119 Mon Sep 17 00:00:00 2001 From: Michael Flegler Date: Wed, 3 Sep 2025 23:27:34 +0200 Subject: [PATCH 2/3] Feat(trade): Implement direct in-game whisper and teleport --- spec/System/TestTradeQuery_spec.lua | 71 +++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 spec/System/TestTradeQuery_spec.lua diff --git a/spec/System/TestTradeQuery_spec.lua b/spec/System/TestTradeQuery_spec.lua new file mode 100644 index 0000000000..404dba5bd4 --- /dev/null +++ b/spec/System/TestTradeQuery_spec.lua @@ -0,0 +1,71 @@ +describe("TestTradeQuery", function() + before_each(function() + -- Mock necessary global objects and functions to isolate the test + _G.new = function(className, ...) + if className == "TradeQueryRateLimiter" then + return { + GetPolicyName = function() return "mock_policy" end, + NextRequestTime = function() return 0 end, + InsertRequest = function() return 1 end, + FinishRequest = function() end, + UpdateFromHeader = function() end + } + end + -- Fallback for other classes if needed + return {} + end + _G.main = { + POESESSID = "mock_poesessid_for_testing" + } + _G.dkjson = require("dkjson") + _G.tradeQuery = nil -- Mock or setup as needed + end) + + teardown(function() + -- Clean up global mocks + _G.new = nil + _G.main = nil + _G.dkjson = nil + _G.tradeQuery = nil + end) + + it("should construct the correct whisper URL", function() + local requests = newClass("TradeQueryRequests")() + assert.are.equals("https://www.pathofexile.com/api/trade2/whisper", requests.hostName .. "api/trade2/whisper") + end) + + it("should format the whisper request body correctly with a space", function() + local requests = newClass("TradeQueryRequests")() + local testToken = "test_token_12345" + requests:SendWhisper(testToken, "http://mock.referer", function() end) + + local whisperRequest = requests.requestQueue.whisper[1] + assert.is_not_nil(whisperRequest) + + local expectedBody = '{"token": "' .. testToken .. '"}' + assert.are.equals(expectedBody, whisperRequest.body) + end) + + it("should include correct headers for the whisper request", function() + local requests = newClass("TradeQueryRequests")() + local testToken = "test_token_12345" + local testReferer = "https://www.pathofexile.com/trade2/search/poe2/Test%20League/abcdef123" + requests:SendWhisper(testToken, testReferer, function() end) + + local whisperRequest = requests.requestQueue.whisper[1] + assert.is_not_nil(whisperRequest) + assert.is_not_nil(whisperRequest.headers) + + assert.truthy(string.find(whisperRequest.headers, "Content-Type: application/json", 1, true)) + assert.truthy(string.find(whisperRequest.headers, "User-Agent: Mozilla/5.0", 1, true)) + assert.truthy(string.find(whisperRequest.headers, "X-Requested-With: XMLHttpRequest", 1, true)) + assert.truthy(string.find(whisperRequest.headers, "Referer: " .. testReferer, 1, true)) + end) + + it("should correctly build a trade search URL for the referer header", function() + local requests = newClass("TradeQueryRequests")() + local url = requests:buildUrl("https://www.pathofexile.com/trade2/search", "poe2", "Rise of the Abyssal", "kMLBg0KC5") + local expectedUrl = "https://www.pathofexile.com/trade2/search/poe2/Rise+of+the+Abyssal/kMLBg0KC5" + assert.are.equals(expectedUrl, url) + end) +end) From c16d3e82884a988d46b6686dfedc1bdf1dc83974 Mon Sep 17 00:00:00 2001 From: Michael Flegler Date: Wed, 3 Sep 2025 23:44:48 +0200 Subject: [PATCH 3/3] Feat(trade): Implement direct in-game whisper and teleport --- spec/System/TestTradeQuery_spec.lua | 71 ----------------------------- 1 file changed, 71 deletions(-) delete mode 100644 spec/System/TestTradeQuery_spec.lua diff --git a/spec/System/TestTradeQuery_spec.lua b/spec/System/TestTradeQuery_spec.lua deleted file mode 100644 index 404dba5bd4..0000000000 --- a/spec/System/TestTradeQuery_spec.lua +++ /dev/null @@ -1,71 +0,0 @@ -describe("TestTradeQuery", function() - before_each(function() - -- Mock necessary global objects and functions to isolate the test - _G.new = function(className, ...) - if className == "TradeQueryRateLimiter" then - return { - GetPolicyName = function() return "mock_policy" end, - NextRequestTime = function() return 0 end, - InsertRequest = function() return 1 end, - FinishRequest = function() end, - UpdateFromHeader = function() end - } - end - -- Fallback for other classes if needed - return {} - end - _G.main = { - POESESSID = "mock_poesessid_for_testing" - } - _G.dkjson = require("dkjson") - _G.tradeQuery = nil -- Mock or setup as needed - end) - - teardown(function() - -- Clean up global mocks - _G.new = nil - _G.main = nil - _G.dkjson = nil - _G.tradeQuery = nil - end) - - it("should construct the correct whisper URL", function() - local requests = newClass("TradeQueryRequests")() - assert.are.equals("https://www.pathofexile.com/api/trade2/whisper", requests.hostName .. "api/trade2/whisper") - end) - - it("should format the whisper request body correctly with a space", function() - local requests = newClass("TradeQueryRequests")() - local testToken = "test_token_12345" - requests:SendWhisper(testToken, "http://mock.referer", function() end) - - local whisperRequest = requests.requestQueue.whisper[1] - assert.is_not_nil(whisperRequest) - - local expectedBody = '{"token": "' .. testToken .. '"}' - assert.are.equals(expectedBody, whisperRequest.body) - end) - - it("should include correct headers for the whisper request", function() - local requests = newClass("TradeQueryRequests")() - local testToken = "test_token_12345" - local testReferer = "https://www.pathofexile.com/trade2/search/poe2/Test%20League/abcdef123" - requests:SendWhisper(testToken, testReferer, function() end) - - local whisperRequest = requests.requestQueue.whisper[1] - assert.is_not_nil(whisperRequest) - assert.is_not_nil(whisperRequest.headers) - - assert.truthy(string.find(whisperRequest.headers, "Content-Type: application/json", 1, true)) - assert.truthy(string.find(whisperRequest.headers, "User-Agent: Mozilla/5.0", 1, true)) - assert.truthy(string.find(whisperRequest.headers, "X-Requested-With: XMLHttpRequest", 1, true)) - assert.truthy(string.find(whisperRequest.headers, "Referer: " .. testReferer, 1, true)) - end) - - it("should correctly build a trade search URL for the referer header", function() - local requests = newClass("TradeQueryRequests")() - local url = requests:buildUrl("https://www.pathofexile.com/trade2/search", "poe2", "Rise of the Abyssal", "kMLBg0KC5") - local expectedUrl = "https://www.pathofexile.com/trade2/search/poe2/Rise+of+the+Abyssal/kMLBg0KC5" - assert.are.equals(expectedUrl, url) - end) -end)