bdoap 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,414 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'httparty'
4
+ require 'json'
5
+
6
+ require_relative '../utils/constants'
7
+ require_relative '../utils/price_calculator'
8
+ require_relative '../utils/recipe_logger'
9
+ require_relative '../utils/npc_item_index'
10
+ require_relative './category_search_options'
11
+
12
+ # search for information on recipes in given categories
13
+ class MarketSearcher
14
+ include Utils
15
+ include MarketSearchTools
16
+
17
+ def initialize(region, cli)
18
+ @root_url = ENVData.get_root_url region
19
+ @region_subdomain = CLIConstants::REGION_DOMAINS[region.to_sym].split('.')[1..].join('.')
20
+ @market_list_url = "#{@root_url}#{ENVData::WORLD_MARKET_LIST}"
21
+ @market_search_url = "#{@root_url}#{ENVData::MARKET_SEARCH_LIST}"
22
+ @market_sub_url = "#{@root_url}#{ENVData::MARKET_SUB_LIST}"
23
+ @market_sell_buy_url = "#{@root_url}#{ENVData::MARKET_SELL_BUY_INFO}"
24
+ @cli = cli
25
+ @ingredient_cache = {}
26
+ end
27
+
28
+ def get_alchemy_market_data(category)
29
+ construct_item_data(category, category == 'all')
30
+ end
31
+
32
+ def get_price_data(elem)
33
+ data = HTTParty.post(
34
+ URI(@market_sub_url),
35
+ headers: ENVData.get_central_market_headers(ENVData.get_incap_cookie(@region_subdomain)),
36
+ body: "#{ENVData::RVT}&mainKey=#{elem['mainKey']}&usingCleint=0",
37
+ content_type: 'application/x-www-form-urlencoded'
38
+ )
39
+
40
+ sleep 1
41
+
42
+ if data&.dig('detailList')
43
+ # data_to_use = { **elem, **data['detailList'][0] }
44
+ # cache_data = {}
45
+ # cache_data[elem['mainKey']] = data_to_use
46
+ # @market_cache.write cache_data
47
+ { **elem, **data['detailList'][0] }
48
+ else
49
+ puts 'incapsula might have your number, ip swap'
50
+ end
51
+ end
52
+
53
+ def construct_item_data(subcategory, all_subcategories)
54
+ aggregate = aggregate_category_data(@market_list_url, @market_search_url, subcategory, all_subcategories)
55
+
56
+ filtered_aggregate = aggregate.filter do |elem|
57
+ elem != nil
58
+ end
59
+
60
+ # TODO: this is probably not a smart way to do this type of retry logic
61
+ puts
62
+ mapped_aggregate = filtered_aggregate.map.with_index do |elem, index|
63
+
64
+ @cli.vipiko_overwrite "(#{index + 1} / #{filtered_aggregate.length}) researching #{@cli.yellow elem['name'].downcase}... (category: #{subcategory})"
65
+
66
+ begin
67
+ get_price_data elem
68
+ rescue
69
+ sleep 2
70
+ begin
71
+ get_price_data elem
72
+ rescue StandardError => error
73
+ puts @cli.red("this could be a network failure. construct_item_data broke.")
74
+
75
+ File.open(ENVData::ERROR_LOG, 'a+') do |file|
76
+ file.write(error.full_message)
77
+ file.write("\n\r")
78
+ end
79
+
80
+ []
81
+ end
82
+ end
83
+ end.filter { |item| !item&.nil? && !item&.dig('pricePerOne').nil? }
84
+
85
+ puts "\n\n" unless mapped_aggregate.empty?
86
+
87
+ mapped_aggregate.sort do |a, b|
88
+ b['pricePerOne'] - a['pricePerOne']
89
+ end
90
+ end
91
+
92
+ def aggregate_category_data(url, search_url, subcategory, all_subcategories)
93
+ make_match_options = proc do |subcat_to_match|
94
+ {
95
+ subcat_to_match: subcat_to_match,
96
+ subcategory: subcategory,
97
+ all_subcategories: all_subcategories
98
+ }
99
+ end
100
+
101
+ aggregate_response = []
102
+
103
+ category_search_options(url, search_url).each do |category_opts|
104
+ do_if_category_matches(make_match_options.call(category_opts[:name])) do
105
+ begin
106
+ data = HTTParty.post(
107
+ URI(category_opts[:url]),
108
+ headers: ENVData.get_central_market_headers(ENVData.get_incap_cookie(@region_subdomain)),
109
+ body: category_opts[:query_string],
110
+ content_type: 'application/x-www-form-urlencoded'
111
+ )
112
+
113
+ sleep 1
114
+
115
+ aggregate_response.push category_opts[:update].call(data) if data
116
+ rescue StandardError => error
117
+ puts @cli.red("this could be a network failure. aggregate_category_data broke.")
118
+
119
+ File.open(ENVData::ERROR_LOG, 'a+') do |file|
120
+ file.write(error.full_message)
121
+ file.write("\n\r")
122
+ end
123
+
124
+ []
125
+ end
126
+ end
127
+ end
128
+
129
+ aggregate_response.flatten
130
+ end
131
+
132
+ def get_item_price_info(ingredient_id, is_recipe_ingredient = true)
133
+ npc_item = NPCItemIndex.get_item(ingredient_id)
134
+
135
+ return npc_item if npc_item
136
+
137
+ if is_recipe_ingredient
138
+ ingredient_data = {}
139
+
140
+ begin
141
+ ingredient_data = HTTParty.post(
142
+ URI(@market_sub_url),
143
+ headers: ENVData.get_central_market_headers(ENVData.get_incap_cookie(@region_subdomain)),
144
+ body: "#{ENVData::RVT}&mainKey=#{ingredient_id}&usingCleint=0",
145
+ content_type: 'application/x-www-form-urlencoded'
146
+ )
147
+
148
+ sleep rand(1..3)
149
+ rescue StandardError => error
150
+ puts @cli.red("this could be a network failure. get_item_price_info broke.")
151
+
152
+ File.open(ENVData::ERROR_LOG, 'a+') do |file|
153
+ file.write(error.full_message)
154
+ file.write("\n\r")
155
+ ingredient_data = {}
156
+ end
157
+ end
158
+
159
+ ingredient_data = {} if ingredient_data.include? 'use a different browser'
160
+
161
+ if ingredient_data.dig('detailList')
162
+ resolved_data = ingredient_data['detailList'][0]
163
+ unless resolved_data.nil?
164
+ body_string = "#{ENVData::RVT}&mainKey=#{resolved_data['mainKey']}&subKey=0&chooseKey=0&isUp=true&keyType=0&name=#{URI.encode_www_form_component(resolved_data['name'])}"
165
+
166
+ detailed_price_list = {}
167
+
168
+ begin
169
+ detailed_price_list = HTTParty.post(
170
+ URI(@market_sell_buy_url),
171
+ headers: ENVData.get_central_market_headers(ENVData.get_incap_cookie(@region_subdomain)),
172
+ body: body_string,
173
+ content_type: 'application/x-www-form-urlencoded'
174
+ )
175
+ sleep rand
176
+ rescue StandardError => error
177
+ puts @cli.red("this could be a network failure. get_item_price_info broke.")
178
+
179
+ File.open(ENVData::ERROR_LOG, 'a+') do |file|
180
+ file.write(error&.full_message || error)
181
+ file.write("\n\r")
182
+ detailed_price_list = {}
183
+ end
184
+ end
185
+
186
+ optimal_prices = detailed_price_list&.dig('marketConditionList')&.sort do |a, b|
187
+ b['sellCount'] - a['sellCount']
188
+ end
189
+
190
+ total_stock = optimal_prices.to_a.map { |price| price["sellCount"] }.sum
191
+
192
+ optimal_price = optimal_prices.to_a.first
193
+
194
+ if optimal_price
195
+ if optimal_price['pricePerOne'] && optimal_price['sellCount']
196
+ { **resolved_data, count: total_stock, pricePerOne: optimal_price['pricePerOne'] }
197
+ else
198
+ { **resolved_data, count: total_stock, pricePerOne: resolved_data['pricePerOne'] }
199
+ end
200
+ end
201
+ end
202
+ end
203
+
204
+ end
205
+ end
206
+
207
+ def get_all_recipe_prices(item_codex_data, subcategory)
208
+ mapped_recipe_prices = []
209
+ out_of_stock_items = []
210
+ # vipiko is about to start writing carriage returns,
211
+ # so printing newline here
212
+
213
+ item_codex_data.each.with_index do |item_with_recipe, index|
214
+ potential_recipes = []
215
+ name = item_with_recipe[:name].downcase
216
+ recipe_list = item_with_recipe[:recipe_list]
217
+
218
+ @cli.vipiko_overwrite "(#{index + 1} / #{item_codex_data.length}) I'll ask a merchant about the price of ingredients for #{@cli.yellow name}!"
219
+
220
+ recipe_list.each do |(recipe_id, recipe)|
221
+ potential_recipe = []
222
+
223
+ recipe.each do |ingredient|
224
+ ingredient_id = ingredient['id'] ? ingredient['id'] : ingredient[:id]
225
+ quant = ingredient['quant'] ? ingredient['quant'] : ingredient[:quant]
226
+
227
+ if @ingredient_cache[ingredient_id]
228
+ cached_ingredient = { **@ingredient_cache[ingredient_id], quant: quant }
229
+ potential_recipe.push cached_ingredient
230
+ next
231
+ end
232
+
233
+ item_price_info_hash = get_item_price_info ingredient_id, true
234
+
235
+ next if item_price_info_hash.nil?
236
+
237
+ item_price_info = item_price_info_hash.transform_keys { |key|
238
+ key.to_s.gsub(/(.)([A-Z])/,'\1_\2').downcase.to_sym
239
+ }
240
+
241
+ if item_price_info[:count].zero? && rand > 0.5
242
+ out_of_stock_items.push(item_price_info[:name].downcase)
243
+ end
244
+
245
+ stock_count = get_stock_count item_price_info
246
+
247
+ # attach the npc item data (such as infinite stock, etc) to the item
248
+ npc_data = {}
249
+ npc_data = item_price_info if item_price_info[:is_npc_item]
250
+ price_per_one = npc_data[:price].to_i.zero? ? item_price_info[:price_per_one].to_i : npc_data[:price].to_i
251
+
252
+ # TODO: this is ridiculous, dedupe the hash values
253
+ potential_ingredient_hash = {
254
+ name: item_price_info[:name],
255
+ price: price_per_one,
256
+ id: item_price_info[:main_key],
257
+ total_trade_count: item_price_info[:total_trade_count].to_i,
258
+ total_in_stock: stock_count,
259
+ main_category: item_price_info[:main_category],
260
+ sub_category: item_price_info[:sub_category],
261
+ quant: quant,
262
+ price_per_one: price_per_one,
263
+ count: stock_count,
264
+ for_recipe_id: recipe_id,
265
+ **npc_data
266
+ }
267
+
268
+ @ingredient_cache[ingredient_id] = potential_ingredient_hash
269
+
270
+ potential_recipe.push potential_ingredient_hash
271
+ end
272
+
273
+ next unless potential_recipe.length == recipe.length
274
+
275
+ potential_recipes.push potential_recipe
276
+ end
277
+
278
+ mapped_recipe_prices.push map_recipe_prices(potential_recipes, item_with_recipe, subcategory)
279
+ end
280
+
281
+ mapped_recipe_prices.filter { |e| !e.nil? && !!e }
282
+ end
283
+
284
+ def map_recipe_prices(potential_recipes, item, category)
285
+ average_procs = 1
286
+
287
+ if [25, 35].include? item[:main_category]
288
+ unless /oil of|draught|\[mix\]|\[party\]|immortal:|perfume|indignation/im.match item[:name].downcase
289
+ average_procs = 2.5
290
+ end
291
+
292
+ if category == 'reagent' || /reagent/.match(item[:name].downcase)
293
+ average_procs = 3
294
+ end
295
+
296
+ # harmony draught recipe produces 10
297
+ if item[:id].to_s == '1399'
298
+ average_procs = 10
299
+ end
300
+ end
301
+
302
+ filtered_recipes = potential_recipes.filter do |recipe|
303
+ recipe.all? do |ingredient|
304
+ if ingredient[:total_in_stock] == Float::INFINITY
305
+ true
306
+ else
307
+ stock = ingredient[:total_in_stock].to_i.zero? ? ingredient[:count] : ingredient[:total_in_stock]
308
+
309
+ stock == Float::INFINITY ? true : stock.to_i > 0
310
+ end
311
+ end
312
+ end
313
+
314
+ selected_recipe = filtered_recipes.sort_by do |recipe|
315
+ recipe.map { |ingredient| mapper ingredient }.sum
316
+ end[0]
317
+
318
+ return nil if selected_recipe.nil?
319
+
320
+ average_procs = 1 if selected_recipe.find { |a| a[:name].downcase == 'blue reagent' } != nil
321
+
322
+ # remove recipes where one ingredient is used twice
323
+ # this usually happens because of incorrect substitution being
324
+ # found from bdocodex. or maybe they're not incorrect, and there
325
+ # are some seriously mysterious alchemy recipes out there...
326
+ ingredients_already_appeared = []
327
+ filtered_selected_recipe = selected_recipe.filter do |ingredient|
328
+ # set procs to 1 if blue reagent required
329
+ average_procs = 1 if ingredient[:name].downcase == 'blue reagent'
330
+ return false if ingredients_already_appeared.include? ingredient[:name]
331
+ ingredients_already_appeared.push(ingredient[:name])
332
+ true
333
+ end
334
+
335
+ return nil if filtered_selected_recipe.length != selected_recipe.length
336
+
337
+ total_ingredient_cost = (selected_recipe.map { |ing| ing[:price] * ing[:quant] }.sum / average_procs).floor
338
+ total_ingredient_stock = selected_recipe.filter { |ing| !ing[:is_npc_item] }.map { |ing| ing[:total_in_stock] }.sum
339
+ any_ingredient_out = selected_recipe.any? { |ing| ing[:total_in_stock].zero? || ing[:total_in_stock] < ing[:quant] }
340
+
341
+ body_string = "#{ENVData::RVT}&mainKey=#{item[:id]}&subKey=0&chooseKey=0&isUp=true&keyType=0&name=#{URI.encode_www_form_component(item[:name])}"
342
+
343
+ item_price_data = HTTParty.post(
344
+ URI(@market_sell_buy_url),
345
+ headers: ENVData.get_central_market_headers(ENVData.get_incap_cookie(@region_subdomain)),
346
+ body: body_string,
347
+ content_type: 'application/x-www-form-urlencoded'
348
+ )
349
+
350
+ # assuming we were able to find the item price list
351
+ if item_price_data&.dig('marketConditionList')
352
+ item_market_sell_price = item_price_data['marketConditionList']&.last&.dig('pricePerOne').to_i
353
+ raw_profit_with_procs = (item_market_sell_price - total_ingredient_cost) * average_procs
354
+ raw_profit_before_procs = item_market_sell_price - total_ingredient_cost
355
+
356
+ # TODO: allow the user to configure if the tool should show them. this would require some alteration
357
+ # to the output logger formatting
358
+
359
+ # skip out of stock / unprofitable recipes
360
+ return nil if raw_profit_before_procs.zero?
361
+ return nil if any_ingredient_out
362
+ return nil if total_ingredient_stock < 10
363
+
364
+ max_potion_count = selected_recipe.map { |ing| ing[:total_in_stock] == Float::INFINITY ? Float::INFINITY : (ing[:total_in_stock] / ing[:quant]).floor }.min
365
+
366
+ # important - the total cost of making the maximum possible
367
+ # amount of this recipe
368
+ # @type [Integer]
369
+ total_max_ingredient_cost = total_ingredient_cost * max_potion_count
370
+
371
+ # important - the untaxed sell price of the maximum amount of this
372
+ # recipe
373
+ # @type [Integer]
374
+ raw_max_market_sell_price = (max_potion_count * item_market_sell_price) * average_procs
375
+
376
+ # taxed profit on selling one of this this recipe, with
377
+ # average procs accounted for
378
+ # @type [Integer]
379
+ taxed_sell_profit_after_procs = (((PriceCalculator.calculate_taxed_price(item_market_sell_price) - total_ingredient_cost)) * average_procs).floor
380
+
381
+ # taxed profit on selling max amount of this this recipe, with
382
+ # average procs accounted for
383
+ # @type [Integer]
384
+ #
385
+ max_taxed_sell_profit_after_procs = (PriceCalculator.calculate_taxed_price(item_market_sell_price * max_potion_count) - total_max_ingredient_cost) * average_procs
386
+
387
+ return nil if max_taxed_sell_profit_after_procs.to_s.downcase == 'nan'
388
+ return nil if max_taxed_sell_profit_after_procs < 0
389
+
390
+ recipe_logger = RecipeLogger.new @cli
391
+ results = recipe_logger.log_recipe_data(item, selected_recipe, max_potion_count, item_market_sell_price, total_ingredient_cost, average_procs, total_max_ingredient_cost, raw_max_market_sell_price, max_taxed_sell_profit_after_procs, raw_profit_with_procs, taxed_sell_profit_after_procs)
392
+ { information: results[:recipe_info], max_profit: max_taxed_sell_profit_after_procs, silver_per_hour: results[:silver_per_hour] }
393
+ end
394
+ end
395
+
396
+ def mapper(item)
397
+ item[:price].to_i * item[:quant].to_i
398
+ end
399
+
400
+ def get_stock_count(item_info)
401
+ return Float::INFINITY if item_info.dig(:is_npc_item)
402
+
403
+ item_info[:total_in_stock].to_i.zero? ? item_info[:count].to_i : item_info[:total_in_stock].to_i
404
+ end
405
+
406
+ # async def do_if_category_matches(options, &procedure)
407
+ # procedure.call.wait if options[:subcategory] == options[:subcat_to_match] || options[:all_subcategories]
408
+ # end
409
+
410
+ def do_if_category_matches(options, &procedure)
411
+ procedure.call if options[:all_subcategories]
412
+ procedure.call if options[:subcategory] == options[:subcat_to_match]
413
+ end
414
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Utils
4
+ class ArrayUtils
5
+ def self.make_permutations(list, n = 0, result = [], current = [], limit)
6
+ if n == list.length && n == limit
7
+ result.push current
8
+ else
9
+ list[n].each do |item|
10
+ make_permutations list, n + 1, result, [*current, item], limit
11
+ end
12
+ end
13
+
14
+ result
15
+ end
16
+
17
+ def self.deep_permute(input, limit)
18
+ make_permutations input, limit
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ $request_verification_token = '__RequestVerificationToken=aVYGQPovG8EI6bRIagh8tbHJUhZlM-nH3UKVQaV9R9N0vODzmWcB747BHEsHaphwANvzsaNi5TCPlB-72-e1LadqAlL-bdkDkTqVh4gMnu81' # rubocop:disable Layout/LineLength
6
+
7
+ module Utils
8
+ # constants to use when user is configuring the tool
9
+ class CLIConstants
10
+ CATEGORY_OPTIONS = {
11
+ all: 'collates everything',
12
+ offensive: 'category 35, subcategory 1',
13
+ defensive: 'category 35, subcategory 2',
14
+ functional: 'category 35, subcategory 3',
15
+ potion: 'category 35, subcategory 5',
16
+ other: 'category 35, subcategory 8',
17
+ blood: 'searches "\'s blood"',
18
+ oil: "searches 'oil of'",
19
+ 'alchemy stone': "searches 'stone of'",
20
+ reagent: "searches 'reagent'",
21
+ 'black stone': 'category 30, subcategory 1',
22
+ 'misc': 'category 25, subcategory 8',
23
+ 'other tools': 'category 40, subcategory 10',
24
+ # 'magic crystal': "searches 'magic crystal'",
25
+ exit: 'stops the search'
26
+ }.freeze
27
+
28
+ REGION_DOMAINS = {
29
+ na: 'na-trade.naeu.playblackdesert.com',
30
+ eu: 'eu-trade.naeu.playblackdesert.com',
31
+ eu_console: 'eu-trade.console.playblackdesert.com',
32
+ na_console: 'na-trade.console.playblackdesert.com',
33
+ asia_console: 'asia-trade.console.playblackdesert.com',
34
+ sea: 'trade.sea.playblackdesert.com',
35
+ mena: 'trade.tr.playblackdesert.com',
36
+ kr: 'trade.kr.playblackdesert.com',
37
+ ru: 'trade.ru.playblackdesert.com',
38
+ jp: 'trade.jp.playblackdesert.com',
39
+ th: 'trade.th.playblackdesert.com',
40
+ tw: 'trade.tw.playblackdesert.com',
41
+ sa: 'blackdesert-tradeweb.playredfox.com',
42
+ exit: 'stops the search'
43
+ }.freeze
44
+
45
+ REGION_LANGUAGES = {
46
+ us: 'us english',
47
+ de: 'deutsch',
48
+ fr: 'français',
49
+ ru: 'русский',
50
+ es: 'español (na/eu)',
51
+ sp: 'español (sa)',
52
+ pt: 'português',
53
+ jp: '日本語',
54
+ kr: '한국어',
55
+ cn: '中文',
56
+ tw: '繁体中文',
57
+ th: 'ภาษาไทย',
58
+ tr: 'türkçe',
59
+ id: 'basa indonesia',
60
+ se: 'sea english',
61
+ gl: 'global lab',
62
+ exit: 'stops the search'
63
+ }.freeze
64
+
65
+ AGGRESSION_LEVELS = {
66
+ normal: 'evaluate one permutation of each recipe',
67
+ hyperaggressive: 'evaluate every substitution for every recipe',
68
+ exit: 'stops the search'
69
+ }.freeze
70
+
71
+ def self.set_rvt(rvt)
72
+ $request_verification_token = rvt
73
+ end
74
+ end
75
+
76
+ # constants that will be used in by search / scraping scripts
77
+ class ENVData
78
+ MARKET_CACHE = './market_cache'
79
+ BDO_CODEX_CACHE = './bdo_codex_cache'
80
+ ERROR_LOG = './error.log'
81
+ # note that the following two tokens are different!! the first one is from a request cookie
82
+ # the second is from the dom of the actual BDO central market interface
83
+ COOKIE = '__RequestVerificationToken=aVYGQPovG8EI6bRIagh8tbHJUhZlM-nH3UKVQaV9R9N0vODzmWcB747BHEsHaphwANvzsaNi5TCPlB-72-e1LadqAlL-bdkDkTqVh4gMnu81' # rubocop:disable Layout/LineLength
84
+ RVT = '__RequestVerificationToken=yWbqJmiU4wcp2IRQGkbDfqMFs2fjCYx4UqVxg4umK8CvdbLhweMLZ1es-4SFWD8J1UfoqwbaQyo_YuzAkSzHWY8Wjyx4ttwZBvtu_pd--9U1' # rubocop:disable Layout/LineLength
85
+ WORLD_MARKET_LIST = '/GetWorldMarketList'
86
+ MARKET_SUB_LIST = '/GetWorldMarketSubList'
87
+ MARKET_SEARCH_LIST = '/GetWorldMarketSearchList'
88
+ MARKET_SELL_BUY_INFO = '/GetItemSellBuyInfo'
89
+ MARKET_HOT_LIST = '/GetWorldMarketHotList'
90
+ MARKET_WAIT_LIST = '/GetWorldMarketWaitList'
91
+ REQUEST_OPTS = {
92
+ method: 'post',
93
+ central_market_headers: {
94
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
95
+ 'User-Agent':
96
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
97
+ Cookie: COOKIE,
98
+ Dnt: '1',
99
+ 'x-cdn': 'Imperva'
100
+ },
101
+ bdo_codex_headers: {
102
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
103
+ 'User-Agent':
104
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
105
+ Dnt: '1'
106
+ }
107
+ }.freeze
108
+
109
+ def self.get_central_market_headers(incap_cookie = '')
110
+ original_cookie = REQUEST_OPTS[:central_market_headers][:Cookie]
111
+ if rand > 0.5
112
+ REQUEST_OPTS[:central_market_headers]
113
+ { **REQUEST_OPTS[:central_market_headers], 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36', Cookie: "#{original_cookie};#{incap_cookie}" }
114
+ else
115
+ { **REQUEST_OPTS[:central_market_headers], 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36', Cookie: "#{original_cookie};#{incap_cookie}" }
116
+ end
117
+ end
118
+
119
+ def self.get_incap_cookie(region_domain)
120
+ new_expiry = Date.today + 365
121
+ day = Time.now.strftime("%a")
122
+ month = Time.now.strftime("%b")
123
+ hour = rand(1..12)
124
+ minute = rand(1..60)
125
+ second = rand(1..60)
126
+
127
+ # TODO: EXPERIMENTAL - figure out how to simulate incapsula data
128
+ "visid_incap_2504212=xoYUIj+XR/acq/q6uc0RZyLI/2cAAAAAQUIPAAAAAAAMYr/xXVQYe6Eo4uVK+L6V; expires=#{day}, #{new_expiry.day} #{month} #{new_expiry.year} #{hour}:#{minute}:#{second} GMT; HttpOnly; path=/; Domain=.#{region_domain}; Secure; SameSite=None"
129
+ end
130
+
131
+ def self.get_root_url(region)
132
+ "https://#{CLIConstants::REGION_DOMAINS[region.to_sym]}/Home"
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Utils
4
+ class HashCache
5
+ def initialize(cache_file)
6
+ @cache_file = cache_file
7
+ @all_content = read_all
8
+ end
9
+
10
+ def read_all
11
+ begin
12
+ file_content = File.read(@cache_file)
13
+ return JSON.parse file_content unless file_content.empty?
14
+
15
+ {}
16
+ rescue
17
+ {}
18
+ end
19
+ end
20
+
21
+ def read(key)
22
+ @all_content&.dig(key)
23
+ end
24
+
25
+ def write(data)
26
+ item_map = read_all
27
+ File.open(@cache_file, 'w') do |file|
28
+ new_data = { **item_map, **data }
29
+
30
+ file.write new_data.to_json
31
+ end
32
+ end
33
+ end
34
+ end