From 2ee7cee2d72b90a7ee3ee6f51c54cfbd09fa5368 Mon Sep 17 00:00:00 2001 From: vaisest Date: Tue, 29 Sep 2026 15:52:03 +0000 Subject: [PATCH] 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 709d0c51bbf..daf4225fe1a 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)