rest-gw2 0.4.0 → 0.5.0
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.
- checksums.yaml +5 -5
- data/CHANGES.md +30 -0
- data/README.md +7 -3
- data/Rakefile +1 -0
- data/TODO.md +26 -0
- data/config.ru +6 -0
- data/lib/rest-gw2.rb +1 -0
- data/lib/rest-gw2/client.rb +249 -110
- data/lib/rest-gw2/client/item_detail.rb +80 -0
- data/lib/rest-gw2/server.rb +5 -583
- data/lib/rest-gw2/server/action.rb +283 -0
- data/lib/rest-gw2/server/cache.rb +14 -4
- data/lib/rest-gw2/server/imp.rb +270 -0
- data/lib/rest-gw2/server/runner.rb +1 -0
- data/lib/rest-gw2/server/view.rb +309 -0
- data/lib/rest-gw2/{view → server/view}/characters.erb +10 -6
- data/lib/rest-gw2/server/view/check_list.erb +10 -0
- data/lib/rest-gw2/server/view/check_percentage.erb +9 -0
- data/lib/rest-gw2/server/view/commerce.erb +24 -0
- data/lib/rest-gw2/{view → server/view}/dyes.erb +7 -7
- data/lib/rest-gw2/server/view/error.erb +1 -0
- data/lib/rest-gw2/server/view/exchange.erb +29 -0
- data/lib/rest-gw2/server/view/guild_info.erb +37 -0
- data/lib/rest-gw2/{view → server/view}/index.erb +0 -0
- data/lib/rest-gw2/{view → server/view}/info.erb +1 -1
- data/lib/rest-gw2/{view → server/view}/item_list.erb +0 -0
- data/lib/rest-gw2/{view/items.erb → server/view/item_section.erb} +0 -0
- data/lib/rest-gw2/{view → server/view}/item_show.erb +7 -1
- data/lib/rest-gw2/server/view/items.erb +8 -0
- data/lib/rest-gw2/server/view/items_from.erb +35 -0
- data/lib/rest-gw2/{view → server/view}/layout.erb +14 -4
- data/lib/rest-gw2/server/view/members.erb +23 -0
- data/lib/rest-gw2/server/view/menu.erb +13 -0
- data/lib/rest-gw2/server/view/menu_armors.erb +17 -0
- data/lib/rest-gw2/server/view/menu_commerce.erb +7 -0
- data/lib/rest-gw2/server/view/menu_guild.erb +11 -0
- data/lib/rest-gw2/server/view/menu_unlocks.erb +11 -0
- data/lib/rest-gw2/{view → server/view}/menu_weapons.erb +2 -1
- data/lib/rest-gw2/{view → server/view}/pages.erb +2 -2
- data/lib/rest-gw2/{view → server/view}/profile.erb +7 -7
- data/lib/rest-gw2/server/view/skins.erb +15 -0
- data/lib/rest-gw2/server/view/stash.erb +10 -0
- data/lib/rest-gw2/server/view/titles.erb +5 -0
- data/lib/rest-gw2/server/view/unlock_percentage.erb +1 -0
- data/lib/rest-gw2/server/view/unlocks_items.erb +5 -0
- data/lib/rest-gw2/server/view/unlocks_list.erb +3 -0
- data/lib/rest-gw2/{view → server/view}/wallet.erb +1 -1
- data/lib/rest-gw2/server/view/wip.erb +1 -0
- data/lib/rest-gw2/version.rb +2 -1
- data/rest-gw2.gemspec +43 -25
- data/task/README.md +8 -8
- data/task/gemgem.rb +29 -7
- metadata +42 -25
- data/lib/rest-gw2/view/error.erb +0 -1
- data/lib/rest-gw2/view/guild.erb +0 -14
- data/lib/rest-gw2/view/items_from.erb +0 -30
- data/lib/rest-gw2/view/menu.erb +0 -16
- data/lib/rest-gw2/view/menu_armors.erb +0 -11
- data/lib/rest-gw2/view/skins.erb +0 -14
- data/lib/rest-gw2/view/transactions.erb +0 -28
- data/lib/rest-gw2/view/wip.erb +0 -1
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RestGW2
|
4
|
+
ItemDetailStruct = Struct.new(:client, :raw, :opts)
|
5
|
+
|
6
|
+
class ItemDetail < ItemDetailStruct
|
7
|
+
attr_reader :detail, :upgrades, :stats, :skins
|
8
|
+
|
9
|
+
def populate
|
10
|
+
detail_promises = item_detail_group_by_id(raw)
|
11
|
+
upgrades_promises = extract_items_in_slots('upgrades', 'infusions')
|
12
|
+
stats_promises = [expand_stats_detail]
|
13
|
+
|
14
|
+
@detail = detail_promises_to_map(detail_promises)
|
15
|
+
@upgrades = detail_promises_to_map(upgrades_promises)
|
16
|
+
@stats = detail_promises_to_map(stats_promises)
|
17
|
+
@skins = client.all_skins.flatten.group_by{ |s| s['id'] }
|
18
|
+
end
|
19
|
+
|
20
|
+
def fill item
|
21
|
+
return item unless item
|
22
|
+
|
23
|
+
s = item['skin']
|
24
|
+
u = item['upgrades']
|
25
|
+
f = item['infusions']
|
26
|
+
t = item['stats']
|
27
|
+
|
28
|
+
# Items might not have details, but they could still have upgrades!
|
29
|
+
# Blame ANet for not populating whitelist for existing items.
|
30
|
+
item.merge(detail[item['id']] || {}).merge(
|
31
|
+
'count' => item['count'] || 1,
|
32
|
+
'skin' => s && skins.dig(s, 0),
|
33
|
+
'upgrades' => u && u.flat_map(&upgrades.method(:[])),
|
34
|
+
'infusions' => f && f.flat_map(&upgrades.method(:[])),
|
35
|
+
'stats' => t && stats[t['id']].merge(t))
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# Returns Array[Promise[Array[Detail]]]
|
41
|
+
# https://wiki.guildwars2.com/wiki/API:2/items
|
42
|
+
# https://wiki.guildwars2.com/wiki/API:2/commerce/prices
|
43
|
+
def item_detail_group_by_id items
|
44
|
+
items.map{ |i| i && i['id'] }.compact.each_slice(100).map do |slice|
|
45
|
+
q = {:ids => slice.join(',')}
|
46
|
+
[client.get('v2/items', q),
|
47
|
+
client.get('v2/commerce/prices', q,
|
48
|
+
{:error_detector => false}.merge(opts))]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns Array[Promise[Array[Detail]]]
|
53
|
+
def extract_items_in_slots *slots
|
54
|
+
items = raw.flat_map do |i|
|
55
|
+
if i
|
56
|
+
i.values_at(*slots).flatten.compact.map do |id|
|
57
|
+
{'id' => id}
|
58
|
+
end
|
59
|
+
else
|
60
|
+
[]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
item_detail_group_by_id(items)
|
64
|
+
end
|
65
|
+
|
66
|
+
# https://wiki.guildwars2.com/wiki/API:2/itemstats
|
67
|
+
def expand_stats_detail
|
68
|
+
raw.map{ |i| i && i.dig('stats', 'id') }.
|
69
|
+
compact.uniq.each_slice(100).map do |ids|
|
70
|
+
client.get('v2/itemstats', :ids => ids.join(','))
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# this is probably a dirty way to workaround converting hashes to arrays
|
75
|
+
def detail_promises_to_map promises
|
76
|
+
promises.flat_map(&:itself).map(&:to_a).flatten.group_by{ |i| i['id'] }.
|
77
|
+
inject({}){ |r, (id, v)| r[id] = v.inject(&:merge); r }
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/lib/rest-gw2/server.rb
CHANGED
@@ -1,16 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rest-gw2/server/action'
|
1
4
|
|
2
|
-
require 'rest-gw2/server/cache'
|
3
|
-
require 'rest-gw2/client'
|
4
|
-
require 'mime/types'
|
5
|
-
require 'rest-core'
|
6
5
|
require 'jellyfish'
|
7
6
|
require 'rack'
|
8
7
|
|
9
|
-
require 'timeout'
|
10
|
-
require 'openssl'
|
11
|
-
require 'erb'
|
12
|
-
require 'cgi'
|
13
|
-
|
14
8
|
module RestGW2
|
15
9
|
CONFIG = ENV['RESTGW2_CONFIG'] || File.expand_path("#{__dir__}/../../.env")
|
16
10
|
|
@@ -18,7 +12,7 @@ module RestGW2
|
|
18
12
|
return {} unless File.exist?(path)
|
19
13
|
Hash[File.read(path).strip.squeeze("\n").each_line.map do |line|
|
20
14
|
name, value = line.split('=')
|
21
|
-
[name, value.chomp] if name && value
|
15
|
+
[name, value.chomp] if !line.start_with?('#') && name && value
|
22
16
|
end.compact]
|
23
17
|
end
|
24
18
|
|
@@ -26,574 +20,6 @@ module RestGW2
|
|
26
20
|
ENV[k] ||= v
|
27
21
|
end
|
28
22
|
|
29
|
-
class ServerCore
|
30
|
-
include Jellyfish
|
31
|
-
SECRET = ENV['RESTGW2_SECRET'] || 'RESTGW2_SECRET'*2
|
32
|
-
COINS = %w[gold silver copper].zip(%w[
|
33
|
-
https://wiki.guildwars2.com/images/d/d1/Gold_coin.png
|
34
|
-
https://wiki.guildwars2.com/images/3/3c/Silver_coin.png
|
35
|
-
https://wiki.guildwars2.com/images/e/eb/Copper_coin.png
|
36
|
-
]).freeze
|
37
|
-
|
38
|
-
def self.weapons
|
39
|
-
%w[Greatsword Sword Hammer Mace Axe Dagger
|
40
|
-
Staff Scepter
|
41
|
-
LongBow ShortBow Rifle Pistol
|
42
|
-
Shield Torch Focus Warhorn
|
43
|
-
Harpoon Speargun Trident]
|
44
|
-
end
|
45
|
-
|
46
|
-
def self.armors
|
47
|
-
%w[Helm Shoulders Coat Gloves Leggings Boots HelmAquatic]
|
48
|
-
end
|
49
|
-
|
50
|
-
def self.armors_weight
|
51
|
-
%w[Light Medium Heavy Clothing]
|
52
|
-
end
|
53
|
-
|
54
|
-
def self.crafting
|
55
|
-
%w[Weaponsmith Huntsman Artificer
|
56
|
-
Armorsmith Leatherworker Tailor
|
57
|
-
Jeweler Chef Scribe]
|
58
|
-
end
|
59
|
-
|
60
|
-
HTML = Class.new(Struct.new(:to_s))
|
61
|
-
|
62
|
-
controller_include NormalizedPath, Module.new{
|
63
|
-
# VIEW
|
64
|
-
def render path, arg=nil
|
65
|
-
erb(:layout){ erb(path, arg) }
|
66
|
-
end
|
67
|
-
|
68
|
-
def erb name, arg=nil, &block
|
69
|
-
ERB.new(views(name)).result(binding, &block)
|
70
|
-
end
|
71
|
-
|
72
|
-
def h str
|
73
|
-
case str
|
74
|
-
when String
|
75
|
-
CGI.escape_html(str)
|
76
|
-
when HTML
|
77
|
-
str.to_s
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
def u str
|
82
|
-
CGI.escape(str) if str.kind_of?(String)
|
83
|
-
end
|
84
|
-
|
85
|
-
def path str, q={}
|
86
|
-
RC::Middleware.request_uri(
|
87
|
-
RC::REQUEST_PATH => "#{ENV['RESTGW2_PREFIX']}#{str}",
|
88
|
-
RC::REQUEST_QUERY => q)
|
89
|
-
end
|
90
|
-
|
91
|
-
def views name
|
92
|
-
@views ||= {}
|
93
|
-
@views[name] = File.read("#{__dir__}/view/#{name}.erb")
|
94
|
-
end
|
95
|
-
|
96
|
-
def refresh_path
|
97
|
-
path(request.path, :p => p, :r => '1', :t => t)
|
98
|
-
end
|
99
|
-
|
100
|
-
# TODO: clean me up
|
101
|
-
def menu item, title, query={}
|
102
|
-
href = path(item, query.merge(:t => t))
|
103
|
-
if path(request.path, :p => p, :t => t) == href
|
104
|
-
title
|
105
|
-
else
|
106
|
-
%Q{<a href="#{href}">#{title}</a>}
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
# TODO: clean me up
|
111
|
-
def menu_sub prefix, item, title
|
112
|
-
key = "#{prefix}#{item}"
|
113
|
-
if path(request.path) == path(key)
|
114
|
-
menu(key, title, :p => p)
|
115
|
-
else
|
116
|
-
menu(key, title)
|
117
|
-
end
|
118
|
-
end
|
119
|
-
|
120
|
-
def menu_char name
|
121
|
-
menu("/characters/#{RC::Middleware.escape(name)}", name)
|
122
|
-
end
|
123
|
-
|
124
|
-
def menu_skin item, title
|
125
|
-
menu_sub('/skins', item, title)
|
126
|
-
end
|
127
|
-
|
128
|
-
def menu_trans item, title
|
129
|
-
menu_sub('/transactions', item, title)
|
130
|
-
end
|
131
|
-
|
132
|
-
def page num
|
133
|
-
menu(request.path, num.to_s, :p => zero_is_nil(num))
|
134
|
-
end
|
135
|
-
|
136
|
-
# HELPER
|
137
|
-
def show_guild g
|
138
|
-
HTML.new(menu("/guilds/#{g['guild_id']}",
|
139
|
-
h("#{g['guild_name']} [#{g['tag']}]")))
|
140
|
-
end
|
141
|
-
|
142
|
-
def blank_icon
|
143
|
-
%Q{<img class="icon" src="https://upload.wikimedia.org/wikipedia/commons/d/d2/Blank.png"/>}
|
144
|
-
end
|
145
|
-
|
146
|
-
def item_wiki_list items
|
147
|
-
items.map(&method(:item_wiki)).join("\n")
|
148
|
-
end
|
149
|
-
|
150
|
-
def item_wiki item
|
151
|
-
if item['name'] && item['icon']
|
152
|
-
name = item['name'].tr(' ', '_')
|
153
|
-
missing = if item['count'] == 0 then ' missing' else nil end
|
154
|
-
img = %Q{<img class="icon#{missing}" title="#{item_title(item)}"} +
|
155
|
-
%Q{ src="#{h item['icon']}"/>}
|
156
|
-
%Q{<a href="http://wiki.guildwars2.com/wiki/#{u name}">#{img}</a>}
|
157
|
-
else
|
158
|
-
blank_icon
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
def item_link item
|
163
|
-
name = item_name(item)
|
164
|
-
if item['nolink']
|
165
|
-
name
|
166
|
-
else
|
167
|
-
menu("/items/#{item['id']}", name)
|
168
|
-
end
|
169
|
-
end
|
170
|
-
|
171
|
-
def item_name item
|
172
|
-
h(item['name'] || "?#{item['id']}?")
|
173
|
-
end
|
174
|
-
|
175
|
-
def item_title item
|
176
|
-
d = item['description']
|
177
|
-
d && d.unpack('U*').map{ |c| "&##{c};" }.join
|
178
|
-
end
|
179
|
-
|
180
|
-
def item_count item
|
181
|
-
c = item['count']
|
182
|
-
"(#{c})" if c > 1
|
183
|
-
end
|
184
|
-
|
185
|
-
def item_price item
|
186
|
-
b = item['buys']
|
187
|
-
s = item['sells']
|
188
|
-
bb = b && price(b['unit_price'])
|
189
|
-
ss = s && price(s['unit_price'])
|
190
|
-
%Q{#{bb} / #{ss}} if bb || ss
|
191
|
-
end
|
192
|
-
|
193
|
-
def price copper
|
194
|
-
g = copper / 100_00
|
195
|
-
s = copper % 100_00 / 100
|
196
|
-
c = copper % 100
|
197
|
-
l = [g, s, c]
|
198
|
-
n = l.index(&:nonzero?)
|
199
|
-
return '-' unless n
|
200
|
-
l.zip(COINS).drop(n).map do |(num, (title, src))|
|
201
|
-
%Q{#{num}<img class="price" title="#{h title}" src="#{h src}"/>}
|
202
|
-
end.join(' ')
|
203
|
-
end
|
204
|
-
|
205
|
-
def dye_color dye
|
206
|
-
%w[cloth leather metal].map do |kind|
|
207
|
-
rgb = dye[kind]['rgb']
|
208
|
-
rgb && dye_preview(kind, rgb.join(', '))
|
209
|
-
end.join("\n")
|
210
|
-
end
|
211
|
-
|
212
|
-
def dye_preview kind, rgb
|
213
|
-
%Q{<span class="icon" title="#{kind}, rgb(#{rgb})"} +
|
214
|
-
%Q{ style="background-color: rgb(#{rgb})"></span>}
|
215
|
-
end
|
216
|
-
|
217
|
-
def abbr_time_ago time, precision=1
|
218
|
-
return unless time
|
219
|
-
ago = time_ago(time)
|
220
|
-
short = ago.take(precision).join(' ')
|
221
|
-
%Q{(<abbr title="#{time}, #{ago.join(' ')} ago">#{short} ago</abbr>)}
|
222
|
-
end
|
223
|
-
|
224
|
-
def time_ago time
|
225
|
-
duration((Time.now - Time.parse(time)).to_i)
|
226
|
-
end
|
227
|
-
|
228
|
-
def duration delta
|
229
|
-
result = []
|
230
|
-
|
231
|
-
[[ 60, :seconds],
|
232
|
-
[ 60, :minutes],
|
233
|
-
[ 24, :hours ],
|
234
|
-
[365, :days ],
|
235
|
-
[999, :years ]].
|
236
|
-
inject(delta) do |length, (divisor, name)|
|
237
|
-
quotient, remainder = length.divmod(divisor)
|
238
|
-
result.unshift("#{remainder} #{name}")
|
239
|
-
break if quotient == 0
|
240
|
-
quotient
|
241
|
-
end
|
242
|
-
|
243
|
-
result
|
244
|
-
end
|
245
|
-
|
246
|
-
def sum_trans trans
|
247
|
-
trans.inject(0) do |sum, t|
|
248
|
-
sum + t['price'] * t['quantity']
|
249
|
-
end
|
250
|
-
end
|
251
|
-
|
252
|
-
def sum_items items
|
253
|
-
items.inject([0, 0]) do |sum, i|
|
254
|
-
next sum unless i
|
255
|
-
b = i['buys']
|
256
|
-
s = i['sells']
|
257
|
-
sum[0] += b['unit_price'] * i['count'] if b
|
258
|
-
sum[1] += s['unit_price'] * i['count'] if s
|
259
|
-
sum
|
260
|
-
end
|
261
|
-
end
|
262
|
-
|
263
|
-
def all_items
|
264
|
-
bank, mats, chars = all_items_defer
|
265
|
-
flatten_chars = chars.flat_map do |c|
|
266
|
-
c['equipment'] +
|
267
|
-
c['bags'] +
|
268
|
-
c['bags'].flat_map{ |c| c && c['inventory'] }
|
269
|
-
end
|
270
|
-
(bank + mats + flatten_chars).compact.
|
271
|
-
sort_by{ |i| i['name'] || i['id'].to_s }.inject([]) do |r, i|
|
272
|
-
last = r.last
|
273
|
-
if last && last['id'] == i['id'] &&
|
274
|
-
last.values_at('skin', 'upgrades', 'infusions').compact.empty?
|
275
|
-
last['count'] += i['count']
|
276
|
-
else
|
277
|
-
r << i
|
278
|
-
end
|
279
|
-
r
|
280
|
-
end
|
281
|
-
end
|
282
|
-
|
283
|
-
def find_my_item id
|
284
|
-
bank, mats, chars = all_items_defer
|
285
|
-
[select_item(bank.compact, id), select_item(mats.compact, id),
|
286
|
-
chars.inject({}){ |r, c|
|
287
|
-
equi = select_item(c['equipment'].compact, id)
|
288
|
-
bags = c['bags'].reject(&:nil?).map do |b|
|
289
|
-
selected = select_item(b['inventory'].compact, id)
|
290
|
-
b.merge('inventory' => selected) if selected.any? ||
|
291
|
-
b['id'] == id
|
292
|
-
end.compact
|
293
|
-
r[c['name']] = [equi, bags] if equi.any? || bags.any?
|
294
|
-
r
|
295
|
-
}]
|
296
|
-
end
|
297
|
-
|
298
|
-
def select_item items, id
|
299
|
-
items.select{ |i| i['id'] == id }
|
300
|
-
end
|
301
|
-
|
302
|
-
def all_items_defer
|
303
|
-
bank = gw2_defer(:with_item_detail, 'v2/account/bank')
|
304
|
-
mats = gw2_defer(:with_item_detail, 'v2/account/materials')
|
305
|
-
chars = gw2_defer(:characters_with_detail).map do |c|
|
306
|
-
c['equipment'] = gw2_defer(:expand_item_detail, c['equipment'])
|
307
|
-
c['bags'] = gw2_defer(:bags_with_detail , c['bags'])
|
308
|
-
c
|
309
|
-
end
|
310
|
-
[bank, mats, chars]
|
311
|
-
end
|
312
|
-
|
313
|
-
# CONTROLLER
|
314
|
-
def gw2_request msg, *args
|
315
|
-
block ||= :itself.to_proc
|
316
|
-
refresh = !!request.GET['r']
|
317
|
-
opts = {'cache.update' => refresh, 'expires_in' => 600}
|
318
|
-
args << {} if msg == :with_item_detail
|
319
|
-
gw2.public_send(msg, *args, opts)
|
320
|
-
end
|
321
|
-
|
322
|
-
def gw2_defer msg, *args
|
323
|
-
PromisePool::Promise.new.defer do
|
324
|
-
gw2_request(msg, *args)
|
325
|
-
end.future
|
326
|
-
end
|
327
|
-
|
328
|
-
def skin_request type, subtype=nil, weight=nil
|
329
|
-
@items = gw2_request(:skins_with_detail).select do |i|
|
330
|
-
i['type'] == type &&
|
331
|
-
(subtype.nil? || subtype == i['details']['type']) &&
|
332
|
-
(weight.nil? || weight == i['details']['weight_class'])
|
333
|
-
end
|
334
|
-
@skin_submenu = "menu_#{type.downcase}s" if subtype
|
335
|
-
@subtype = subtype.downcase if subtype
|
336
|
-
@weight = weight.downcase if weight
|
337
|
-
@unlocked = @items.count{ |i| i['count'] > 0 }
|
338
|
-
render :skins
|
339
|
-
end
|
340
|
-
|
341
|
-
def trans_request msg, path
|
342
|
-
@trans = gw2_request(msg, path, :page => p)
|
343
|
-
@pages = calculate_pages("v2/commerce/transactions/#{path}")
|
344
|
-
@total = sum_trans(@trans)
|
345
|
-
render :transactions
|
346
|
-
end
|
347
|
-
|
348
|
-
def group_by_crafting characters
|
349
|
-
characters.inject(Hash.new{|h,k|h[k]=[]}) do |group, char|
|
350
|
-
char['crafting'].each do |crafting|
|
351
|
-
group[crafting['discipline']] <<
|
352
|
-
[crafting['rating'], char['name'], crafting['active']]
|
353
|
-
end
|
354
|
-
group
|
355
|
-
end
|
356
|
-
end
|
357
|
-
|
358
|
-
def calculate_pages path
|
359
|
-
link = gw2.get(path, {:page_size => 200},
|
360
|
-
RC::RESPONSE_KEY => RC::RESPONSE_HEADERS)['LINK']
|
361
|
-
pages = RC::ParseLink.parse_link(link)
|
362
|
-
parse_page(pages['first']['uri'])..parse_page(pages['last']['uri'])
|
363
|
-
end
|
364
|
-
|
365
|
-
def parse_page uri
|
366
|
-
RC::ParseQuery.parse_query(URI.parse(uri).query)['page'].to_i
|
367
|
-
end
|
368
|
-
|
369
|
-
def gw2
|
370
|
-
Client.new(:access_token => access_token,
|
371
|
-
:log_method => logger(env).method(:info),
|
372
|
-
:cache => RestGW2::Cache.default(logger(env)))
|
373
|
-
end
|
374
|
-
|
375
|
-
# ACCESS TOKEN
|
376
|
-
def access_token
|
377
|
-
t && decrypt(t) || ENV['RESTGW2_ACCESS_TOKEN']
|
378
|
-
rescue ArgumentError, OpenSSL::Cipher::CipherError => e
|
379
|
-
raise RestGW2::Error.new({'text' => e.message}, 0)
|
380
|
-
end
|
381
|
-
|
382
|
-
def t
|
383
|
-
@t ||= begin
|
384
|
-
r = request.GET['t']
|
385
|
-
r if r && !r.strip.empty?
|
386
|
-
end
|
387
|
-
end
|
388
|
-
|
389
|
-
def p
|
390
|
-
@p ||= zero_is_nil(request.GET['p'])
|
391
|
-
end
|
392
|
-
|
393
|
-
def zero_is_nil n
|
394
|
-
r = n.to_i
|
395
|
-
r if r != 0
|
396
|
-
end
|
397
|
-
|
398
|
-
# UTILITIES
|
399
|
-
def encrypt data
|
400
|
-
cipher = new_cipher
|
401
|
-
cipher.encrypt
|
402
|
-
cipher.key = SECRET
|
403
|
-
iv = cipher.random_iv
|
404
|
-
encrypted = cipher.update(data) + cipher.final
|
405
|
-
tag = auth_tag(cipher)
|
406
|
-
encode_base64(iv, encrypted, tag)
|
407
|
-
end
|
408
|
-
|
409
|
-
def decrypt data
|
410
|
-
iv, encrypted, tag = decode_base64(data)
|
411
|
-
cipher = new_cipher
|
412
|
-
cipher.decrypt
|
413
|
-
cipher.key = SECRET
|
414
|
-
cipher.iv = iv
|
415
|
-
set_auth_tag(cipher, tag)
|
416
|
-
cipher.update(encrypted) + cipher.final
|
417
|
-
end
|
418
|
-
|
419
|
-
def encode_base64 *data
|
420
|
-
data.map{ |d| [d].pack('m0') }.join('.').tr('+/=', '-_~')
|
421
|
-
end
|
422
|
-
|
423
|
-
def decode_base64 str
|
424
|
-
str.split('.').map{ |d| d.tr('-_~', '+/=').unpack('m0').first }
|
425
|
-
end
|
426
|
-
|
427
|
-
def new_cipher
|
428
|
-
OpenSSL::Cipher.new(ENV['CIPHER_ALGO'] || 'aes-128-gcm')
|
429
|
-
rescue OpenSSL::Cipher::CipherError
|
430
|
-
OpenSSL::Cipher.new('aes-128-cbc')
|
431
|
-
end
|
432
|
-
|
433
|
-
def auth_tag cipher
|
434
|
-
cipher.respond_to?(:auth_tag) && cipher.auth_tag || ''
|
435
|
-
end
|
436
|
-
|
437
|
-
def set_auth_tag cipher, tag
|
438
|
-
cipher.respond_to?(:auth_tag=) && cipher.auth_tag = tag
|
439
|
-
end
|
440
|
-
|
441
|
-
# MISC
|
442
|
-
def logger env
|
443
|
-
env['rack.logger'] || begin
|
444
|
-
require 'logger'
|
445
|
-
Logger.new(env['rack.errors'])
|
446
|
-
end
|
447
|
-
end
|
448
|
-
}
|
449
|
-
|
450
|
-
handle Timeout::Error do
|
451
|
-
status 504
|
452
|
-
@error = 'Timeout. Please try again.'
|
453
|
-
render :error
|
454
|
-
end
|
455
|
-
|
456
|
-
handle RestGW2::Error do |e|
|
457
|
-
status 502
|
458
|
-
@error = e.error['text']
|
459
|
-
render :error
|
460
|
-
end
|
461
|
-
|
462
|
-
post '/access_token' do
|
463
|
-
t = encrypt(request.POST['access_token'])
|
464
|
-
r = request.POST['referrer']
|
465
|
-
u = if r == path('/') then path('/account') else r end
|
466
|
-
found "#{u}?t=#{t}"
|
467
|
-
end
|
468
|
-
|
469
|
-
get '/' do
|
470
|
-
render :index
|
471
|
-
end
|
472
|
-
|
473
|
-
get '/account' do
|
474
|
-
@info = gw2_request(:account_with_detail)
|
475
|
-
@info['guilds'].map!(&method(:show_guild))
|
476
|
-
render :info
|
477
|
-
end
|
478
|
-
|
479
|
-
get %r{\A/guilds/(?<uuid>[^/]+)\z} do |m|
|
480
|
-
gid = m[:uuid]
|
481
|
-
@guilds = gw2_request(:account_with_detail)['guilds']
|
482
|
-
if @guilds.find{ |g| g['guild_id'] == gid }
|
483
|
-
@treasury = gw2_defer(:treasury_with_detail, gid)
|
484
|
-
@stash = gw2_defer( :stash_with_detail, gid)
|
485
|
-
render :guild
|
486
|
-
else
|
487
|
-
status 404
|
488
|
-
@error = "Cannot find guild id: #{gid}"
|
489
|
-
render :error
|
490
|
-
end
|
491
|
-
end
|
492
|
-
|
493
|
-
get '/characters' do
|
494
|
-
@chars = gw2_request(:characters_with_detail)
|
495
|
-
@total = @chars.inject(0){ |t, c| t + c['age'] }
|
496
|
-
@craftings = group_by_crafting(@chars)
|
497
|
-
render :characters
|
498
|
-
end
|
499
|
-
|
500
|
-
get %r{\A/characters/(?<name>[\w ]+)\z} do |m|
|
501
|
-
characters = gw2_request(:characters_with_detail)
|
502
|
-
@names = characters.map { |c| c['name'] }
|
503
|
-
name = m[:name]
|
504
|
-
char = characters.find{ |c| c['name'] == name }
|
505
|
-
|
506
|
-
equi = gw2_defer(:expand_item_detail, char['equipment'])
|
507
|
-
bags = gw2_defer(:bags_with_detail , char['bags'])
|
508
|
-
|
509
|
-
@equi = equi
|
510
|
-
@bags = bags
|
511
|
-
|
512
|
-
@equi_buy, @equi_sell = sum_items(@equi)
|
513
|
-
@bags_buy, @bags_sell = sum_items(@bags +
|
514
|
-
@bags.flat_map{ |c| c && c['inventory'] })
|
515
|
-
render :profile
|
516
|
-
end
|
517
|
-
|
518
|
-
get '/dyes' do
|
519
|
-
@dyes = gw2_request(:dyes_with_detail)
|
520
|
-
@buy, @sell = sum_items(@dyes)
|
521
|
-
@unlocked = @dyes.count{ |d| d['count'] > 0 }
|
522
|
-
render :dyes
|
523
|
-
end
|
524
|
-
|
525
|
-
get '/skins/backpacks' do
|
526
|
-
skin_request('Back')
|
527
|
-
end
|
528
|
-
|
529
|
-
weapons.each do |weapon|
|
530
|
-
get "/skins/weapons/#{weapon.downcase}" do
|
531
|
-
skin_request('Weapon', weapon)
|
532
|
-
end
|
533
|
-
end
|
534
|
-
|
535
|
-
armors.each do |armor|
|
536
|
-
armors_weight.each do |weight|
|
537
|
-
get "/skins/armors/#{armor.downcase}/#{weight.downcase}" do
|
538
|
-
skin_request('Armor', armor, weight)
|
539
|
-
end
|
540
|
-
end
|
541
|
-
end
|
542
|
-
|
543
|
-
get '/minis' do
|
544
|
-
render :items, gw2_request(:minis_with_detail)
|
545
|
-
end
|
546
|
-
|
547
|
-
get '/achievements' do
|
548
|
-
render :wip
|
549
|
-
end
|
550
|
-
|
551
|
-
get '/bank' do
|
552
|
-
render :items, gw2_request(:with_item_detail, 'v2/account/bank')
|
553
|
-
end
|
554
|
-
|
555
|
-
get '/materials' do
|
556
|
-
render :items, gw2_request(:with_item_detail, 'v2/account/materials')
|
557
|
-
end
|
558
|
-
|
559
|
-
get '/wallet' do
|
560
|
-
@wallet = gw2_request(:wallet_with_detail)
|
561
|
-
render :wallet
|
562
|
-
end
|
563
|
-
|
564
|
-
get '/items' do
|
565
|
-
render :items, all_items
|
566
|
-
end
|
567
|
-
|
568
|
-
get %r{\A/items/(?<id>\d+)\z} do |m|
|
569
|
-
items = find_my_item(m[:id].to_i)
|
570
|
-
@bank, @materials, @chars = items
|
571
|
-
@buy, @sell = sum_items(@bank + @materials + @chars.values.flatten)
|
572
|
-
render :items_from
|
573
|
-
end
|
574
|
-
|
575
|
-
get '/transactions/buying' do
|
576
|
-
trans_request(:transactions_with_detail_compact, 'current/buys')
|
577
|
-
end
|
578
|
-
|
579
|
-
get '/transactions/selling' do
|
580
|
-
trans_request(:transactions_with_detail_compact, 'current/sells')
|
581
|
-
end
|
582
|
-
|
583
|
-
get '/transactions/bought' do
|
584
|
-
trans_request(:transactions_with_detail_compact, 'history/buys')
|
585
|
-
end
|
586
|
-
|
587
|
-
get '/transactions/sold' do
|
588
|
-
trans_request(:transactions_with_detail_compact, 'history/sells')
|
589
|
-
end
|
590
|
-
|
591
|
-
get '/tokeninfo' do
|
592
|
-
@info = gw2_request(:get, 'v2/tokeninfo')
|
593
|
-
render :info
|
594
|
-
end
|
595
|
-
end
|
596
|
-
|
597
23
|
Server = Jellyfish::Builder.app do
|
598
24
|
use Rack::CommonLogger
|
599
25
|
use Rack::Chunked
|
@@ -601,12 +27,8 @@ module RestGW2
|
|
601
27
|
use Rack::Deflater
|
602
28
|
use Rack::ContentType, 'text/html; charset=utf-8'
|
603
29
|
|
604
|
-
map '/assets' do
|
605
|
-
run Rack::Directory.new('public')
|
606
|
-
end
|
607
|
-
|
608
30
|
map '/' do
|
609
|
-
run
|
31
|
+
run ServerAction.new
|
610
32
|
end
|
611
33
|
end
|
612
34
|
end
|