From 7285107ce08e2e718413742733bc730b18b38ed9 Mon Sep 17 00:00:00 2001 From: vaisest Date: Tue, 29 Sep 2026 15:52:03 +0000 Subject: [PATCH 1/2] Apply changes from https://github.com/PathOfBuildingCommunity/PathOfBuilding-PoE2/pull/2502 --- LICENSE.md.rej | 36 ++ runtime/lua/ffi-zlib.lua | 340 ++++++++++++++++++ spec/System/TestCommon_spec.lua.rej | 40 +++ .../System/TestCompareBuySimilar_spec.lua.rej | 44 +++ spec/System/TestTradeHelpers_spec.lua.rej | 20 ++ .../TestTradeQueryGenerator_spec.lua.rej | 12 + spec/System/TestTradeQueryRequests_spec.lua | 152 ++++++++ spec/System/TestTradeQuery_spec.lua.rej | 51 +++ src/Classes/CompareBuySimilar.lua | 27 +- src/Classes/CompareBuySimilar.lua.rej | 12 + src/Classes/TradeHelpers.lua | 19 + src/Classes/TradeQuery.lua | 10 +- src/Classes/TradeQuery.lua.rej | 13 + src/Classes/TradeQueryGenerator.lua | 59 ++- src/Classes/TradeQueryGenerator.lua.rej | 24 ++ src/Classes/TradeQueryRequests.lua | 30 +- src/Classes/TradeQueryRequests.lua.rej | 58 +++ src/Classes/TreeTab.lua.rej | 52 +++ src/_SimpleGraphic.def.lua | 6 +- 19 files changed, 943 insertions(+), 62 deletions(-) create mode 100644 LICENSE.md.rej create mode 100644 runtime/lua/ffi-zlib.lua create mode 100644 spec/System/TestCommon_spec.lua.rej create mode 100644 spec/System/TestCompareBuySimilar_spec.lua.rej create mode 100644 spec/System/TestTradeHelpers_spec.lua.rej create mode 100644 spec/System/TestTradeQueryGenerator_spec.lua.rej create mode 100644 spec/System/TestTradeQuery_spec.lua.rej create mode 100644 src/Classes/CompareBuySimilar.lua.rej create mode 100644 src/Classes/TradeQuery.lua.rej create mode 100644 src/Classes/TradeQueryGenerator.lua.rej create mode 100644 src/Classes/TradeQueryRequests.lua.rej create mode 100644 src/Classes/TreeTab.lua.rej diff --git a/LICENSE.md.rej b/LICENSE.md.rej new file mode 100644 index 00000000000..d290145c6a6 --- /dev/null +++ b/LICENSE.md.rej @@ -0,0 +1,36 @@ +diff a/LICENSE.md b/LICENSE.md (rejected hunks) +@@ -1358,32 +1358,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-SOFTWARE. +- +-******************************************************************************* +- +-lua-ffi-zlib: +- +-******************************************************************************* +- +-MIT License +- +-Copyright (c) 2016 Hamish Forbes +- +-Permission is hereby granted, free of charge, to any person obtaining a copy +-of this software and associated documentation files (the "Software"), to deal +-in the Software without restriction, including without limitation the rights +-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +-copies of the Software, and to permit persons to whom the Software is +-furnished to do so, subject to the following conditions: +- +-The above copyright notice and this permission notice shall be included in all +-copies or substantial portions of the Software. +- +-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-SOFTWARE. ++SOFTWARE. +\ No newline at end of file diff --git a/runtime/lua/ffi-zlib.lua b/runtime/lua/ffi-zlib.lua new file mode 100644 index 00000000000..c099d98bfeb --- /dev/null +++ b/runtime/lua/ffi-zlib.lua @@ -0,0 +1,340 @@ +local ffi = require "ffi" +local ffi_new = ffi.new +local ffi_str = ffi.string +local ffi_sizeof = ffi.sizeof +local ffi_copy = ffi.copy +local tonumber = tonumber + +local _M = { + _VERSION = '0.6.0', +} + +local mt = { __index = _M } + + +ffi.cdef([[ +enum { + Z_NO_FLUSH = 0, + Z_PARTIAL_FLUSH = 1, + Z_SYNC_FLUSH = 2, + Z_FULL_FLUSH = 3, + Z_FINISH = 4, + Z_BLOCK = 5, + Z_TREES = 6, + /* Allowed flush values; see deflate() and inflate() below for details */ + Z_OK = 0, + Z_STREAM_END = 1, + Z_NEED_DICT = 2, + Z_ERRNO = -1, + Z_STREAM_ERROR = -2, + Z_DATA_ERROR = -3, + Z_MEM_ERROR = -4, + Z_BUF_ERROR = -5, + Z_VERSION_ERROR = -6, + /* Return codes for the compression/decompression functions. Negative values + * are errors, positive values are used for special but normal events. + */ + Z_NO_COMPRESSION = 0, + Z_BEST_SPEED = 1, + Z_BEST_COMPRESSION = 9, + Z_DEFAULT_COMPRESSION = -1, + /* compression levels */ + Z_FILTERED = 1, + Z_HUFFMAN_ONLY = 2, + Z_RLE = 3, + Z_FIXED = 4, + Z_DEFAULT_STRATEGY = 0, + /* compression strategy; see deflateInit2() below for details */ + Z_BINARY = 0, + Z_TEXT = 1, + Z_ASCII = Z_TEXT, /* for compatibility with 1.2.2 and earlier */ + Z_UNKNOWN = 2, + /* Possible values of the data_type field (though see inflate()) */ + Z_DEFLATED = 8, + /* The deflate compression method (the only one supported in this version) */ + Z_NULL = 0, /* for initializing zalloc, zfree, opaque */ +}; + + +typedef void* (* z_alloc_func)( void* opaque, unsigned items, unsigned size ); +typedef void (* z_free_func) ( void* opaque, void* address ); + +typedef struct z_stream_s { + char* next_in; + unsigned avail_in; + unsigned long total_in; + char* next_out; + unsigned avail_out; + unsigned long total_out; + char* msg; + void* state; + z_alloc_func zalloc; + z_free_func zfree; + void* opaque; + int data_type; + unsigned long adler; + unsigned long reserved; +} z_stream; + + +const char* zlibVersion(); +const char* zError(int); + +int inflate(z_stream*, int flush); +int inflateEnd(z_stream*); +int inflateInit2_(z_stream*, int windowBits, const char* version, int stream_size); + +int deflate(z_stream*, int flush); +int deflateEnd(z_stream* ); +int deflateInit2_(z_stream*, int level, int method, int windowBits, int memLevel,int strategy, const char *version, int stream_size); + +unsigned long adler32(unsigned long adler, const char *buf, unsigned len); +unsigned long crc32(unsigned long crc, const char *buf, unsigned len); +unsigned long adler32_combine(unsigned long, unsigned long, long); +unsigned long crc32_combine(unsigned long, unsigned long, long); + +]]) + +local zlib +if ffi.os == "Windows" then + zlib = ffi.load("../runtime/zlib1") +elseif ffi.os == "OSX" then + zlib = ffi.load("z") +elseif ffi.os == "Linux" then + zlib = ffi.load("libz.so.1") +else + error("lua-ffi-zlib doesn't support platform: " .. ffi.os) +end + +_M.zlib = zlib + +-- Default to 16k output buffer +local DEFAULT_CHUNK = 16384 + +local Z_OK = zlib.Z_OK +local Z_NO_FLUSH = zlib.Z_NO_FLUSH +local Z_STREAM_END = zlib.Z_STREAM_END +local Z_FINISH = zlib.Z_FINISH +local Z_NEED_DICT = zlib.Z_NEED_DICT +local Z_BUF_ERROR = zlib.Z_BUF_ERROR +local Z_STREAM_ERROR = zlib.Z_STREAM_ERROR + +local function zlib_err(err) + return ffi_str(zlib.zError(err)) +end +_M.zlib_err = zlib_err + +local function createStream(bufsize) + -- Setup Stream + local stream = ffi_new("z_stream") + + -- Create input buffer var + local inbuf = ffi_new('char[?]', bufsize+1) + stream.next_in, stream.avail_in = inbuf, 0 + + -- create the output buffer + local outbuf = ffi_new('char[?]', bufsize) + stream.next_out, stream.avail_out = outbuf, 0 + + return stream, inbuf, outbuf +end +_M.createStream = createStream + +local function initInflate(stream, windowBits) + -- Setup inflate process + local windowBits = windowBits or (15 + 32) -- +32 sets automatic header detection + local version = ffi_str(zlib.zlibVersion()) + + return zlib.inflateInit2_(stream, windowBits, version, ffi_sizeof(stream)) +end +_M.initInflate = initInflate + +local function initDeflate(stream, options) + -- Setup deflate process + local method = zlib.Z_DEFLATED + local level = options.level or zlib.Z_DEFAULT_COMPRESSION + local memLevel = options.memLevel or 8 + local strategy = options.strategy or zlib.Z_DEFAULT_STRATEGY + local windowBits = options.windowBits or (15 + 16) -- +16 sets gzip wrapper not zlib + local version = ffi_str(zlib.zlibVersion()) + + return zlib.deflateInit2_(stream, level, method, windowBits, memLevel, strategy, version, ffi_sizeof(stream)) +end +_M.initDeflate = initDeflate + +local function flushOutput(stream, bufsize, output, outbuf) + -- Calculate available output bytes + local out_sz = bufsize - stream.avail_out + if out_sz == 0 then + return + end + -- Read bytes from output buffer and pass to output function + local ok, err = output(ffi_str(outbuf, out_sz)) + if not ok then + return err + end +end + +local function inflate(input, output, bufsize, stream, inbuf, outbuf) + local zlib_flate = zlib.inflate + local zlib_flateEnd = zlib.inflateEnd + -- Inflate a stream + local err = 0 + repeat + -- Read some input + local data = input(bufsize) + if data ~= nil then + ffi_copy(inbuf, data) + stream.next_in, stream.avail_in = inbuf, #data + else + -- no more input data + stream.avail_in = 0 + end + + if stream.avail_in == 0 then + -- When decompressing we *must* have input bytes + zlib_flateEnd(stream) + return false, "INFLATE: Data error, no input bytes" + end + + -- While the output buffer is being filled completely just keep going + repeat + stream.next_out = outbuf + stream.avail_out = bufsize + -- Process the stream, always Z_NO_FLUSH in inflate mode + err = zlib_flate(stream, Z_NO_FLUSH) + + -- Buffer errors are OK here + if err == Z_BUF_ERROR then + err = Z_OK + end + if err < Z_OK or err == Z_NEED_DICT then + -- Error, clean up and return + zlib_flateEnd(stream) + return false, "INFLATE: "..zlib_err(err), stream + end + -- Write the data out + local err = flushOutput(stream, bufsize, output, outbuf) + if err then + zlib_flateEnd(stream) + return false, "INFLATE: "..err + end + until stream.avail_out ~= 0 + + until err == Z_STREAM_END + + -- Stream finished, clean up and return + zlib_flateEnd(stream) + return true, zlib_err(err) +end +_M.inflate = inflate + +local function deflate(input, output, bufsize, stream, inbuf, outbuf) + local zlib_flate = zlib.deflate + local zlib_flateEnd = zlib.deflateEnd + + -- Deflate a stream + local err = 0 + local mode = Z_NO_FLUSH + repeat + -- Read some input + local data = input(bufsize) + if data ~= nil then + ffi_copy(inbuf, data) + stream.next_in, stream.avail_in = inbuf, #data + else + -- EOF, try and finish up + mode = Z_FINISH + stream.avail_in = 0 + end + + -- While the output buffer is being filled completely just keep going + repeat + stream.next_out = outbuf + stream.avail_out = bufsize + + -- Process the stream + err = zlib_flate(stream, mode) + + -- Only possible *bad* return value here + if err == Z_STREAM_ERROR then + -- Error, clean up and return + zlib_flateEnd(stream) + return false, "DEFLATE: "..zlib_err(err), stream + end + -- Write the data out + local err = flushOutput(stream, bufsize, output, outbuf) + if err then + zlib_flateEnd(stream) + return false, "DEFLATE: "..err + end + until stream.avail_out ~= 0 + + -- In deflate mode all input must be used by this point + if stream.avail_in ~= 0 then + zlib_flateEnd(stream) + return false, "DEFLATE: Input not used" + end + + until err == Z_STREAM_END + + -- Stream finished, clean up and return + zlib_flateEnd(stream) + return true, zlib_err(err) +end +_M.deflate = deflate + +local function adler(str, chksum) + local chksum = chksum or 0 + local str = str or "" + return zlib.adler32(chksum, str, #str) +end +_M.adler = adler + +local function crc(str, chksum) + local chksum = chksum or 0 + local str = str or "" + return zlib.crc32(chksum, str, #str) +end +_M.crc = crc + +function _M.inflateGzip(input, output, bufsize, windowBits) + local bufsize = bufsize or DEFAULT_CHUNK + + -- Takes 2 functions that provide input data from a gzip stream and receives output data + -- Returns uncompressed string + local stream, inbuf, outbuf = createStream(bufsize) + + local init = initInflate(stream, windowBits) + if init == Z_OK then + return inflate(input, output, bufsize, stream, inbuf, outbuf) + else + -- Init error + zlib.inflateEnd(stream) + return false, "INIT: "..zlib_err(init) + end +end + +function _M.deflateGzip(input, output, bufsize, options) + local bufsize = bufsize or DEFAULT_CHUNK + options = options or {} + + -- Takes 2 functions that provide plain input data and receives output data + -- Returns gzip compressed string + local stream, inbuf, outbuf = createStream(bufsize) + + local init = initDeflate(stream, options) + if init == Z_OK then + return deflate(input, output, bufsize, stream, inbuf, outbuf) + else + -- Init error + zlib.deflateEnd(stream) + return false, "INIT: "..zlib_err(init) + end +end + +function _M.version() + return ffi_str(zlib.zlibVersion()) +end + +return _M diff --git a/spec/System/TestCommon_spec.lua.rej b/spec/System/TestCommon_spec.lua.rej new file mode 100644 index 00000000000..d173754a30a --- /dev/null +++ b/spec/System/TestCommon_spec.lua.rej @@ -0,0 +1,40 @@ +diff a/spec/System/TestCommon_spec.lua b/spec/System/TestCommon_spec.lua (rejected hunks) +@@ -94,36 +94,4 @@ describe("Common", function() + -- common.classes.StupidClass = nil + -- end) + end) +- -- tests for headless wrapper implementation of deflate/inflate. disabled until support is added +- -- describe("Deflate and Inflate", function() +- -- it("round-trips a simple string", function() +- -- local text = "Hello my name is ????!" +- -- local compressed = Deflate(text) +- -- assert.is_not_nil(compressed) +- -- assert.are.equal(text, Inflate(compressed)) +- -- end) +- -- it("produces a zlib header", function() +- -- local compressed = Deflate("some data to compress") +- -- assert.are.equal(0x78, compressed:byte(1)) +- -- end) +- -- it("round-trips an empty string", function() +- -- local compressed = Deflate("") +- -- assert.is_not_nil(compressed) +- -- assert.are.equal("", Inflate(compressed)) +- -- end) +- -- it("round-trips data larger than the 16k buffer", function() +- -- local text = string.rep("The quick brown fox jumps over the lazy dog. ", 5000) +- -- local compressed = Deflate(text) +- -- assert.is_true(#compressed < #text) +- -- assert.are.equal(text, Inflate(compressed)) +- -- end) +- -- it("round-trips binary data", function() +- -- local bytes = {} +- -- for i = 0, 255 do +- -- bytes[i + 1] = string.char(i) +- -- end +- -- local text = table.concat(bytes) +- -- assert.are.equal(text, Inflate(Deflate(text))) +- -- end) +- -- end) +-end) +\ No newline at end of file ++end) diff --git a/spec/System/TestCompareBuySimilar_spec.lua.rej b/spec/System/TestCompareBuySimilar_spec.lua.rej new file mode 100644 index 00000000000..19d9c59d2ef --- /dev/null +++ b/spec/System/TestCompareBuySimilar_spec.lua.rej @@ -0,0 +1,44 @@ +diff a/spec/System/TestCompareBuySimilar_spec.lua b/spec/System/TestCompareBuySimilar_spec.lua (rejected hunks) +@@ -149,12 +149,13 @@ Implicits: 1 + controls.mod1Check.state = true + controls.mod1Check.changeFunc(true) + controls.search.onClick() +- local queryB64 = copiedUrl:match("Test%%20League/(.*)$"):gsub("%%(%x%x)", function(hex) +- return string.char(tonumber(hex, 16)) +- end) +- local query = require("dkjson").decode(require("Classes.TradeHelpers").B64GzipDecode(queryB64)) ++ -- disabled for now due to headless wrapper lacking zlib bindings ++ -- local queryB64 = copiedUrl:match("Test%%20League/(.*)$"):gsub("%%(%x%x)", function(hex) ++ -- return string.char(tonumber(hex, 16)) ++ -- end) ++ -- local query = require("dkjson").decode(require("Classes.TradeHelpers").B64GzipDecode(queryB64)) + +- assert.same({ { type = "and", filters = { { id = "explicit.stat_1526933524" } } } }, query.stats) ++ -- assert.same({ { type = "and", filters = { { id = "explicit.stat_1526933524" } } } }, query.stats) + end) + + it("rebuilds the URL when league and listed status change", function() +@@ -166,14 +167,16 @@ Implicits: 1 + controls.search.onClick() + assert.not_equal(initialUrl, copiedUrl) + assert.is_truthy(copiedUrl:find("/Standard/", 1, true)) +- local standardUrl = copiedUrl + +- controls.listedDrop:SetSel(4) +- controls.search.onClick() +- assert.not_equal(standardUrl, copiedUrl) +- local b64 = copiedUrl:match("Standard/(.-)$") +- local json = require("Classes.TradeHelpers").B64GzipDecode(b64) +- assert.is_truthy(json:find("any", 1, true)) ++ -- disabled for now due to headless wrapper lacking zlib bindings ++ -- local standardUrl = copiedUrl ++ ++ -- controls.listedDrop:SetSel(4) ++ -- controls.search.onClick() ++ -- assert.not_equal(standardUrl, copiedUrl) ++ -- local b64 = copiedUrl:match("Standard/(.-)$") ++ -- local json = require("Classes.TradeHelpers").B64GzipDecode(b64) ++ -- assert.is_truthy(json:find("any", 1, true)) + end) + + it("persists popup selector choices", function() diff --git a/spec/System/TestTradeHelpers_spec.lua.rej b/spec/System/TestTradeHelpers_spec.lua.rej new file mode 100644 index 00000000000..75bff6807cc --- /dev/null +++ b/spec/System/TestTradeHelpers_spec.lua.rej @@ -0,0 +1,20 @@ +diff a/spec/System/TestTradeHelpers_spec.lua b/spec/System/TestTradeHelpers_spec.lua (rejected hunks) +@@ -169,18 +169,4 @@ describe("TradeHelpers trade hash matching", function() + assert.is_nil(tradeHelpers.findTradeIdOption("+100 to IQ", "explicit")) + end) + end) +- -- disabled for now since the headless wrapper has no zlib bindings +- -- it("gzip decode", function() +- -- local sampleText = "Test string please ignore" +- -- local gzipped = tradeHelpers.B64GzipEncode(sampleText) +- -- local roundTrip = tradeHelpers.B64GzipDecode(gzipped) +- +- -- assert.are.Equal(sampleText, roundTrip) +- -- assert.are_not_equal(sampleText, gzipped) +- +- -- local longText = string.rep("12345678", 4096) +- -- local gzippedLong = tradeHelpers.B64GzipEncode(longText) +- -- local longRoundTrip = tradeHelpers.B64GzipDecode(gzippedLong) +- -- assert.are.Equal(longText, longRoundTrip) +- -- end) + end) diff --git a/spec/System/TestTradeQueryGenerator_spec.lua.rej b/spec/System/TestTradeQueryGenerator_spec.lua.rej new file mode 100644 index 00000000000..7a53aaa68d8 --- /dev/null +++ b/spec/System/TestTradeQueryGenerator_spec.lua.rej @@ -0,0 +1,12 @@ +diff a/spec/System/TestTradeQueryGenerator_spec.lua b/spec/System/TestTradeQueryGenerator_spec.lua (rejected hunks) +@@ -148,8 +148,8 @@ describe("TradeQueryGenerator", function() + } + queryGen.tradeTypeIndex = 1 + local query +- queryGen.requesterCallback = function(_, queryJson) +- query = require("dkjson").decode(queryJson).query ++ queryGen.requesterCallback = function(_, queryTable) ++ query = queryTable + end + queryGen:FinishQuery() + diff --git a/spec/System/TestTradeQueryRequests_spec.lua b/spec/System/TestTradeQueryRequests_spec.lua index f05068d8fa2..e7921d90f62 100644 --- a/spec/System/TestTradeQueryRequests_spec.lua +++ b/spec/System/TestTradeQueryRequests_spec.lua @@ -283,6 +283,158 @@ Strict-Transport-Security: max-age=63115200; includeSubDomains; preload]] end) end) + describe("SearchWithURL", function() + local dkjson = require "dkjson" + local tradeHelpers = require "Classes.TradeHelpers" + local originalDecode, urlRequests, decodedQuery + local url = "https://www.pathofexile.com/trade2/search/poe2/Standard/encoded" + + before_each(function() + urlRequests = new("TradeQueryRequests"):TradeQueryRequests(mock_limiter) + originalDecode = tradeHelpers.B64GzipDecode + -- Compression is provided by SimpleGraphic, which the headless wrapper stubs. + tradeHelpers.B64GzipDecode = function() return decodedQuery end + end) + + after_each(function() + tradeHelpers.B64GzipDecode = originalDecode + end) + + it("fetches ordinary searches by price and returns their query", function() + decodedQuery = [[{"stats":[{"type":"and","filters":[{"id":"explicit.stat_3299347043","value":{"min":1}}]}]}]] + local expectedQuery = dkjson.decode(decodedQuery) + local fetchedItems = { { id = "item1" } } + urlRequests.FetchResults = function(_, ids, queryId, callback) + assert.same({ "item1" }, ids) + assert.are.equal("searchId", queryId) + callback(fetchedItems) + end + local calls = 0 + urlRequests:SearchWithURL(url, function(items, err, query) + calls = calls + 1 + assert.is_nil(err) + assert.same(fetchedItems, items) + assert.same({ query = expectedQuery, sort = { price = "asc" } }, dkjson.decode(query)) + end) + local search = table.remove(urlRequests.requestQueue.search, 1) + assert.are.equal("https://www.pathofexile.com/api/trade2/search/poe2/Standard", search.url) + search.callback([[{"total":1,"result":["item1"],"id":"searchId"}]]) + assert.are.equal(1, calls) + assert.are.equal(0, #urlRequests.requestQueue.search) + end) + + it("relays ordinary search errors to the caller", function() + decodedQuery = [[{"stats":[{"type":"and","filters":[]}]}]] + local calls = 0 + urlRequests:SearchWithURL(url, function(items, err, query) + calls = calls + 1 + assert.is_nil(items) + assert.are.equal("Response code: 403", err) + assert.is_table(dkjson.decode(query).query) + end) + table.remove(urlRequests.requestQueue.search, 1).callback(nil, "Response code: 403") + assert.are.equal(1, calls) + end) + + it("moves a later weighted group first while retaining the other filters", function() + decodedQuery = [[{"stats":[{"type":"count","value":{"min":1},"filters":[]},{"type":"weight","value":{"min":10},"filters":[]}]}]] + urlRequests:SearchWithURL(url, function() end) + local query = dkjson.decode(urlRequests.requestQueue.search[1].body) + assert.same({ ["statgroup.0"] = "desc" }, query.sort) + assert.are.equal("weight", query.query.stats[1].type) + assert.are.equal(10, query.query.stats[1].value.min) + assert.are.equal("count", query.query.stats[2].type) + assert.are.equal(1, query.query.stats[2].value.min) + end) + + for _, case in ipairs({ + { name = "an explicit minimum with only five matches", bounds = { min = 2000 }, weight = 2100, total = 5 }, + { name = "negative weights with a maximum-only bound", bounds = { max = -50 }, weight = -60, total = 10000 }, + { name = "negative weights without bounds", weight = -60, total = 10000 }, + { name = "both bounds with exactly the fetch limit", bounds = { min = 100, max = 200 }, weight = 150, total = 10 }, + }) do + it("fetches pasted searches once, preserving " .. case.name, function() + local filters = { { id = "explicit.stat_3299347043", value = { weight = case.weight < 0 and -1 or 1 } } } + local suppliedQuery = { stats = { { type = "weight", filters = filters, value = case.bounds } } } + decodedQuery = dkjson.encode(suppliedQuery) + local fetchedItems = { { id = "item1", weight = tostring(case.weight) } } + local fetchCalls, callbackCalls = 0, 0 + urlRequests.FetchResults = function(_, ids, queryId, callback) + fetchCalls = fetchCalls + 1 + assert.same({ "item1" }, ids) + assert.are.equal("searchId", queryId) + callback(fetchedItems) + end + urlRequests:SearchWithURL(url, function(items, err, query) + callbackCalls = callbackCalls + 1 + assert.is_nil(err) + assert.same(fetchedItems, items) + assert.same({ query = suppliedQuery, sort = { ["statgroup.0"] = "desc" } }, dkjson.decode(query)) + end) + assert.are.equal(1, #urlRequests.requestQueue.search) + local search = table.remove(urlRequests.requestQueue.search, 1) + assert.same(suppliedQuery, dkjson.decode(search.body).query) + search.callback(dkjson.encode({ total = case.total, result = { "item1" }, id = "searchId" })) + assert.are.equal(1, fetchCalls) + assert.are.equal(1, callbackCalls) + assert.are.equal(0, #urlRequests.requestQueue.search) + end) + end + + it("reports no matches without relaxing a pasted minimum", function() + decodedQuery = [[{"stats":[{"type":"weight","value":{"min":2000},"filters":[]}]}]] + local calls = 0 + urlRequests:SearchWithURL(url, function(items, err, query) + calls = calls + 1 + assert.is_nil(items) + assert.are.equal("No Matching Results Found", err) + assert.are.equal(2000, dkjson.decode(query).query.stats[1].value.min) + end) + table.remove(urlRequests.requestQueue.search, 1).callback([[{"total":0,"result":[],"id":"emptySearch"}]]) + assert.are.equal(1, calls) + assert.are.equal(0, #urlRequests.requestQueue.search) + end) + + for _, malformed in ipairs({ + [[{"stats":[]}]], + [[{"stats":[{"type":"and"},false]}]], + [[{"stats":[{}]}]], + [[42]], + [[not JSON]], + }) do + it("rejects malformed query " .. malformed .. " through the callback", function() + decodedQuery = malformed + local calls = 0 + urlRequests:SearchWithURL(url, function(items, err) + calls = calls + 1 + assert.is_nil(items) + assert.are.equal("URL is malformed", err) + end) + assert.are.equal(1, calls) + assert.are.equal(0, #urlRequests.requestQueue.search) + end) + end + + it("rejects failed decompression through the callback", function() + decodedQuery = nil + local calls = 0 + urlRequests:SearchWithURL(url, function(_, err) + calls = calls + 1 + assert.are.equal("URL is malformed", err) + end) + assert.are.equal(1, calls) + end) + + it("rejects an unrelated URL through the callback", function() + local calls = 0 + urlRequests:SearchWithURL("https://example.com/", function(_, err) + calls = calls + 1 + assert.are.equal("Invalid URL", err) + end) + assert.are.equal(1, calls) + end) + end) + describe("FetchResults", function() -- Pass: Fetches exactly 10 from 11, in 1 block -- Fail: Fetches wrong count/blocks, indicating batch limit violation, triggering rate limits diff --git a/spec/System/TestTradeQuery_spec.lua.rej b/spec/System/TestTradeQuery_spec.lua.rej new file mode 100644 index 00000000000..3df71f731b1 --- /dev/null +++ b/spec/System/TestTradeQuery_spec.lua.rej @@ -0,0 +1,51 @@ +diff a/spec/System/TestTradeQuery_spec.lua b/spec/System/TestTradeQuery_spec.lua (rejected hunks) +@@ -62,6 +62,49 @@ describe("TradeQuery", function () + end) + end) + ++ describe("purchase search filters", function() ++ local dkjson = require "dkjson" ++ local tradeHelpers = require "Classes.TradeHelpers" ++ local originalEncode, originalCopy, originalOpenURL, encodedQuery ++ ++ before_each(function() ++ originalEncode, originalCopy, originalOpenURL = tradeHelpers.B64GzipEncode, Copy, OpenURL ++ tradeHelpers.B64GzipEncode = function(query) ++ encodedQuery = dkjson.decode(query) ++ return "encoded" ++ end ++ Copy, OpenURL = function() end, function() end ++ mock_tradeQuery.itemsTab.activeItemSet = {} ++ mock_tradeQuery.itemsTab.slots = {} ++ mock_tradeQuery.slotTables[1] = { slotName = "Ring 1" } ++ mock_tradeQuery:PriceItemRowDisplay(1, nil, 0, 20) ++ mock_tradeQuery.pbLeague = "Standard" ++ mock_tradeQuery.itemIndexTbl[1] = 1 ++ mock_tradeQuery.resultTbl[1] = { { trader = "ExampleAccount", weight = "100", priceType = "~b/o" } } ++ end) ++ ++ after_each(function() ++ tradeHelpers.B64GzipEncode, Copy, OpenURL = originalEncode, originalCopy, originalOpenURL ++ end) ++ ++ for _, group in ipairs({ ++ { type = "and", filters = {} }, ++ { type = "count", value = { min = 1 }, filters = {} }, ++ { type = "weight", value = { min = 10 }, filters = {} }, ++ }) do ++ it("preserves " .. group.type .. " group semantics when opening a purchase search", function() ++ mock_tradeQuery.lastQueries[1] = dkjson.encode({ query = { stats = { group } } }) ++ mock_tradeQuery.controls.whisperButton1.onClick() ++ if group.type == "weight" then ++ assert.same({ min = 99, max = 101 }, encodedQuery.stats[1].value) ++ else ++ assert.same(group, encodedQuery.stats[1]) ++ end ++ assert.same({ input = "ExampleAccount" }, encodedQuery.filters.trade_filters.filters.account) ++ end) ++ end ++ end) ++ + it("fits the OAuth clipboard status inside the login button", function() + local status = mock_tradeQuery:FormatOAuthLoginStatus(60) + diff --git a/src/Classes/CompareBuySimilar.lua b/src/Classes/CompareBuySimilar.lua index 55445eae65a..8e6a34ebadc 100644 --- a/src/Classes/CompareBuySimilar.lua +++ b/src/Classes/CompareBuySimilar.lua @@ -60,16 +60,13 @@ local function buildURL(item, slotName, controls, modEntries, defenceEntries, is -- Build query local queryTable = { - query = { - status = { option = listedApiValue }, - stats = { - { - type = "and", - filters = {} - } - }, + status = { option = listedApiValue }, + stats = { + { + type = "and", + filters = {} + } }, - sort = { price = "asc" } } local queryFilters = {} @@ -101,7 +98,7 @@ local function buildURL(item, slotName, controls, modEntries, defenceEntries, is -- Base type filter if controls.baseTypeCheck and controls.baseTypeCheck.state then - queryTable.query.type = item.baseName + queryTable.type = item.baseName end -- Item level filter @@ -173,21 +170,21 @@ local function buildURL(item, slotName, controls, modEntries, defenceEntries, is if controls[prefix .. "Check"] and controls[prefix .. "Check"].state then if #entry.tradeIds == 1 then -- 1 id entries are added to the stat filters section - t_insert(queryTable.query.stats[1].filters, getFilter(entry.tradeIds[1])) + t_insert(queryTable.stats[1].filters, getFilter(entry.tradeIds[1])) elseif #entry.tradeIds > 1 then -- ambiguous entries are added as a separate count filter local countFilter = { type = "count", value = { min = 1 }, filters = {} } for _, tradeId in ipairs(entry.tradeIds) do t_insert(countFilter.filters, getFilter(tradeId)) end - t_insert(queryTable.query.stats, countFilter) + t_insert(queryTable.stats, countFilter) end end end -- Only include filters if we have any if next(queryFilters) then - queryTable.query.filters = queryFilters + queryTable.filters = queryFilters end -- Build URL @@ -199,8 +196,8 @@ local function buildURL(item, slotName, controls, modEntries, defenceEntries, is local encodedLeague = league:gsub("[^%w%-%.%_%~]", function(c) return string.format("%%%02X", string.byte(c)) end):gsub(" ", "+") - url = url .. "/" .. encodedLeague - url = url .. "?q=" .. urlEncode(queryJson) + url ..= "/" .. encodedLeague + url ..= "/" .. tradeHelpers.B64GzipEncode(queryJson) return url end diff --git a/src/Classes/CompareBuySimilar.lua.rej b/src/Classes/CompareBuySimilar.lua.rej new file mode 100644 index 00000000000..2e144e7207d --- /dev/null +++ b/src/Classes/CompareBuySimilar.lua.rej @@ -0,0 +1,12 @@ +diff a/src/Classes/CompareBuySimilar.lua b/src/Classes/CompareBuySimilar.lua (rejected hunks) +@@ -75,8 +72,8 @@ local function buildURL(item, slotName, controls, modEntries, defenceEntries, is + -- Search by unique name + -- Strip "Foulborn" prefix from unique name for trade search + local tradeName = (item.title or item.name):gsub("^Foulborn%s+", "") +- queryTable.query.name = tradeName +- queryTable.query.type = item.baseName ++ queryTable.name = tradeName ++ queryTable.type = item.baseName + -- If item is Foulborn, add the foulborn_item filter + if item.foulborn then + queryFilters.misc_filters = queryFilters.misc_filters or { filters = {} } diff --git a/src/Classes/TradeHelpers.lua b/src/Classes/TradeHelpers.lua index 444834a7974..9f5580ce70b 100644 --- a/src/Classes/TradeHelpers.lua +++ b/src/Classes/TradeHelpers.lua @@ -620,4 +620,23 @@ function M.newPlainNumericEdit(anchor, rect, init, prompt, limit, integer, chang end return ctrl end + + +---@param str string String which will be encoded +---@return string? result The given string, gzipped and then Base64URL encoded +function M.B64GzipEncode(str) + local b64 = require("base64") + local deflated = Deflate(str, true) + if not deflated then return end + return b64.encode(deflated):gsub("%+", "-"):gsub("/", "_") +end + +---@param str string String which will be decoded +---@return string? result The given string, Base64URL decoded and the ungzipped +function M.B64GzipDecode(str) + local b64 = require("base64") + local data = b64.decode(str:gsub("%-", "+"):gsub("_", "/")) + if not data then return end + return Inflate(data) +end return M diff --git a/src/Classes/TradeQuery.lua b/src/Classes/TradeQuery.lua index 66ad1fc1ff9..2a12bfc33bd 100644 --- a/src/Classes/TradeQuery.lua +++ b/src/Classes/TradeQuery.lua @@ -1052,7 +1052,8 @@ 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"):LabelControl(top_pane_alignment_ref, { 0, row_idx * (row_height + row_vertical_padding), 135, row_height - 4 }, nameColor .. slotTbl.slotName) controls["bestButton" .. row_idx] = new("ButtonControl"):ButtonControl({ "LEFT", controls["name" .. row_idx], "LEFT" }, { 135 + 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) + ---@param query table A table of filters + local function requestQueryHandler(context, query, errMsg) if errMsg then self:SetNotice(context.controls.pbNotice, colorCodes.NEGATIVE .. errMsg) return @@ -1109,7 +1110,8 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro end } ) - end) + end + self.tradeQueryGenerator:RequestQuery(activeSlot, { slotTbl = slotTbl, controls = controls, row_idx = row_idx }, self.statSortSelectionList, requestQueryHandler) end) controls["bestButton"..row_idx].shown = function() return not self.resultTbl[row_idx] end controls["bestButton"..row_idx].enabled = function() return self.pbLeague end @@ -1300,7 +1302,9 @@ you can add them, copy the link here, and press "Price Item" to evaluate the ite -- use trade sum to get the specific item. both min and max -- weight on site uses floats but only shows integer in the api -- e.g. weight of 172.3 shows up as 172 in the api - exactQuery.query.stats[1].value = { min = floor(itemResult.weight, 1) - 1, max = round(itemResult.weight, 1) + 1 } + if exactQuery.query.stats[1].type == "weight" then + exactQuery.query.stats[1].value = { min = floor(itemResult.weight, 1) - 1, max = round(itemResult.weight, 1) + 1 } + end -- also apply trader name. this should make false positives -- extremely unlikely. this doesn't seem to take up a filter slot exactQuery.query.filters = exactQuery.query.filters or { } diff --git a/src/Classes/TradeQuery.lua.rej b/src/Classes/TradeQuery.lua.rej new file mode 100644 index 00000000000..3a76c43a70f --- /dev/null +++ b/src/Classes/TradeQuery.lua.rej @@ -0,0 +1,13 @@ +diff a/src/Classes/TradeQuery.lua b/src/Classes/TradeQuery.lua (rejected hunks) +@@ -1391,9 +1393,9 @@ you can add them, copy the link here, and press "Price Item" to evaluate the ite + exactQuery.query.filters.trade_filters.filters = exactQuery.query.filters.trade_filters.filters or { } + exactQuery.query.filters.trade_filters.filters.account = { input = itemResult.trader } + +- local exactQueryStr = dkjson.encode(exactQuery.query) ++ local exactQueryStr = dkjson.encode(exactQuery.query) + +- local encodedUrl = s_format("https://www.pathofexile.com/trade2/search/%s/%s", self.pbLeague, tradeHelpers.B64GzipEncode(exactQueryStr)) ++ local encodedUrl = s_format("https://www.pathofexile.com/trade2/search/%s/%s", self.pbLeague, tradeHelpers.B64GzipEncode(exactQueryStr)) + + Copy(encodedUrl) + OpenURL(encodedUrl) diff --git a/src/Classes/TradeQueryGenerator.lua b/src/Classes/TradeQueryGenerator.lua index 826cb34a086..0214e854cc4 100644 --- a/src/Classes/TradeQueryGenerator.lua +++ b/src/Classes/TradeQueryGenerator.lua @@ -961,38 +961,34 @@ function TradeQueryGeneratorClass:FinishQuery() local requiredMods = self.calcContext.requiredMods or {} local blockedMods = self.calcContext.blockedMods or {} local queryTable = { - query = { - filters = self.calcContext.special.queryFilters or { - type_filters = { - filters = { - category = { option = self.calcContext.itemCategoryQueryStr }, - rarity = { option = "nonunique" } - } + filters = self.calcContext.special.queryFilters or { + type_filters = { + filters = { + category = { option = self.calcContext.itemCategoryQueryStr }, + rarity = { option = "nonunique" } } + } + }, + status = { option = selectedTradeType }, + stats = { + { + type = "weight", + value = { min = minWeight }, + filters = {}, }, - status = { option = selectedTradeType }, - stats = { - { - type = "weight", - value = { min = minWeight }, - filters = {}, - }, - { - type = "and", - filters = {}, - }, - { - type = "not", - filters = {}, - } + { + type = "and", + filters = {}, + }, + { + type = "not", + filters = {}, } }, - sort = { ["statgroup.0"] = "desc" }, - engine = "new" } - local weightGroup = queryTable.query.stats[1] - local andGroup = queryTable.query.stats[2] - local notGroup = queryTable.query.stats[3] + local weightGroup = queryTable.stats[1] + local andGroup = queryTable.stats[2] + local notGroup = queryTable.stats[3] -- the trade site has a maximum complexity of 200 for each query. our baseline is 54 for the weighted sum group, 4 for the rarity filter plus category, and 4 for the and group local complexityBudget = 200 - 54 - 4 - 4 @@ -1058,7 +1054,7 @@ function TradeQueryGeneratorClass:FinishQuery() for k, v in pairs(self.calcContext.special.queryExtra or {}) do complexityBudget = complexityBudget - 2 - queryTable.query[k] = v + queryTable[k] = v end -- and filters specified by the user @@ -1073,7 +1069,7 @@ function TradeQueryGeneratorClass:FinishQuery() local options = self.calcContext.options if not options.includeMirrored then complexityBudget = complexityBudget - 3 - queryTable.query.filters.misc_filters = { + queryTable.filters.misc_filters = { disabled = false, filters = { mirrored = false, @@ -1083,7 +1079,7 @@ function TradeQueryGeneratorClass:FinishQuery() if options.maxPrice and options.maxPrice > 0 then complexityBudget = complexityBudget - 3 - queryTable.query.filters.trade_filters = { + queryTable.filters.trade_filters = { filters = { price = { option = options.maxPriceType, @@ -1158,8 +1154,7 @@ function TradeQueryGeneratorClass:FinishQuery() errMsg = "Could not generate search, found no mods to search for" end - local queryJson = dkjson.encode(queryTable) - self.requesterCallback(self.requesterContext, queryJson, errMsg) + self.requesterCallback(self.requesterContext, queryTable, errMsg) -- Close blocker popup main:ClosePopup() diff --git a/src/Classes/TradeQueryGenerator.lua.rej b/src/Classes/TradeQueryGenerator.lua.rej new file mode 100644 index 00000000000..9b3ab54aff7 --- /dev/null +++ b/src/Classes/TradeQueryGenerator.lua.rej @@ -0,0 +1,24 @@ +diff a/src/Classes/TradeQueryGenerator.lua b/src/Classes/TradeQueryGenerator.lua (rejected hunks) +@@ -1093,11 +1089,11 @@ function TradeQueryGeneratorClass:FinishQuery() + + if options.account then + complexityBudget = complexityBudget - 3 +- queryTable.query.filters.trade_filters.filters.account = { input = options.account } ++ queryTable.filters.trade_filters.filters.account = { input = options.account } + end + if options.maxLevel and options.maxLevel > 0 then + complexityBudget = complexityBudget - 3 +- queryTable.query.filters.req_filters = { ++ queryTable.filters.req_filters = { + disabled = false, + filters = { + lvl = { +@@ -1109,7 +1105,7 @@ function TradeQueryGeneratorClass:FinishQuery() + + if options.sockets and options.sockets > 0 then + complexityBudget = complexityBudget - 3 +- queryTable.query.filters.equipment_filters = { ++ queryTable.filters.equipment_filters = { + disabled = false, + filters = { + rune_sockets = { diff --git a/src/Classes/TradeQueryRequests.lua b/src/Classes/TradeQueryRequests.lua index 0ddff307ebb..301630763da 100644 --- a/src/Classes/TradeQueryRequests.lua +++ b/src/Classes/TradeQueryRequests.lua @@ -114,7 +114,7 @@ end ---the search to fetch more items when the search cap (10k items) is reached ---@param league string ---@param query string ----@param callback fun(items:table, errMsg:string) +---@param callback fun(items: table, errMsg: string, query: string) ---@param params table @ params = { callbackQueryId = fun(queryId:string) } function TradeQueryRequestsClass:SearchWithQueryWeightAdjusted(realm, league, query, callback, params) params = params or {} @@ -125,13 +125,17 @@ function TradeQueryRequestsClass:SearchWithQueryWeightAdjusted(realm, league, qu -- Each repeat is a leap of 10k items, normally we shouldn't need more than 1-2 steps anyways local maxRecursion = 5 local currentRecursion = 0 + -- the query is adjusted as the search repeats, so return the final query + local function resultCallback(items, errMsg) + return callback(items, errMsg, query) + end local function performSearchCallback(response, errMsg) currentRecursion = currentRecursion + 1 if params.callbackQueryId and response and response.id then params.callbackQueryId(response.id) end if errMsg and ((errMsg == "No Matching Results Found" and currentRecursion >= maxRecursion) or errMsg ~= "No Matching Results Found") then - return callback(nil, errMsg) + return resultCallback(nil, errMsg) end if (response.total >= self.maxFetchPerSearch and response.total < 10000) or currentRecursion >= maxRecursion then -- Search not clipped or max recursion reached, fetch results and finalize @@ -139,7 +143,7 @@ function TradeQueryRequestsClass:SearchWithQueryWeightAdjusted(realm, league, qu -- Not enough items in the last search, fill results from previous search self:FetchResults(response.result, response.id, function(items, errMsg) if errMsg then - return callback(nil, errMsg) + return resultCallback(nil, errMsg) end local fetchedItemIds = {} local idSet = {} @@ -172,23 +176,26 @@ function TradeQueryRequestsClass:SearchWithQueryWeightAdjusted(realm, league, qu end self:FetchResults(unfetchedItemIds, previousSearchId, function(newItems, errMsg) if errMsg then - return callback(nil, errMsg) + return resultCallback(nil, errMsg) end items = tableConcat(items, newItems) - callback(items, errMsg) + resultCallback(items, errMsg) end) else - callback(items, errMsg) + resultCallback(items, errMsg) end end) else -- Search not clipped and result count satisfy maxFetchPerSearch, proceed normally - self:FetchResults(response.result, response.id, callback) + self:FetchResults(response.result, response.id, resultCallback) end else if response.total < self.maxFetchPerSearch then -- Less than maximum items retrieved lower weight to try and get more. local queryJson = dkjson.decode(query) - queryJson.query.stats[1].value.min = queryJson.query.stats[1].value.min / 2 + if not queryJson.query.stats[1].value then + queryJson.query.stats[1].value = { min = 0 } + end + queryJson.query.stats[1].value.min = (queryJson.query.stats[1].value.min or 0) / 2 query = dkjson.encode(queryJson) self:PerformSearch(realm, league, query, performSearchCallback) else -- Search clipped, fetch highest weight item, update query weight and repeat search @@ -197,12 +204,15 @@ function TradeQueryRequestsClass:SearchWithQueryWeightAdjusted(realm, league, qu local firstResultBatch = {unpack(response.result, 1, math.min(#response.result, 10))} self:FetchResults(firstResultBatch, response.id, function(items, errMsg) if errMsg then - return callback(nil, errMsg) + return resultCallback(nil, errMsg) end previousSearchItems = items local highestWeight = items[1].weight local queryJson = dkjson.decode(query) - queryJson.query.stats[1].value.min = (tonumber(highestWeight) + queryJson.query.stats[1].value.min) / 2 + if not queryJson.query.stats[1].value then + queryJson.query.stats[1].value = { min = 0 } + end + queryJson.query.stats[1].value.min = (tonumber(highestWeight) + (queryJson.query.stats[1].value.min or 0)) / 2 query = dkjson.encode(queryJson) self:PerformSearch(realm, league, query, performSearchCallback) end) diff --git a/src/Classes/TradeQueryRequests.lua.rej b/src/Classes/TradeQueryRequests.lua.rej new file mode 100644 index 00000000000..bc4b2500c27 --- /dev/null +++ b/src/Classes/TradeQueryRequests.lua.rej @@ -0,0 +1,58 @@ +diff a/src/Classes/TradeQueryRequests.lua b/src/Classes/TradeQueryRequests.lua (rejected hunks) +@@ -467,6 +467,9 @@ end + ---@param callback fun(items:table, errMsg:string, query: string?) + function TradeQueryRequestsClass:SearchWithURL(url, callback) + local subpath = url:match(self.hostName .. "trade2/search/(.+)$") ++ if not subpath then ++ return callback(nil, "Invalid URL") ++ end + local paths = {} + for path in subpath:gmatch("[^/]+") do + table.insert(paths, path) +@@ -485,27 +488,32 @@ function TradeQueryRequestsClass:SearchWithURL(url, callback) + return callback(nil, "URL is malformed") + end + local queryIdDecoded = dkjson.decode(json) +- if not queryIdDecoded or type(queryIdDecoded.stats) ~= "table" then ++ if type(queryIdDecoded) ~= "table" or type(queryIdDecoded.stats) ~= "table" or type(queryIdDecoded.stats[1]) ~= "table" then + return callback(nil, "URL is malformed") + end +- -- the trader assumes that the first stat group will be a weight group +- if queryIdDecoded.stats[1].type ~= "weight" then +- for i, group in ipairs(queryIdDecoded.stats) do +- -- swap a weight group to be the first group if it exists +- if group.type == "weight" then +- queryIdDecoded.stats[1], queryIdDecoded.stats[i] = queryIdDecoded.stats[i], queryIdDecoded.stats[1] +- break +- end ++ local weightGroupIndex ++ for i, group in ipairs(queryIdDecoded.stats) do ++ if type(group) ~= "table" or type(group.type) ~= "string" then ++ return callback(nil, "URL is malformed") ++ end ++ if group.type == "weight" and not weightGroupIndex then ++ weightGroupIndex = i + end +- end +- if queryIdDecoded.stats[1].type ~= "weight" then +- return callback(nil, "Trade search URL is not a weight search") + end + local newQuery = { + query = queryIdDecoded, +- sort = { ["statgroup.0"] = "desc" }, ++ sort = { price = "asc" }, + } +- self:SearchWithQueryWeightAdjusted(realm, league, dkjson.encode(newQuery), callback) ++ if weightGroupIndex then ++ -- Purchase links expect the weighted group first. ++ queryIdDecoded.stats[1], queryIdDecoded.stats[weightGroupIndex] = queryIdDecoded.stats[weightGroupIndex], queryIdDecoded.stats[1] ++ newQuery.sort = { ["statgroup.0"] = "desc" } ++ end ++ -- Pasted searches contain user constraints; only generated searches may adjust weights. ++ local query = dkjson.encode(newQuery) ++ self:SearchWithQuery(realm, league, query, function(items, errMsg) ++ callback(items, errMsg, query) ++ end) + end + + --- Fetches the list of all available leagues using trade2 league API diff --git a/src/Classes/TreeTab.lua.rej b/src/Classes/TreeTab.lua.rej new file mode 100644 index 00000000000..2efdcfeae28 --- /dev/null +++ b/src/Classes/TreeTab.lua.rej @@ -0,0 +1,52 @@ +diff a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua (rejected hunks) +@@ -18,6 +18,7 @@ local m_abs = math.abs + local s_format = string.format + local s_gsub = string.gsub + local s_byte = string.byte ++local tradeHelpers = require("Classes.TradeHelpers") + local dkjson = require "dkjson" + + -- Helper function to find toast index by content pattern +@@ -2012,22 +2013,17 @@ function TreeTabClass:FindTimelessJewel() + end + + local search = { +- query = { +- status = { +- option = "available" +- }, +- stats = { +- { +- filters = seedTrades, +- type = "count", +- value = { +- min = 1 +- } ++ status = { ++ option = "available" ++ }, ++ stats = { ++ { ++ filters = seedTrades, ++ type = "count", ++ value = { ++ min = 1 + } + } +- }, +- sort = { +- price = "asc" + } + } + +@@ -2050,9 +2046,7 @@ function TreeTabClass:FindTimelessJewel() + -- if the league was not selected via dropdown, then default to the first league in the dropdown or "" if the leagues could not be read + self.timelessJewelLeagueSelect = self.timelessJewelLeagueSelect or (self.tradeLeaguesList and #self.tradeLeaguesList > 0 and self.tradeLeaguesList[1]) or "" + +- Copy("https://www.pathofexile.com/trade/search/"..(self.timelessJewelLeagueSelect).."/?q=" .. (s_gsub(dkjson.encode(search), "[^a-zA-Z0-9]", function(a) +- return s_format("%%%02X", s_byte(a)) +- end))) ++ Copy("https://www.pathofexile.com/trade/search/" .. (self.timelessJewelLeagueSelect) .. "/" .. tradeHelpers.B64GzipEncode(dkjson.encode(search))) + + controls.searchTradeButton.label = "Copy Next Trade URL" + end) diff --git a/src/_SimpleGraphic.def.lua b/src/_SimpleGraphic.def.lua index 6e159e4134e..9336ee36c3c 100644 --- a/src/_SimpleGraphic.def.lua +++ b/src/_SimpleGraphic.def.lua @@ -360,13 +360,15 @@ function Copy(text) end function Paste() end ---@param data string +---@param isGzip boolean? Whether a Gzip header should be used instead of the default ZLib header. ---@return string? compressedData ---@return string? errMsg -function Deflate(data) +function Deflate(data, isGzip) + -- TODO: add FFI bindings to `runtime/zlib1.dll` similar to what SimpleGraphic does return "" end ----@param data string +---@param data string DEFLATE data with either ZLib or Gzip headers. The format is detected automatically. Raw DEFLATE data is not supported. ---@return string? data ---@return string? errMsg function Inflate(data) From 55b7d6df14be1a190b48183b59413d7d21fe4b3a Mon Sep 17 00:00:00 2001 From: LocalIdentity <31035929+LocalIdentity@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:22:43 +1100 Subject: [PATCH 2/2] Fix PoE 1 gzip trade link port Complete the query-table conversion across generated searches, pasted URLs, purchase links and Timeless Jewel searches. Preserve PoE 1 filters, console realms and encoded league names, and keep pasted weight constraints unchanged. Adapt and extend trade behavior tests, and remove rejected patch files and the unused ffi-zlib library. Validation: 666 default Busted tests pass, native gzip round-trip checks pass, and a gzip link returns results on the official PoE 1 trade site. --- LICENSE.md.rej | 36 -- runtime/lua/ffi-zlib.lua | 340 ------------------ spec/System/TestCommon_spec.lua.rej | 40 --- spec/System/TestCompareBuySimilar_spec.lua | 28 +- .../System/TestCompareBuySimilar_spec.lua.rej | 44 --- .../System/TestTimelessJewelSettings_spec.lua | 49 +++ spec/System/TestTradeHelpers_spec.lua.rej | 20 -- spec/System/TestTradeQueryGenerator_spec.lua | 15 +- .../TestTradeQueryGenerator_spec.lua.rej | 12 - spec/System/TestTradeQueryRequests_spec.lua | 20 +- spec/System/TestTradeQuery_spec.lua | 84 +++++ spec/System/TestTradeQuery_spec.lua.rej | 51 --- src/Classes/CompareBuySimilar.lua | 4 +- src/Classes/CompareBuySimilar.lua.rej | 12 - src/Classes/TradeQuery.lua | 18 +- src/Classes/TradeQuery.lua.rej | 13 - src/Classes/TradeQueryGenerator.lua | 13 +- src/Classes/TradeQueryGenerator.lua.rej | 24 -- src/Classes/TradeQueryRequests.lua | 76 ++-- src/Classes/TradeQueryRequests.lua.rej | 58 --- src/Classes/TreeTab.lua | 40 +-- src/Classes/TreeTab.lua.rej | 52 --- 22 files changed, 249 insertions(+), 800 deletions(-) delete mode 100644 LICENSE.md.rej delete mode 100644 runtime/lua/ffi-zlib.lua delete mode 100644 spec/System/TestCommon_spec.lua.rej delete mode 100644 spec/System/TestCompareBuySimilar_spec.lua.rej delete mode 100644 spec/System/TestTradeHelpers_spec.lua.rej delete mode 100644 spec/System/TestTradeQueryGenerator_spec.lua.rej delete mode 100644 spec/System/TestTradeQuery_spec.lua.rej delete mode 100644 src/Classes/CompareBuySimilar.lua.rej delete mode 100644 src/Classes/TradeQuery.lua.rej delete mode 100644 src/Classes/TradeQueryGenerator.lua.rej delete mode 100644 src/Classes/TradeQueryRequests.lua.rej delete mode 100644 src/Classes/TreeTab.lua.rej diff --git a/LICENSE.md.rej b/LICENSE.md.rej deleted file mode 100644 index d290145c6a6..00000000000 --- a/LICENSE.md.rej +++ /dev/null @@ -1,36 +0,0 @@ -diff a/LICENSE.md b/LICENSE.md (rejected hunks) -@@ -1358,32 +1358,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE --SOFTWARE. -- --******************************************************************************* -- --lua-ffi-zlib: -- --******************************************************************************* -- --MIT License -- --Copyright (c) 2016 Hamish Forbes -- --Permission is hereby granted, free of charge, to any person obtaining a copy --of this software and associated documentation files (the "Software"), to deal --in the Software without restriction, including without limitation the rights --to use, copy, modify, merge, publish, distribute, sublicense, and/or sell --copies of the Software, and to permit persons to whom the Software is --furnished to do so, subject to the following conditions: -- --The above copyright notice and this permission notice shall be included in all --copies or substantial portions of the Software. -- --THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR --IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, --FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE --AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER --LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, --OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE --SOFTWARE. -+SOFTWARE. -\ No newline at end of file diff --git a/runtime/lua/ffi-zlib.lua b/runtime/lua/ffi-zlib.lua deleted file mode 100644 index c099d98bfeb..00000000000 --- a/runtime/lua/ffi-zlib.lua +++ /dev/null @@ -1,340 +0,0 @@ -local ffi = require "ffi" -local ffi_new = ffi.new -local ffi_str = ffi.string -local ffi_sizeof = ffi.sizeof -local ffi_copy = ffi.copy -local tonumber = tonumber - -local _M = { - _VERSION = '0.6.0', -} - -local mt = { __index = _M } - - -ffi.cdef([[ -enum { - Z_NO_FLUSH = 0, - Z_PARTIAL_FLUSH = 1, - Z_SYNC_FLUSH = 2, - Z_FULL_FLUSH = 3, - Z_FINISH = 4, - Z_BLOCK = 5, - Z_TREES = 6, - /* Allowed flush values; see deflate() and inflate() below for details */ - Z_OK = 0, - Z_STREAM_END = 1, - Z_NEED_DICT = 2, - Z_ERRNO = -1, - Z_STREAM_ERROR = -2, - Z_DATA_ERROR = -3, - Z_MEM_ERROR = -4, - Z_BUF_ERROR = -5, - Z_VERSION_ERROR = -6, - /* Return codes for the compression/decompression functions. Negative values - * are errors, positive values are used for special but normal events. - */ - Z_NO_COMPRESSION = 0, - Z_BEST_SPEED = 1, - Z_BEST_COMPRESSION = 9, - Z_DEFAULT_COMPRESSION = -1, - /* compression levels */ - Z_FILTERED = 1, - Z_HUFFMAN_ONLY = 2, - Z_RLE = 3, - Z_FIXED = 4, - Z_DEFAULT_STRATEGY = 0, - /* compression strategy; see deflateInit2() below for details */ - Z_BINARY = 0, - Z_TEXT = 1, - Z_ASCII = Z_TEXT, /* for compatibility with 1.2.2 and earlier */ - Z_UNKNOWN = 2, - /* Possible values of the data_type field (though see inflate()) */ - Z_DEFLATED = 8, - /* The deflate compression method (the only one supported in this version) */ - Z_NULL = 0, /* for initializing zalloc, zfree, opaque */ -}; - - -typedef void* (* z_alloc_func)( void* opaque, unsigned items, unsigned size ); -typedef void (* z_free_func) ( void* opaque, void* address ); - -typedef struct z_stream_s { - char* next_in; - unsigned avail_in; - unsigned long total_in; - char* next_out; - unsigned avail_out; - unsigned long total_out; - char* msg; - void* state; - z_alloc_func zalloc; - z_free_func zfree; - void* opaque; - int data_type; - unsigned long adler; - unsigned long reserved; -} z_stream; - - -const char* zlibVersion(); -const char* zError(int); - -int inflate(z_stream*, int flush); -int inflateEnd(z_stream*); -int inflateInit2_(z_stream*, int windowBits, const char* version, int stream_size); - -int deflate(z_stream*, int flush); -int deflateEnd(z_stream* ); -int deflateInit2_(z_stream*, int level, int method, int windowBits, int memLevel,int strategy, const char *version, int stream_size); - -unsigned long adler32(unsigned long adler, const char *buf, unsigned len); -unsigned long crc32(unsigned long crc, const char *buf, unsigned len); -unsigned long adler32_combine(unsigned long, unsigned long, long); -unsigned long crc32_combine(unsigned long, unsigned long, long); - -]]) - -local zlib -if ffi.os == "Windows" then - zlib = ffi.load("../runtime/zlib1") -elseif ffi.os == "OSX" then - zlib = ffi.load("z") -elseif ffi.os == "Linux" then - zlib = ffi.load("libz.so.1") -else - error("lua-ffi-zlib doesn't support platform: " .. ffi.os) -end - -_M.zlib = zlib - --- Default to 16k output buffer -local DEFAULT_CHUNK = 16384 - -local Z_OK = zlib.Z_OK -local Z_NO_FLUSH = zlib.Z_NO_FLUSH -local Z_STREAM_END = zlib.Z_STREAM_END -local Z_FINISH = zlib.Z_FINISH -local Z_NEED_DICT = zlib.Z_NEED_DICT -local Z_BUF_ERROR = zlib.Z_BUF_ERROR -local Z_STREAM_ERROR = zlib.Z_STREAM_ERROR - -local function zlib_err(err) - return ffi_str(zlib.zError(err)) -end -_M.zlib_err = zlib_err - -local function createStream(bufsize) - -- Setup Stream - local stream = ffi_new("z_stream") - - -- Create input buffer var - local inbuf = ffi_new('char[?]', bufsize+1) - stream.next_in, stream.avail_in = inbuf, 0 - - -- create the output buffer - local outbuf = ffi_new('char[?]', bufsize) - stream.next_out, stream.avail_out = outbuf, 0 - - return stream, inbuf, outbuf -end -_M.createStream = createStream - -local function initInflate(stream, windowBits) - -- Setup inflate process - local windowBits = windowBits or (15 + 32) -- +32 sets automatic header detection - local version = ffi_str(zlib.zlibVersion()) - - return zlib.inflateInit2_(stream, windowBits, version, ffi_sizeof(stream)) -end -_M.initInflate = initInflate - -local function initDeflate(stream, options) - -- Setup deflate process - local method = zlib.Z_DEFLATED - local level = options.level or zlib.Z_DEFAULT_COMPRESSION - local memLevel = options.memLevel or 8 - local strategy = options.strategy or zlib.Z_DEFAULT_STRATEGY - local windowBits = options.windowBits or (15 + 16) -- +16 sets gzip wrapper not zlib - local version = ffi_str(zlib.zlibVersion()) - - return zlib.deflateInit2_(stream, level, method, windowBits, memLevel, strategy, version, ffi_sizeof(stream)) -end -_M.initDeflate = initDeflate - -local function flushOutput(stream, bufsize, output, outbuf) - -- Calculate available output bytes - local out_sz = bufsize - stream.avail_out - if out_sz == 0 then - return - end - -- Read bytes from output buffer and pass to output function - local ok, err = output(ffi_str(outbuf, out_sz)) - if not ok then - return err - end -end - -local function inflate(input, output, bufsize, stream, inbuf, outbuf) - local zlib_flate = zlib.inflate - local zlib_flateEnd = zlib.inflateEnd - -- Inflate a stream - local err = 0 - repeat - -- Read some input - local data = input(bufsize) - if data ~= nil then - ffi_copy(inbuf, data) - stream.next_in, stream.avail_in = inbuf, #data - else - -- no more input data - stream.avail_in = 0 - end - - if stream.avail_in == 0 then - -- When decompressing we *must* have input bytes - zlib_flateEnd(stream) - return false, "INFLATE: Data error, no input bytes" - end - - -- While the output buffer is being filled completely just keep going - repeat - stream.next_out = outbuf - stream.avail_out = bufsize - -- Process the stream, always Z_NO_FLUSH in inflate mode - err = zlib_flate(stream, Z_NO_FLUSH) - - -- Buffer errors are OK here - if err == Z_BUF_ERROR then - err = Z_OK - end - if err < Z_OK or err == Z_NEED_DICT then - -- Error, clean up and return - zlib_flateEnd(stream) - return false, "INFLATE: "..zlib_err(err), stream - end - -- Write the data out - local err = flushOutput(stream, bufsize, output, outbuf) - if err then - zlib_flateEnd(stream) - return false, "INFLATE: "..err - end - until stream.avail_out ~= 0 - - until err == Z_STREAM_END - - -- Stream finished, clean up and return - zlib_flateEnd(stream) - return true, zlib_err(err) -end -_M.inflate = inflate - -local function deflate(input, output, bufsize, stream, inbuf, outbuf) - local zlib_flate = zlib.deflate - local zlib_flateEnd = zlib.deflateEnd - - -- Deflate a stream - local err = 0 - local mode = Z_NO_FLUSH - repeat - -- Read some input - local data = input(bufsize) - if data ~= nil then - ffi_copy(inbuf, data) - stream.next_in, stream.avail_in = inbuf, #data - else - -- EOF, try and finish up - mode = Z_FINISH - stream.avail_in = 0 - end - - -- While the output buffer is being filled completely just keep going - repeat - stream.next_out = outbuf - stream.avail_out = bufsize - - -- Process the stream - err = zlib_flate(stream, mode) - - -- Only possible *bad* return value here - if err == Z_STREAM_ERROR then - -- Error, clean up and return - zlib_flateEnd(stream) - return false, "DEFLATE: "..zlib_err(err), stream - end - -- Write the data out - local err = flushOutput(stream, bufsize, output, outbuf) - if err then - zlib_flateEnd(stream) - return false, "DEFLATE: "..err - end - until stream.avail_out ~= 0 - - -- In deflate mode all input must be used by this point - if stream.avail_in ~= 0 then - zlib_flateEnd(stream) - return false, "DEFLATE: Input not used" - end - - until err == Z_STREAM_END - - -- Stream finished, clean up and return - zlib_flateEnd(stream) - return true, zlib_err(err) -end -_M.deflate = deflate - -local function adler(str, chksum) - local chksum = chksum or 0 - local str = str or "" - return zlib.adler32(chksum, str, #str) -end -_M.adler = adler - -local function crc(str, chksum) - local chksum = chksum or 0 - local str = str or "" - return zlib.crc32(chksum, str, #str) -end -_M.crc = crc - -function _M.inflateGzip(input, output, bufsize, windowBits) - local bufsize = bufsize or DEFAULT_CHUNK - - -- Takes 2 functions that provide input data from a gzip stream and receives output data - -- Returns uncompressed string - local stream, inbuf, outbuf = createStream(bufsize) - - local init = initInflate(stream, windowBits) - if init == Z_OK then - return inflate(input, output, bufsize, stream, inbuf, outbuf) - else - -- Init error - zlib.inflateEnd(stream) - return false, "INIT: "..zlib_err(init) - end -end - -function _M.deflateGzip(input, output, bufsize, options) - local bufsize = bufsize or DEFAULT_CHUNK - options = options or {} - - -- Takes 2 functions that provide plain input data and receives output data - -- Returns gzip compressed string - local stream, inbuf, outbuf = createStream(bufsize) - - local init = initDeflate(stream, options) - if init == Z_OK then - return deflate(input, output, bufsize, stream, inbuf, outbuf) - else - -- Init error - zlib.deflateEnd(stream) - return false, "INIT: "..zlib_err(init) - end -end - -function _M.version() - return ffi_str(zlib.zlibVersion()) -end - -return _M diff --git a/spec/System/TestCommon_spec.lua.rej b/spec/System/TestCommon_spec.lua.rej deleted file mode 100644 index d173754a30a..00000000000 --- a/spec/System/TestCommon_spec.lua.rej +++ /dev/null @@ -1,40 +0,0 @@ -diff a/spec/System/TestCommon_spec.lua b/spec/System/TestCommon_spec.lua (rejected hunks) -@@ -94,36 +94,4 @@ describe("Common", function() - -- common.classes.StupidClass = nil - -- end) - end) -- -- tests for headless wrapper implementation of deflate/inflate. disabled until support is added -- -- describe("Deflate and Inflate", function() -- -- it("round-trips a simple string", function() -- -- local text = "Hello my name is ????!" -- -- local compressed = Deflate(text) -- -- assert.is_not_nil(compressed) -- -- assert.are.equal(text, Inflate(compressed)) -- -- end) -- -- it("produces a zlib header", function() -- -- local compressed = Deflate("some data to compress") -- -- assert.are.equal(0x78, compressed:byte(1)) -- -- end) -- -- it("round-trips an empty string", function() -- -- local compressed = Deflate("") -- -- assert.is_not_nil(compressed) -- -- assert.are.equal("", Inflate(compressed)) -- -- end) -- -- it("round-trips data larger than the 16k buffer", function() -- -- local text = string.rep("The quick brown fox jumps over the lazy dog. ", 5000) -- -- local compressed = Deflate(text) -- -- assert.is_true(#compressed < #text) -- -- assert.are.equal(text, Inflate(compressed)) -- -- end) -- -- it("round-trips binary data", function() -- -- local bytes = {} -- -- for i = 0, 255 do -- -- bytes[i + 1] = string.char(i) -- -- end -- -- local text = table.concat(bytes) -- -- assert.are.equal(text, Inflate(Deflate(text))) -- -- end) -- -- end) --end) -\ No newline at end of file -+end) diff --git a/spec/System/TestCompareBuySimilar_spec.lua b/spec/System/TestCompareBuySimilar_spec.lua index fc38217f6e8..d2b8e0d4136 100644 --- a/spec/System/TestCompareBuySimilar_spec.lua +++ b/spec/System/TestCompareBuySimilar_spec.lua @@ -123,6 +123,8 @@ Can have 3 additional Enchantment Modifiers]]) end) end) describe("popup URL controls", function() + local tradeHelpers = require "Classes.TradeHelpers" + local originalEncode, encodedQuery local originalCopy local originalOpenURL local originalFetchLeagues @@ -135,6 +137,12 @@ Can have 3 additional Enchantment Modifiers]]) before_each(function() newBuild() + originalEncode = tradeHelpers.B64GzipEncode + -- SimpleGraphic provides compression; capture the query at that boundary. + tradeHelpers.B64GzipEncode = function(json) + encodedQuery = dkjson.decode(json) + return urlEncode(json) + end originalCopy = _G.Copy originalOpenURL = _G.OpenURL copiedUrl = nil @@ -146,6 +154,7 @@ Can have 3 additional Enchantment Modifiers]]) end) after_each(function() + tradeHelpers.B64GzipEncode = originalEncode build.itemsTab.tradeQuery.tradeQueryRequests.FetchLeagues = originalFetchLeagues _G.Copy = originalCopy _G.OpenURL = originalOpenURL @@ -166,8 +175,7 @@ Can have 3 additional Enchantment Modifiers]]) local function getQuery(controls) controls.search.onClick() - local queryJson = urlDecode(assert(copiedUrl:match("[?&]q=(.+)$"))) - return dkjson.decode(queryJson) + return encodedQuery end local function getUniqueQuery(name, baseName) @@ -185,13 +193,13 @@ Can have 3 additional Enchantment Modifiers]]) controls.leagueDrop:SetSel(isValueInArray(controls.leagueDrop.list, "Standard")) getQuery(controls) assert.not_equal(initialUrl, copiedUrl) - assert.is_truthy(copiedUrl:find("/Standard?", 1, true)) + assert.is_truthy(copiedUrl:find("/Standard/", 1, true)) local standardUrl = copiedUrl controls.listedDrop:SetSel(4) local query = getQuery(controls) assert.not_equal(standardUrl, copiedUrl) - assert.equal("any", query.query.status.option) + assert.equal("any", query.status.option) end) it("preserves apostrophes in unique names", function() @@ -202,15 +210,15 @@ Riveted Boots Implicits: 0]]) local controls = openPopup(item, "Boots") - assert.equal("Ralakesh's Impatience", getQuery(controls).query.name) + assert.equal("Ralakesh's Impatience", getQuery(controls).name) end) it("removes a trailing unique ID without removing name punctuation", function() - assert.equal("Uul-Netol's Embrace", getUniqueQuery("Uul-Netol's Embrace 1234", "Vaal Axe").query.name) + assert.equal("Uul-Netol's Embrace", getUniqueQuery("Uul-Netol's Embrace 1234", "Vaal Axe").name) end) it("trims whitespace after a unique name", function() - assert.equal("The Hateful Accuser", getUniqueQuery("The Hateful Accuser ", "Ghastly Eye Jewel").query.name) + assert.equal("The Hateful Accuser", getUniqueQuery("The Hateful Accuser ", "Ghastly Eye Jewel").name) end) it("persists league choices by name for each realm", function() @@ -250,7 +258,7 @@ Corrupted]]) controls.mod1Check.state = true controls.mod1Check.changeFunc() - local filter = getQuery(controls).query.stats[1].filters[1] + local filter = getQuery(controls).stats[1].filters[1] assert.equal("explicit.stat_2422708892", filter.id) assert.equal(22088, filter.value.option) end) @@ -266,7 +274,7 @@ Implicits: 0 controls.mod1Check.state = true controls.mod1Check.changeFunc() - local value = getQuery(controls).query.stats[1].filters[1].value + local value = getQuery(controls).stats[1].filters[1].value assert.is_nil(value.min) assert.equal(-67, value.max) end) @@ -293,7 +301,7 @@ Adds 5 to 15 Fire Damage]]) check.state = true check.changeFunc() - local countFilter = getQuery(controls).query.stats[2] + local countFilter = getQuery(controls).stats[2] assert.equal("count", countFilter.type) assert.equal(1, countFilter.value.min) assert.equal(#modEntries[ambiguousIndex].tradeIds, #countFilter.filters) diff --git a/spec/System/TestCompareBuySimilar_spec.lua.rej b/spec/System/TestCompareBuySimilar_spec.lua.rej deleted file mode 100644 index 19d9c59d2ef..00000000000 --- a/spec/System/TestCompareBuySimilar_spec.lua.rej +++ /dev/null @@ -1,44 +0,0 @@ -diff a/spec/System/TestCompareBuySimilar_spec.lua b/spec/System/TestCompareBuySimilar_spec.lua (rejected hunks) -@@ -149,12 +149,13 @@ Implicits: 1 - controls.mod1Check.state = true - controls.mod1Check.changeFunc(true) - controls.search.onClick() -- local queryB64 = copiedUrl:match("Test%%20League/(.*)$"):gsub("%%(%x%x)", function(hex) -- return string.char(tonumber(hex, 16)) -- end) -- local query = require("dkjson").decode(require("Classes.TradeHelpers").B64GzipDecode(queryB64)) -+ -- disabled for now due to headless wrapper lacking zlib bindings -+ -- local queryB64 = copiedUrl:match("Test%%20League/(.*)$"):gsub("%%(%x%x)", function(hex) -+ -- return string.char(tonumber(hex, 16)) -+ -- end) -+ -- local query = require("dkjson").decode(require("Classes.TradeHelpers").B64GzipDecode(queryB64)) - -- assert.same({ { type = "and", filters = { { id = "explicit.stat_1526933524" } } } }, query.stats) -+ -- assert.same({ { type = "and", filters = { { id = "explicit.stat_1526933524" } } } }, query.stats) - end) - - it("rebuilds the URL when league and listed status change", function() -@@ -166,14 +167,16 @@ Implicits: 1 - controls.search.onClick() - assert.not_equal(initialUrl, copiedUrl) - assert.is_truthy(copiedUrl:find("/Standard/", 1, true)) -- local standardUrl = copiedUrl - -- controls.listedDrop:SetSel(4) -- controls.search.onClick() -- assert.not_equal(standardUrl, copiedUrl) -- local b64 = copiedUrl:match("Standard/(.-)$") -- local json = require("Classes.TradeHelpers").B64GzipDecode(b64) -- assert.is_truthy(json:find("any", 1, true)) -+ -- disabled for now due to headless wrapper lacking zlib bindings -+ -- local standardUrl = copiedUrl -+ -+ -- controls.listedDrop:SetSel(4) -+ -- controls.search.onClick() -+ -- assert.not_equal(standardUrl, copiedUrl) -+ -- local b64 = copiedUrl:match("Standard/(.-)$") -+ -- local json = require("Classes.TradeHelpers").B64GzipDecode(b64) -+ -- assert.is_truthy(json:find("any", 1, true)) - end) - - it("persists popup selector choices", function() diff --git a/spec/System/TestTimelessJewelSettings_spec.lua b/spec/System/TestTimelessJewelSettings_spec.lua index 50858ed40b3..9a2719cf617 100644 --- a/spec/System/TestTimelessJewelSettings_spec.lua +++ b/spec/System/TestTimelessJewelSettings_spec.lua @@ -3,6 +3,55 @@ describe("TestTimelessJewelSettings", function() newBuild() end) + describe("trade search", function() + local tradeHelpers = require "Classes.TradeHelpers" + local originalEncode, originalCopy, originalOpenURL + local query, copiedUrl, openedUrl + + before_each(function() + originalEncode, originalCopy, originalOpenURL = tradeHelpers.B64GzipEncode, _G.Copy, _G.OpenURL + tradeHelpers.B64GzipEncode = function(json) + query = require("dkjson").decode(json) + return "encoded" + end + _G.Copy = function(url) copiedUrl = url end + _G.OpenURL = function(url) openedUrl = url end + end) + + after_each(function() + tradeHelpers.B64GzipEncode, _G.Copy, _G.OpenURL = originalEncode, originalCopy, originalOpenURL + main:ClosePopup() + end) + + it("includes seed and devotion constraints in a console trade link", function() + build.timelessData.jewelType = { id = 4 } + build.treeTab.tradeLeaguesList = { pc = { "Standard" }, xbox = { "Hardcore Test" } } + build.treeTab:FindTimelessJewel() + local controls = main.popups[1].controls + build.timelessData.sharedResults = { + conqueror = { id = 3 }, devotionVariant1 = { id = 2 }, devotionVariant2 = { id = 7 }, + } + build.timelessData.searchResults = { { seed = 5000 }, { seed = 6000 } } + controls.realmSelection:SetSel(3) + controls.tradeTypeSelection:SetSel(2) + controls.searchTradeButton.onClick() + + assert.are.equal("https://www.pathofexile.com/trade/search/xbox/Hardcore%20Test/encoded", openedUrl) + assert.are.equal(openedUrl, copiedUrl) + assert.are.equal("available", query.status.option) + assert.same({ + type = "count", value = { min = 1 }, filters = { + { id = data.timelessJewelTradeIDs[4].keystone[2], value = { min = 5000, max = 5000 } }, + { id = data.timelessJewelTradeIDs[4].keystone[2], value = { min = 6000, max = 6000 } }, + }, + }, query.stats[1]) + assert.same({ type = "and", filters = { + { id = data.timelessJewelTradeIDs[4].devotion[1] }, + { id = data.timelessJewelTradeIDs[4].devotion[6] }, + } }, query.stats[2]) + end) + end) + -- The finder's "Socket Jewel" toggle lives in timelessData, which only reaches the -- build XML through the TimelessData element, so cover the save/load round trip. it("round trips socketAllocate through the build XML", function() diff --git a/spec/System/TestTradeHelpers_spec.lua.rej b/spec/System/TestTradeHelpers_spec.lua.rej deleted file mode 100644 index 75bff6807cc..00000000000 --- a/spec/System/TestTradeHelpers_spec.lua.rej +++ /dev/null @@ -1,20 +0,0 @@ -diff a/spec/System/TestTradeHelpers_spec.lua b/spec/System/TestTradeHelpers_spec.lua (rejected hunks) -@@ -169,18 +169,4 @@ describe("TradeHelpers trade hash matching", function() - assert.is_nil(tradeHelpers.findTradeIdOption("+100 to IQ", "explicit")) - end) - end) -- -- disabled for now since the headless wrapper has no zlib bindings -- -- it("gzip decode", function() -- -- local sampleText = "Test string please ignore" -- -- local gzipped = tradeHelpers.B64GzipEncode(sampleText) -- -- local roundTrip = tradeHelpers.B64GzipDecode(gzipped) -- -- -- assert.are.Equal(sampleText, roundTrip) -- -- assert.are_not_equal(sampleText, gzipped) -- -- -- local longText = string.rep("12345678", 4096) -- -- local gzippedLong = tradeHelpers.B64GzipEncode(longText) -- -- local longRoundTrip = tradeHelpers.B64GzipDecode(gzippedLong) -- -- assert.are.Equal(longText, longRoundTrip) -- -- end) - end) diff --git a/spec/System/TestTradeQueryGenerator_spec.lua b/spec/System/TestTradeQueryGenerator_spec.lua index ff8b269fc0d..4f3c035b5eb 100644 --- a/spec/System/TestTradeQueryGenerator_spec.lua +++ b/spec/System/TestTradeQueryGenerator_spec.lua @@ -193,8 +193,8 @@ describe("TradeQueryGenerator", function() } queryGen.tradeTypeIndex = 1 local query - queryGen.requesterCallback = function(_, queryJson) - query = require("dkjson").decode(queryJson).query + queryGen.requesterCallback = function(_, generatedQuery) + query = generatedQuery end queryGen:FinishQuery() @@ -202,6 +202,17 @@ describe("TradeQueryGenerator", function() assert.are.equal(31, #query.stats[1].filters) assert.is_not_nil(query.filters.socket_filters.filters.sockets) assert.is_not_nil(query.filters.socket_filters.filters.links) + + -- Account searches must also work without a maximum price. + queryGen.calcContext.options = { + statWeights = {}, influence1 = 1, influence2 = 1, + includeMirrored = true, account = "ExampleAccount", maxLevel = 80, links = 5, + } + queryGen:FinishQuery() + assert.same({ input = "ExampleAccount" }, query.filters.trade_filters.filters.account) + assert.same({ max = 80 }, query.filters.req_filters.filters.lvl) + assert.same({ min = 5, max = 5 }, query.filters.socket_filters.filters.links) + assert.is_nil(query.filters.socket_filters.filters.sockets) end) end) end) diff --git a/spec/System/TestTradeQueryGenerator_spec.lua.rej b/spec/System/TestTradeQueryGenerator_spec.lua.rej deleted file mode 100644 index 7a53aaa68d8..00000000000 --- a/spec/System/TestTradeQueryGenerator_spec.lua.rej +++ /dev/null @@ -1,12 +0,0 @@ -diff a/spec/System/TestTradeQueryGenerator_spec.lua b/spec/System/TestTradeQueryGenerator_spec.lua (rejected hunks) -@@ -148,8 +148,8 @@ describe("TradeQueryGenerator", function() - } - queryGen.tradeTypeIndex = 1 - local query -- queryGen.requesterCallback = function(_, queryJson) -- query = require("dkjson").decode(queryJson).query -+ queryGen.requesterCallback = function(_, queryTable) -+ query = queryTable - end - queryGen:FinishQuery() - diff --git a/spec/System/TestTradeQueryRequests_spec.lua b/spec/System/TestTradeQueryRequests_spec.lua index e7921d90f62..ccb4fc6c51d 100644 --- a/spec/System/TestTradeQueryRequests_spec.lua +++ b/spec/System/TestTradeQueryRequests_spec.lua @@ -287,7 +287,7 @@ Strict-Transport-Security: max-age=63115200; includeSubDomains; preload]] local dkjson = require "dkjson" local tradeHelpers = require "Classes.TradeHelpers" local originalDecode, urlRequests, decodedQuery - local url = "https://www.pathofexile.com/trade2/search/poe2/Standard/encoded" + local url = "https://www.pathofexile.com/trade/search/Standard/encoded" before_each(function() urlRequests = new("TradeQueryRequests"):TradeQueryRequests(mock_limiter) @@ -300,6 +300,18 @@ Strict-Transport-Security: max-age=63115200; includeSubDomains; preload]] tradeHelpers.B64GzipDecode = originalDecode end) + for _, realmPath in ipairs({ "", "sony/", "xbox/" }) do + it("preserves the realm and league in " .. realmPath .. "Hardcore%20Test", function() + tradeHelpers.B64GzipDecode = function(encoded) + assert.are.equal("encoded", encoded) + return [[{"stats":[{"type":"and","filters":[]}]}]] + end + urlRequests:SearchWithURL("https://pathofexile.com/trade/search/" .. realmPath .. "Hardcore%20Test/encoded", function() end) + assert.are.equal("https://www.pathofexile.com/api/trade/search/" .. realmPath .. "Hardcore%20Test", + urlRequests.requestQueue.search[1].url) + end) + end + it("fetches ordinary searches by price and returns their query", function() decodedQuery = [[{"stats":[{"type":"and","filters":[{"id":"explicit.stat_3299347043","value":{"min":1}}]}]}]] local expectedQuery = dkjson.decode(decodedQuery) @@ -314,10 +326,10 @@ Strict-Transport-Security: max-age=63115200; includeSubDomains; preload]] calls = calls + 1 assert.is_nil(err) assert.same(fetchedItems, items) - assert.same({ query = expectedQuery, sort = { price = "asc" } }, dkjson.decode(query)) + assert.same({ query = expectedQuery, sort = { price = "asc" }, engine = "new" }, dkjson.decode(query)) end) local search = table.remove(urlRequests.requestQueue.search, 1) - assert.are.equal("https://www.pathofexile.com/api/trade2/search/poe2/Standard", search.url) + assert.are.equal("https://www.pathofexile.com/api/trade/search/Standard", search.url) search.callback([[{"total":1,"result":["item1"],"id":"searchId"}]]) assert.are.equal(1, calls) assert.are.equal(0, #urlRequests.requestQueue.search) @@ -369,7 +381,7 @@ Strict-Transport-Security: max-age=63115200; includeSubDomains; preload]] callbackCalls = callbackCalls + 1 assert.is_nil(err) assert.same(fetchedItems, items) - assert.same({ query = suppliedQuery, sort = { ["statgroup.0"] = "desc" } }, dkjson.decode(query)) + assert.same({ query = suppliedQuery, sort = { ["statgroup.0"] = "desc" }, engine = "new" }, dkjson.decode(query)) end) assert.are.equal(1, #urlRequests.requestQueue.search) local search = table.remove(urlRequests.requestQueue.search, 1) diff --git a/spec/System/TestTradeQuery_spec.lua b/spec/System/TestTradeQuery_spec.lua index c700a332cf8..9546edac43e 100644 --- a/spec/System/TestTradeQuery_spec.lua +++ b/spec/System/TestTradeQuery_spec.lua @@ -61,6 +61,90 @@ describe("TradeQuery", function() assert.are.equal(0, #tooltip.lines) end) end) + describe("trade search links", function() + local dkjson = require "dkjson" + local tradeHelpers = require "Classes.TradeHelpers" + local originalEncode, originalCopy, originalOpenURL, originalToken + local encodedQuery, copiedUrl, openedUrl + local generatedQuery = { stats = { { type = "weight", value = { min = 10 }, filters = {} } } } + + before_each(function() + originalEncode, originalCopy, originalOpenURL, originalToken = tradeHelpers.B64GzipEncode, _G.Copy, _G.OpenURL, main.api.authToken + tradeHelpers.B64GzipEncode = function(json) + encodedQuery = dkjson.decode(json) + return "encoded" + end + _G.Copy = function(url) copiedUrl = url end + _G.OpenURL = function(url) openedUrl = url end + encodedQuery, copiedUrl, openedUrl = nil, nil, nil + mock_tradeQuery.itemsTab.activeItemSet = {} + mock_tradeQuery.itemsTab.slots = {} + mock_tradeQuery.slotTables[1] = { slotName = "Ring 1" } + mock_tradeQuery.controls.pbNotice = {} + mock_tradeQuery:PriceItemRowDisplay(1, nil, 0, 20) + mock_tradeQuery.pbRealm = "sony" + mock_tradeQuery.pbLeague = "Hardcore Test" + mock_tradeQuery.tradeQueryGenerator = { + RequestQuery = function(_, slot, context, weights, callback) + callback(context, copyTable(generatedQuery, true)) + end, + } + end) + + after_each(function() + tradeHelpers.B64GzipEncode, _G.Copy, _G.OpenURL, main.api.authToken = originalEncode, originalCopy, originalOpenURL, originalToken + end) + + it("creates an unauthenticated search link from the generated query", function() + main.api.authToken = nil + mock_tradeQuery.controls.bestButton1.onClick() + assert.same(generatedQuery, encodedQuery) + assert.are.equal("https://www.pathofexile.com/trade/search/sony/Hardcore%20Test/encoded", mock_tradeQuery.controls.uri1.buf) + assert.is_true(mock_tradeQuery.controls.uri1.validURL) + assert.are.equal(0, #mock_tradeQuery.tradeQueryRequests.requestQueue.search) + end) + + it("posts generated queries with weighted sorting and keeps the adjusted query", function() + main.api.authToken = "test-token" + -- Complete two searches through the real request flow, without fetching items. + mock_tradeQuery.tradeQueryRequests.FetchResults = function(_, ids, queryId, callback) callback({}) end + mock_tradeQuery.UpdateControlsWithItems = function() end + mock_tradeQuery.controls.bestButton1.onClick() + local requests = mock_tradeQuery.tradeQueryRequests.requestQueue.search + local firstSearch = table.remove(requests, 1) + assert.are.equal("https://www.pathofexile.com/api/trade/search/sony/Hardcore%20Test", firstSearch.url) + assert.same({ query = generatedQuery, sort = { ["statgroup.0"] = "desc" }, engine = "new" }, dkjson.decode(firstSearch.body)) + firstSearch.callback([[{"id":"first","total":1,"result":["item1"]}]]) + local finalSearch = table.remove(requests, 1) + assert.are.equal(5, dkjson.decode(finalSearch.body).query.stats[1].value.min) + finalSearch.callback([[{"id":"final","total":10,"result":["item1"]}]]) + assert.are.equal(finalSearch.body, mock_tradeQuery.lastQueries[1]) + assert.are.equal("https://www.pathofexile.com/trade/search/sony/Hardcore%20Test/final", mock_tradeQuery.controls.uri1.buf) + assert.are.equal("Price Item", mock_tradeQuery.controls.priceButton1.label) + end) + + for _, group in ipairs({ + { type = "and", filters = {} }, + { type = "count", value = { min = 1 }, filters = {} }, + { type = "weight", value = { min = 10 }, filters = {} }, + }) do + it("preserves " .. group.type .. " group semantics in purchase links", function() + mock_tradeQuery.itemIndexTbl[1] = 1 + mock_tradeQuery.resultTbl[1] = { { trader = "ExampleAccount", weight = "100", priceType = "~b/o" } } + mock_tradeQuery.lastQueries[1] = dkjson.encode({ query = { stats = { group } } }) + mock_tradeQuery.controls.whisperButton1.onClick() + if group.type == "weight" then + assert.same({ min = 99, max = 101 }, encodedQuery.stats[1].value) + else + assert.same(group, encodedQuery.stats[1]) + end + assert.same({ input = "ExampleAccount" }, encodedQuery.filters.trade_filters.filters.account) + assert.are.equal("https://www.pathofexile.com/trade/search/sony/Hardcore%20Test/encoded", openedUrl) + assert.are.equal(openedUrl, copiedUrl) + end) + end + end) + describe("GetResultEvaluation", function() it("uses the first visible ring for a Pearl result without a selected slot", function() local tq = new("TradeQuery"):TradeQuery({ itemsTab = {} }) diff --git a/spec/System/TestTradeQuery_spec.lua.rej b/spec/System/TestTradeQuery_spec.lua.rej deleted file mode 100644 index 3df71f731b1..00000000000 --- a/spec/System/TestTradeQuery_spec.lua.rej +++ /dev/null @@ -1,51 +0,0 @@ -diff a/spec/System/TestTradeQuery_spec.lua b/spec/System/TestTradeQuery_spec.lua (rejected hunks) -@@ -62,6 +62,49 @@ describe("TradeQuery", function () - end) - end) - -+ describe("purchase search filters", function() -+ local dkjson = require "dkjson" -+ local tradeHelpers = require "Classes.TradeHelpers" -+ local originalEncode, originalCopy, originalOpenURL, encodedQuery -+ -+ before_each(function() -+ originalEncode, originalCopy, originalOpenURL = tradeHelpers.B64GzipEncode, Copy, OpenURL -+ tradeHelpers.B64GzipEncode = function(query) -+ encodedQuery = dkjson.decode(query) -+ return "encoded" -+ end -+ Copy, OpenURL = function() end, function() end -+ mock_tradeQuery.itemsTab.activeItemSet = {} -+ mock_tradeQuery.itemsTab.slots = {} -+ mock_tradeQuery.slotTables[1] = { slotName = "Ring 1" } -+ mock_tradeQuery:PriceItemRowDisplay(1, nil, 0, 20) -+ mock_tradeQuery.pbLeague = "Standard" -+ mock_tradeQuery.itemIndexTbl[1] = 1 -+ mock_tradeQuery.resultTbl[1] = { { trader = "ExampleAccount", weight = "100", priceType = "~b/o" } } -+ end) -+ -+ after_each(function() -+ tradeHelpers.B64GzipEncode, Copy, OpenURL = originalEncode, originalCopy, originalOpenURL -+ end) -+ -+ for _, group in ipairs({ -+ { type = "and", filters = {} }, -+ { type = "count", value = { min = 1 }, filters = {} }, -+ { type = "weight", value = { min = 10 }, filters = {} }, -+ }) do -+ it("preserves " .. group.type .. " group semantics when opening a purchase search", function() -+ mock_tradeQuery.lastQueries[1] = dkjson.encode({ query = { stats = { group } } }) -+ mock_tradeQuery.controls.whisperButton1.onClick() -+ if group.type == "weight" then -+ assert.same({ min = 99, max = 101 }, encodedQuery.stats[1].value) -+ else -+ assert.same(group, encodedQuery.stats[1]) -+ end -+ assert.same({ input = "ExampleAccount" }, encodedQuery.filters.trade_filters.filters.account) -+ end) -+ end -+ end) -+ - it("fits the OAuth clipboard status inside the login button", function() - local status = mock_tradeQuery:FormatOAuthLoginStatus(60) - diff --git a/src/Classes/CompareBuySimilar.lua b/src/Classes/CompareBuySimilar.lua index 8e6a34ebadc..8c3f5b1af93 100644 --- a/src/Classes/CompareBuySimilar.lua +++ b/src/Classes/CompareBuySimilar.lua @@ -78,8 +78,8 @@ local function buildURL(item, slotName, controls, modEntries, defenceEntries, is -- timeless jewel ids or other numbers appended to the item name. tradeName = tradeName:gsub("%s+$", "") tradeName = tradeName:match("^(.-)%s+%d+$") or tradeName - queryTable.query.name = tradeName - queryTable.query.type = item.baseName + queryTable.name = tradeName + queryTable.type = item.baseName -- If item is Foulborn, add the foulborn_item filter if item.foulborn then queryFilters.misc_filters = queryFilters.misc_filters or { filters = {} } diff --git a/src/Classes/CompareBuySimilar.lua.rej b/src/Classes/CompareBuySimilar.lua.rej deleted file mode 100644 index 2e144e7207d..00000000000 --- a/src/Classes/CompareBuySimilar.lua.rej +++ /dev/null @@ -1,12 +0,0 @@ -diff a/src/Classes/CompareBuySimilar.lua b/src/Classes/CompareBuySimilar.lua (rejected hunks) -@@ -75,8 +72,8 @@ local function buildURL(item, slotName, controls, modEntries, defenceEntries, is - -- Search by unique name - -- Strip "Foulborn" prefix from unique name for trade search - local tradeName = (item.title or item.name):gsub("^Foulborn%s+", "") -- queryTable.query.name = tradeName -- queryTable.query.type = item.baseName -+ queryTable.name = tradeName -+ queryTable.type = item.baseName - -- If item is Foulborn, add the foulborn_item filter - if item.foulborn then - queryFilters.misc_filters = queryFilters.misc_filters or { filters = {} } diff --git a/src/Classes/TradeQuery.lua b/src/Classes/TradeQuery.lua index 2a12bfc33bd..1d0efbbeb80 100644 --- a/src/Classes/TradeQuery.lua +++ b/src/Classes/TradeQuery.lua @@ -7,6 +7,7 @@ local dkjson = require "dkjson" local itemSlotHelper = require("Modules.ItemSlotHelper") +local tradeHelpers = require("Classes.TradeHelpers") local get_time = os.time local t_insert = table.insert @@ -1062,14 +1063,19 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro end if main.api.authToken == nil then local url = self.tradeQueryRequests:buildUrl(self.hostName .. "trade/search", self.pbRealm, self.pbLeague) - url = url .. "?q=" .. urlEncode(query) + url = url .. "/" .. tradeHelpers.B64GzipEncode(dkjson.encode(query)) controls["uri"..context.row_idx]:SetText(url, true) return end context.controls["priceButton"..context.row_idx].label = "Searching..." - self.lastQueries[row_idx] = query + -- Trade URLs contain only the query; the API also needs the sort order. + query = dkjson.encode({ + query = query, + sort = { ["statgroup.0"] = "desc" }, + engine = "new", + }) self.tradeQueryRequests:SearchWithQueryWeightAdjusted(self.pbRealm, self.pbLeague, query, - function(items, errMsg) + function(items, errMsg, finalQuery) if errMsg then self:SetNotice(context.controls.pbNotice, colorCodes.NEGATIVE .. errMsg) context.controls["priceButton"..context.row_idx].label = "Price Item" @@ -1078,6 +1084,7 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro self:SetNotice(context.controls.pbNotice, "") end + self.lastQueries[row_idx] = finalQuery local selectedSlot = getSelectedSlot() local itemsSafe = self:FilterToSafeItems(items, selectedSlot and selectedSlot.slotName) -- replace eldritch mods or enchants if the user requested @@ -1312,9 +1319,8 @@ you can add them, copy the link here, and press "Price Item" to evaluate the ite exactQuery.query.filters.trade_filters.filters = exactQuery.query.filters.trade_filters.filters or { } exactQuery.query.filters.trade_filters.filters.account = { input = itemResult.trader } - local exactQueryStr = dkjson.encode(exactQuery) - - local encodedUrl = s_format("https://www.pathofexile.com/trade/search/%s?q=%s", self.pbLeague, urlEncode(exactQueryStr)) + local encodedUrl = self.tradeQueryRequests:buildUrl(self.hostName .. "trade/search", self.pbRealm, self.pbLeague, + tradeHelpers.B64GzipEncode(dkjson.encode(exactQuery.query))) Copy(encodedUrl) OpenURL(encodedUrl) diff --git a/src/Classes/TradeQuery.lua.rej b/src/Classes/TradeQuery.lua.rej deleted file mode 100644 index 3a76c43a70f..00000000000 --- a/src/Classes/TradeQuery.lua.rej +++ /dev/null @@ -1,13 +0,0 @@ -diff a/src/Classes/TradeQuery.lua b/src/Classes/TradeQuery.lua (rejected hunks) -@@ -1391,9 +1393,9 @@ you can add them, copy the link here, and press "Price Item" to evaluate the ite - exactQuery.query.filters.trade_filters.filters = exactQuery.query.filters.trade_filters.filters or { } - exactQuery.query.filters.trade_filters.filters.account = { input = itemResult.trader } - -- local exactQueryStr = dkjson.encode(exactQuery.query) -+ local exactQueryStr = dkjson.encode(exactQuery.query) - -- local encodedUrl = s_format("https://www.pathofexile.com/trade2/search/%s/%s", self.pbLeague, tradeHelpers.B64GzipEncode(exactQueryStr)) -+ local encodedUrl = s_format("https://www.pathofexile.com/trade2/search/%s/%s", self.pbLeague, tradeHelpers.B64GzipEncode(exactQueryStr)) - - Copy(encodedUrl) - OpenURL(encodedUrl) diff --git a/src/Classes/TradeQueryGenerator.lua b/src/Classes/TradeQueryGenerator.lua index 0214e854cc4..097a2e7f948 100644 --- a/src/Classes/TradeQueryGenerator.lua +++ b/src/Classes/TradeQueryGenerator.lua @@ -1091,12 +1091,13 @@ function TradeQueryGeneratorClass:FinishQuery() if options.account then complexityBudget = complexityBudget - 3 - queryTable.query.filters.trade_filters.filters.account = { input = options.account } + queryTable.filters.trade_filters = queryTable.filters.trade_filters or { filters = {} } + queryTable.filters.trade_filters.filters.account = { input = options.account } end if options.maxLevel and options.maxLevel > 0 then complexityBudget = complexityBudget - 3 - queryTable.query.filters.req_filters = { + queryTable.filters.req_filters = { disabled = false, filters = { lvl = { @@ -1108,7 +1109,7 @@ function TradeQueryGeneratorClass:FinishQuery() if options.sockets and options.sockets > 0 then complexityBudget = complexityBudget - 3 - queryTable.query.filters.socket_filters = { + queryTable.filters.socket_filters = { disabled = false, filters = { sockets = { @@ -1121,8 +1122,8 @@ function TradeQueryGeneratorClass:FinishQuery() if options.links and options.links > 0 then complexityBudget = complexityBudget - 3 - if not queryTable.query.filters.socket_filters then - queryTable.query.filters.socket_filters = { + if not queryTable.filters.socket_filters then + queryTable.filters.socket_filters = { disabled = false, filters = { links = { @@ -1132,7 +1133,7 @@ function TradeQueryGeneratorClass:FinishQuery() } } else -- do not overwrite options.sockets - queryTable.query.filters.socket_filters.filters["links"] = { + queryTable.filters.socket_filters.filters["links"] = { max = options.links, min = options.links } diff --git a/src/Classes/TradeQueryGenerator.lua.rej b/src/Classes/TradeQueryGenerator.lua.rej deleted file mode 100644 index 9b3ab54aff7..00000000000 --- a/src/Classes/TradeQueryGenerator.lua.rej +++ /dev/null @@ -1,24 +0,0 @@ -diff a/src/Classes/TradeQueryGenerator.lua b/src/Classes/TradeQueryGenerator.lua (rejected hunks) -@@ -1093,11 +1089,11 @@ function TradeQueryGeneratorClass:FinishQuery() - - if options.account then - complexityBudget = complexityBudget - 3 -- queryTable.query.filters.trade_filters.filters.account = { input = options.account } -+ queryTable.filters.trade_filters.filters.account = { input = options.account } - end - if options.maxLevel and options.maxLevel > 0 then - complexityBudget = complexityBudget - 3 -- queryTable.query.filters.req_filters = { -+ queryTable.filters.req_filters = { - disabled = false, - filters = { - lvl = { -@@ -1109,7 +1105,7 @@ function TradeQueryGeneratorClass:FinishQuery() - - if options.sockets and options.sockets > 0 then - complexityBudget = complexityBudget - 3 -- queryTable.query.filters.equipment_filters = { -+ queryTable.filters.equipment_filters = { - disabled = false, - filters = { - rune_sockets = { diff --git a/src/Classes/TradeQueryRequests.lua b/src/Classes/TradeQueryRequests.lua index 301630763da..0c93599196e 100644 --- a/src/Classes/TradeQueryRequests.lua +++ b/src/Classes/TradeQueryRequests.lua @@ -6,6 +6,7 @@ local dkjson = require "dkjson" local utils = require("Modules.Utils") +local tradeHelpers = require("Classes.TradeHelpers") local tradeInfluenceApiKeys = { shaper = "shaper", @@ -413,6 +414,9 @@ end ---@param callback fun(items:table, errMsg:string, query: string?) function TradeQueryRequestsClass:SearchWithURL(url, callback) local subpath = url:match(self.hostNamePattern .. "trade/search/(.+)$") + if not subpath then + return callback(nil, "Invalid URL") + end local paths = {} for path in subpath:gmatch("[^/]+") do table.insert(paths, path) @@ -424,54 +428,42 @@ function TradeQueryRequestsClass:SearchWithURL(url, callback) if #paths == 3 then realm = paths[1] end - league = paths[#paths-1] + league = urlDecode(paths[#paths-1]) queryId = paths[#paths] - self:FetchSearchQuery(realm, league, queryId, function(query, errMsg) - if errMsg then - return callback(nil, errMsg, nil) - end - - -- update sorting on provided url to sort by weights. - local json_data = dkjson.decode(query) - if not json_data or json_data.error then - errMsg = json_data and json_data.error or "Failed to parse search query JSON" - return callback(nil, errMsg, nil) + local json = tradeHelpers.B64GzipDecode(queryId) + if not json then + return callback(nil, "URL is malformed") + end + local decodedQuery = dkjson.decode(json) + if type(decodedQuery) ~= "table" or type(decodedQuery.stats) ~= "table" or type(decodedQuery.stats[1]) ~= "table" then + return callback(nil, "URL is malformed") + end + local weightGroupIndex + for i, group in ipairs(decodedQuery.stats) do + if type(group) ~= "table" or type(group.type) ~= "string" then + return callback(nil, "URL is malformed") end - if json_data.query.stats and json_data.query.stats[1] and json_data.query.stats[1].type == "weight" then - json_data.sort = {} - json_data.sort["statgroup.0"] = "desc" - else - json_data.sort = { price = "asc"} + if group.type == "weight" and not weightGroupIndex then + weightGroupIndex = i end - query = dkjson.encode(json_data) - - self:SearchWithQuery(realm, league, query, function(items, searchErrMsg) - callback(items, searchErrMsg, query) - end) + end + local newQuery = { + query = decodedQuery, + sort = { price = "asc" }, + engine = "new", + } + if weightGroupIndex then + -- Purchase links expect the weighted group first. + decodedQuery.stats[1], decodedQuery.stats[weightGroupIndex] = decodedQuery.stats[weightGroupIndex], decodedQuery.stats[1] + newQuery.sort = { ["statgroup.0"] = "desc" } + end + -- Pasted searches contain user constraints; only generated searches may adjust weights. + local query = dkjson.encode(newQuery) + self:SearchWithQuery(realm, league, query, function(items, errMsg) + callback(items, errMsg, query) end) end ----Fetch query data needed to perform the search ----@param queryId string ----@param league string ----@param callback fun(query:string, errMsg:string) -function TradeQueryRequestsClass:FetchSearchQuery(realm, league, queryId, callback) - local url = self:buildUrl(self.hostName .. "api/trade/search", realm, league, queryId) - table.insert(self.requestQueue["search"], { - url = url, - callback = function(response, errMsg) - if errMsg then - return callback(nil, errMsg) - end - local json_data = dkjson.decode(response) - if not json_data or json_data.error then - errMsg = json_data and json_data.error or "Failed to get search query" - end - callback(response, errMsg) - end - }) -end - --- Fetches the list of all available leagues using trade league API ---@param realm string ---@param callback fun(query:table, errMsg:string) diff --git a/src/Classes/TradeQueryRequests.lua.rej b/src/Classes/TradeQueryRequests.lua.rej deleted file mode 100644 index bc4b2500c27..00000000000 --- a/src/Classes/TradeQueryRequests.lua.rej +++ /dev/null @@ -1,58 +0,0 @@ -diff a/src/Classes/TradeQueryRequests.lua b/src/Classes/TradeQueryRequests.lua (rejected hunks) -@@ -467,6 +467,9 @@ end - ---@param callback fun(items:table, errMsg:string, query: string?) - function TradeQueryRequestsClass:SearchWithURL(url, callback) - local subpath = url:match(self.hostName .. "trade2/search/(.+)$") -+ if not subpath then -+ return callback(nil, "Invalid URL") -+ end - local paths = {} - for path in subpath:gmatch("[^/]+") do - table.insert(paths, path) -@@ -485,27 +488,32 @@ function TradeQueryRequestsClass:SearchWithURL(url, callback) - return callback(nil, "URL is malformed") - end - local queryIdDecoded = dkjson.decode(json) -- if not queryIdDecoded or type(queryIdDecoded.stats) ~= "table" then -+ if type(queryIdDecoded) ~= "table" or type(queryIdDecoded.stats) ~= "table" or type(queryIdDecoded.stats[1]) ~= "table" then - return callback(nil, "URL is malformed") - end -- -- the trader assumes that the first stat group will be a weight group -- if queryIdDecoded.stats[1].type ~= "weight" then -- for i, group in ipairs(queryIdDecoded.stats) do -- -- swap a weight group to be the first group if it exists -- if group.type == "weight" then -- queryIdDecoded.stats[1], queryIdDecoded.stats[i] = queryIdDecoded.stats[i], queryIdDecoded.stats[1] -- break -- end -+ local weightGroupIndex -+ for i, group in ipairs(queryIdDecoded.stats) do -+ if type(group) ~= "table" or type(group.type) ~= "string" then -+ return callback(nil, "URL is malformed") -+ end -+ if group.type == "weight" and not weightGroupIndex then -+ weightGroupIndex = i - end -- end -- if queryIdDecoded.stats[1].type ~= "weight" then -- return callback(nil, "Trade search URL is not a weight search") - end - local newQuery = { - query = queryIdDecoded, -- sort = { ["statgroup.0"] = "desc" }, -+ sort = { price = "asc" }, - } -- self:SearchWithQueryWeightAdjusted(realm, league, dkjson.encode(newQuery), callback) -+ if weightGroupIndex then -+ -- Purchase links expect the weighted group first. -+ queryIdDecoded.stats[1], queryIdDecoded.stats[weightGroupIndex] = queryIdDecoded.stats[weightGroupIndex], queryIdDecoded.stats[1] -+ newQuery.sort = { ["statgroup.0"] = "desc" } -+ end -+ -- Pasted searches contain user constraints; only generated searches may adjust weights. -+ local query = dkjson.encode(newQuery) -+ self:SearchWithQuery(realm, league, query, function(items, errMsg) -+ callback(items, errMsg, query) -+ end) - end - - --- Fetches the list of all available leagues using trade2 league API diff --git a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua index 842b3b4e09a..0dcb91e3933 100644 --- a/src/Classes/TreeTab.lua +++ b/src/Classes/TreeTab.lua @@ -15,8 +15,7 @@ local m_min = math.min local m_floor = math.floor local m_abs = math.abs local s_format = string.format -local s_gsub = string.gsub -local s_byte = string.byte +local tradeHelpers = require("Classes.TradeHelpers") local dkjson = require "dkjson" ---@class TreeTab: ControlHost @@ -2282,22 +2281,17 @@ function TreeTabClass:FindTimelessJewel() "any" } local search = { - query = { - status = { - option = tradeTypes[self.tradeTypeIndex] - }, - stats = { - { - filters = seedTrades, - type = "count", - value = { - min = 1 - } + status = { + option = tradeTypes[self.tradeTypeIndex] + }, + stats = { + { + filters = seedTrades, + type = "count", + value = { + min = 1 } } - }, - sort = { - price = "asc" } } @@ -2310,22 +2304,16 @@ function TreeTabClass:FindTimelessJewel() t_insert(devotionFilters, { id = data.timelessJewelTradeIDs[timelessData.jewelType.id].devotion[timelessData.sharedResults.devotionVariant2.id - 1] }) end if next(devotionFilters) then - t_insert(search.query.stats, { + t_insert(search.stats, { filters = devotionFilters, type = "and" }) end end - -- if the league was not selected via dropdown, then default to the first league in the dropdown or "" if the leagues could not be read - local selectedRealm = controls.realmSelection:GetSelValue():lower() - - local realmPath = selectedRealm == "pc" and "" or (selectedRealm .. "/") - local url = "https://www.pathofexile.com/trade/search/" .. realmPath .. - (controls.searchTradeLeagueSelect:GetSelValue()) .. - "/?q=" .. (s_gsub(dkjson.encode(search), "[^a-zA-Z0-9]", function(a) - return s_format("%%%02X", s_byte(a)) - end)) + local url = self.tradeQueryRequests:buildUrl("https://www.pathofexile.com/trade/search", + controls.realmSelection:GetSelValue():lower(), controls.searchTradeLeagueSelect:GetSelValue(), + tradeHelpers.B64GzipEncode(dkjson.encode(search))) OpenURL(url) Copy(url) diff --git a/src/Classes/TreeTab.lua.rej b/src/Classes/TreeTab.lua.rej deleted file mode 100644 index 2efdcfeae28..00000000000 --- a/src/Classes/TreeTab.lua.rej +++ /dev/null @@ -1,52 +0,0 @@ -diff a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua (rejected hunks) -@@ -18,6 +18,7 @@ local m_abs = math.abs - local s_format = string.format - local s_gsub = string.gsub - local s_byte = string.byte -+local tradeHelpers = require("Classes.TradeHelpers") - local dkjson = require "dkjson" - - -- Helper function to find toast index by content pattern -@@ -2012,22 +2013,17 @@ function TreeTabClass:FindTimelessJewel() - end - - local search = { -- query = { -- status = { -- option = "available" -- }, -- stats = { -- { -- filters = seedTrades, -- type = "count", -- value = { -- min = 1 -- } -+ status = { -+ option = "available" -+ }, -+ stats = { -+ { -+ filters = seedTrades, -+ type = "count", -+ value = { -+ min = 1 - } - } -- }, -- sort = { -- price = "asc" - } - } - -@@ -2050,9 +2046,7 @@ function TreeTabClass:FindTimelessJewel() - -- if the league was not selected via dropdown, then default to the first league in the dropdown or "" if the leagues could not be read - self.timelessJewelLeagueSelect = self.timelessJewelLeagueSelect or (self.tradeLeaguesList and #self.tradeLeaguesList > 0 and self.tradeLeaguesList[1]) or "" - -- Copy("https://www.pathofexile.com/trade/search/"..(self.timelessJewelLeagueSelect).."/?q=" .. (s_gsub(dkjson.encode(search), "[^a-zA-Z0-9]", function(a) -- return s_format("%%%02X", s_byte(a)) -- end))) -+ Copy("https://www.pathofexile.com/trade/search/" .. (self.timelessJewelLeagueSelect) .. "/" .. tradeHelpers.B64GzipEncode(dkjson.encode(search))) - - controls.searchTradeButton.label = "Copy Next Trade URL" - end)