rest-gw2 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|