rest-gw2 0.1.0 → 0.2.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 +4 -4
- data/.gitmodules +3 -0
- data/CHANGES.md +28 -0
- data/LICENSE +201 -0
- data/README.md +33 -6
- data/Rakefile +8 -2
- data/bin/rest-gw2 +4 -0
- data/config.ru +3 -0
- data/lib/rest-gw2/client.rb +133 -10
- data/lib/rest-gw2/server.rb +328 -32
- data/lib/rest-gw2/server/cache.rb +38 -0
- data/lib/rest-gw2/server/runner.rb +108 -0
- data/lib/rest-gw2/version.rb +4 -0
- data/lib/rest-gw2/view/error.erb +1 -0
- data/lib/rest-gw2/view/index.erb +16 -0
- data/lib/rest-gw2/view/info.erb +17 -0
- data/lib/rest-gw2/view/items.erb +18 -0
- data/lib/rest-gw2/view/layout.erb +57 -0
- data/lib/rest-gw2/view/menu.erb +15 -0
- data/lib/rest-gw2/view/pages.erb +7 -0
- data/lib/rest-gw2/view/transactions.erb +28 -0
- data/lib/rest-gw2/view/wallet.erb +16 -0
- data/lib/rest-gw2/view/wip.erb +1 -0
- data/rest-gw2.gemspec +39 -8
- metadata +92 -6
- data/lib/rest-gw2/view/bank.erb +0 -25
data/lib/rest-gw2/server.rb
CHANGED
@@ -1,8 +1,14 @@
|
|
1
1
|
|
2
|
-
require '
|
2
|
+
require 'rest-gw2/server/cache'
|
3
|
+
require 'mime/types'
|
4
|
+
require 'rest-core'
|
3
5
|
require 'jellyfish'
|
6
|
+
require 'rack'
|
4
7
|
|
8
|
+
require 'timeout'
|
9
|
+
require 'openssl'
|
5
10
|
require 'erb'
|
11
|
+
require 'cgi'
|
6
12
|
|
7
13
|
module RestGW2
|
8
14
|
CONFIG = ENV['RESTGW2_CONFIG'] || File.expand_path("#{__dir__}/../../.env")
|
@@ -19,55 +25,345 @@ module RestGW2
|
|
19
25
|
ENV[k] ||= v
|
20
26
|
end
|
21
27
|
|
22
|
-
module DalliExtension
|
23
|
-
def [] *args
|
24
|
-
get(*args)
|
25
|
-
end
|
26
|
-
|
27
|
-
def []= *args
|
28
|
-
set(*args)
|
29
|
-
end
|
30
|
-
|
31
|
-
def store *args
|
32
|
-
set(*args)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
def self.cache
|
37
|
-
@cache ||= begin
|
38
|
-
client = Dalli::Client.new
|
39
|
-
client.extend(DalliExtension)
|
40
|
-
client
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
28
|
class ServerCore
|
45
29
|
include Jellyfish
|
30
|
+
SECRET = ENV['RESTGW2_SECRET'] || 'RESTGW2_SECRET'*2
|
31
|
+
COINS = %w[gold silver copper].zip(%w[
|
32
|
+
https://wiki.guildwars2.com/images/d/d1/Gold_coin.png
|
33
|
+
https://wiki.guildwars2.com/images/3/3c/Silver_coin.png
|
34
|
+
https://wiki.guildwars2.com/images/e/eb/Copper_coin.png
|
35
|
+
]).freeze
|
46
36
|
controller_include Module.new{
|
37
|
+
# VIEW
|
47
38
|
def render path
|
48
|
-
|
39
|
+
erb(:layout){ erb(path) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def erb path, &block
|
43
|
+
ERB.new(views(path)).result(binding, &block)
|
44
|
+
end
|
45
|
+
|
46
|
+
def h str
|
47
|
+
CGI.escape_html(str) if str.kind_of?(String)
|
48
|
+
end
|
49
|
+
|
50
|
+
def u str
|
51
|
+
CGI.escape(str) if str.kind_of?(String)
|
52
|
+
end
|
53
|
+
|
54
|
+
def path str, q={}
|
55
|
+
RC::Middleware.request_uri(
|
56
|
+
RC::REQUEST_PATH => "#{ENV['RESTGW2_PREFIX']}#{str}",
|
57
|
+
RC::REQUEST_QUERY => q)
|
49
58
|
end
|
50
59
|
|
51
60
|
def views path
|
52
61
|
@views ||= {}
|
53
|
-
@views[path]
|
62
|
+
@views[path] = File.read("#{__dir__}/view/#{path}.erb")
|
63
|
+
end
|
64
|
+
|
65
|
+
def refresh_path
|
66
|
+
path(request.path, :p => p, :r => '1', :t => t)
|
67
|
+
end
|
68
|
+
|
69
|
+
# TODO: clean me up
|
70
|
+
def menu item, title, query={}
|
71
|
+
href = path(item, query.merge(:t => t))
|
72
|
+
if path(request.path, :p => p, :t => t) == href
|
73
|
+
title
|
74
|
+
else
|
75
|
+
%Q{<a href="#{href}">#{title}</a>}
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# TODO: clean me up
|
80
|
+
def menu_trans item, title
|
81
|
+
key = "/transactions#{item}"
|
82
|
+
if path(request.path) == path(key)
|
83
|
+
menu(key, title, :p => p)
|
84
|
+
else
|
85
|
+
menu(key, title)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def page num
|
90
|
+
menu(request.path, num.to_s, :p => zero_is_nil(num))
|
91
|
+
end
|
92
|
+
|
93
|
+
# HELPER
|
94
|
+
def blank_icon
|
95
|
+
%Q{<img class="icon" src="https://upload.wikimedia.org/wikipedia/commons/d/d2/Blank.png"/>}
|
96
|
+
end
|
97
|
+
|
98
|
+
def item_wiki item
|
99
|
+
if item['name']
|
100
|
+
page = item['name'].tr(' ', '_')
|
101
|
+
missing = if item['count'] == 0 then ' missing' else nil end
|
102
|
+
img = %Q{<img class="icon#{missing}" title="#{item_title(item)}"} +
|
103
|
+
%Q{ src="#{h item['icon']}"/>}
|
104
|
+
%Q{<a href="http://wiki.guildwars2.com/wiki/#{u page}">#{img}</a>}
|
105
|
+
else
|
106
|
+
blank_icon
|
107
|
+
end
|
54
108
|
end
|
55
109
|
|
56
110
|
def item_title item
|
57
|
-
|
58
|
-
|
111
|
+
d = item['description']
|
112
|
+
d && d.unpack('U*').map{ |c| "&##{c};" }.join
|
113
|
+
end
|
114
|
+
|
115
|
+
def item_count item
|
116
|
+
c = item['count']
|
117
|
+
"(#{c})" if c > 1
|
118
|
+
end
|
119
|
+
|
120
|
+
def item_price item
|
121
|
+
b = item['buys']
|
122
|
+
s = item['sells']
|
123
|
+
bb = b && price(b['unit_price'])
|
124
|
+
ss = s && price(s['unit_price'])
|
125
|
+
%Q{#{bb} / #{ss}} if bb || ss
|
126
|
+
end
|
127
|
+
|
128
|
+
def price copper
|
129
|
+
g = copper / 100_00
|
130
|
+
s = copper % 100_00 / 100
|
131
|
+
c = copper % 100
|
132
|
+
l = [g, s, c]
|
133
|
+
n = l.index(&:nonzero?)
|
134
|
+
return '-' unless n
|
135
|
+
l.zip(COINS).drop(n).map do |(num, (title, src))|
|
136
|
+
%Q{#{num}<img class="price" title="#{h title}" src="#{h src}"/>}
|
137
|
+
end.join(' ')
|
138
|
+
end
|
139
|
+
|
140
|
+
def abbr_time_ago time, precision=1
|
141
|
+
return unless time
|
142
|
+
ago = time_ago(time)
|
143
|
+
short = ago.take(precision).join(' ')
|
144
|
+
%Q{(<abbr title="#{time}, #{ago.join(' ')} ago">#{short} ago</abbr>)}
|
145
|
+
end
|
146
|
+
|
147
|
+
def time_ago time, precision=1
|
148
|
+
delta = (Time.now - Time.parse(time)).to_i
|
149
|
+
result = []
|
150
|
+
|
151
|
+
[[ 60, :seconds],
|
152
|
+
[ 60, :minutes],
|
153
|
+
[ 24, :hours ],
|
154
|
+
[365, :days ],
|
155
|
+
[999, :years ]].
|
156
|
+
inject(delta) do |length, (divisor, name)|
|
157
|
+
quotient, remainder = length.divmod(divisor)
|
158
|
+
result.unshift("#{remainder} #{name}")
|
159
|
+
break if quotient == 0
|
160
|
+
quotient
|
161
|
+
end
|
162
|
+
|
163
|
+
result
|
164
|
+
end
|
165
|
+
|
166
|
+
def sum_trans trans
|
167
|
+
trans.inject(0) do |sum, t|
|
168
|
+
sum + t['price'] * t['quantity']
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def sum_items items
|
173
|
+
items.inject([0, 0]) do |sum, i|
|
174
|
+
next sum unless i
|
175
|
+
b = i['buys']
|
176
|
+
s = i['sells']
|
177
|
+
sum[0] += b['unit_price'] * i['count'] if b
|
178
|
+
sum[1] += s['unit_price'] * i['count'] if s
|
179
|
+
sum
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# CONTROLLER
|
184
|
+
def gw2_call msg, *args
|
185
|
+
refresh = !!request.GET['r']
|
186
|
+
opts = {'cache.update' => refresh, 'expires_in' => 600}
|
187
|
+
yield(gw2.public_send(msg, *args, opts).itself)
|
188
|
+
rescue RestGW2::Error => e
|
189
|
+
@error = e.error['text']
|
190
|
+
render :error
|
191
|
+
end
|
192
|
+
|
193
|
+
def trans_call msg, path, &block
|
194
|
+
gw2_call(msg, path, :page => p) do |trans|
|
195
|
+
@pages = calculate_pages("v2/commerce/transactions/#{path}")
|
196
|
+
@trans = trans
|
197
|
+
@total = sum_trans(trans)
|
198
|
+
render :transactions
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def calculate_pages path
|
203
|
+
link = gw2.get(path, {:page_size => 200},
|
204
|
+
RC::RESPONSE_KEY => RC::RESPONSE_HEADERS)['LINK']
|
205
|
+
pages = RC::ParseLink.parse_link(link)
|
206
|
+
parse_page(pages['first']['uri'])..parse_page(pages['last']['uri'])
|
207
|
+
end
|
208
|
+
|
209
|
+
def parse_page uri
|
210
|
+
RC::ParseQuery.parse_query(URI.parse(uri).query)['page'].to_i
|
59
211
|
end
|
60
212
|
|
61
213
|
def gw2
|
62
|
-
Client.new(:access_token =>
|
63
|
-
:log_method => env
|
64
|
-
:cache => RestGW2.
|
214
|
+
Client.new(:access_token => access_token,
|
215
|
+
:log_method => logger(env).method(:info),
|
216
|
+
:cache => RestGW2::Cache.default(logger(env)))
|
217
|
+
end
|
218
|
+
|
219
|
+
# ACCESS TOKEN
|
220
|
+
def access_token
|
221
|
+
t && decrypt(t) || ENV['RESTGW2_ACCESS_TOKEN']
|
222
|
+
rescue ArgumentError, OpenSSL::Cipher::CipherError => e
|
223
|
+
raise RestGW2::Error.new({'text' => e.message}, 0)
|
224
|
+
end
|
225
|
+
|
226
|
+
def t
|
227
|
+
@t ||= begin
|
228
|
+
r = request.GET['t']
|
229
|
+
r if r && !r.strip.empty?
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def p
|
234
|
+
@p ||= zero_is_nil(request.GET['p'])
|
235
|
+
end
|
236
|
+
|
237
|
+
def zero_is_nil n
|
238
|
+
r = n.to_i
|
239
|
+
r if r != 0
|
240
|
+
end
|
241
|
+
|
242
|
+
# UTILITIES
|
243
|
+
def encrypt data
|
244
|
+
cipher = OpenSSL::Cipher.new('aes-128-gcm')
|
245
|
+
cipher.encrypt
|
246
|
+
cipher.key = SECRET
|
247
|
+
iv = cipher.random_iv
|
248
|
+
encrypted = cipher.update(data) + cipher.final
|
249
|
+
tag = cipher.auth_tag
|
250
|
+
encode_base64(iv, encrypted, tag)
|
251
|
+
end
|
252
|
+
|
253
|
+
def decrypt data
|
254
|
+
iv, encrypted, tag = decode_base64(data)
|
255
|
+
decipher = OpenSSL::Cipher.new('aes-128-gcm')
|
256
|
+
decipher.decrypt
|
257
|
+
decipher.key = SECRET
|
258
|
+
decipher.iv = iv
|
259
|
+
decipher.auth_tag = tag
|
260
|
+
decipher.update(encrypted) + decipher.final
|
261
|
+
end
|
262
|
+
|
263
|
+
def encode_base64 *data
|
264
|
+
data.map{ |d| [d].pack('m0') }.join('.').tr('+/=', '-_~')
|
265
|
+
end
|
266
|
+
|
267
|
+
def decode_base64 str
|
268
|
+
str.split('.').map{ |d| d.tr('-_~', '+/=').unpack('m0').first }
|
269
|
+
end
|
270
|
+
|
271
|
+
# MISC
|
272
|
+
def logger env
|
273
|
+
env['rack.logger'] || begin
|
274
|
+
require 'logger'
|
275
|
+
Logger.new(env['rack.errors'])
|
276
|
+
end
|
65
277
|
end
|
66
278
|
}
|
67
279
|
|
280
|
+
handle Timeout::Error do
|
281
|
+
@error = 'Timeout. Please try again.'
|
282
|
+
render :error
|
283
|
+
end
|
284
|
+
|
285
|
+
post '/access_token' do
|
286
|
+
t = encrypt(request.POST['access_token'])
|
287
|
+
r = request.POST['referrer']
|
288
|
+
u = if r == path('/') then path('/account') else r end
|
289
|
+
found "#{u}?t=#{t}"
|
290
|
+
end
|
291
|
+
|
292
|
+
get '/' do
|
293
|
+
render :index
|
294
|
+
end
|
295
|
+
|
296
|
+
get '/account' do
|
297
|
+
gw2_call(:account_with_detail) do |account|
|
298
|
+
@info = account
|
299
|
+
render :info
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
get '/characters' do
|
304
|
+
render :wip
|
305
|
+
end
|
306
|
+
|
307
|
+
get '/dyes' do
|
308
|
+
render :wip
|
309
|
+
end
|
310
|
+
|
311
|
+
get '/skins' do
|
312
|
+
render :wip
|
313
|
+
end
|
314
|
+
|
315
|
+
get '/minis' do
|
316
|
+
render :wip
|
317
|
+
end
|
318
|
+
|
319
|
+
get '/achievements' do
|
320
|
+
render :wip
|
321
|
+
end
|
322
|
+
|
68
323
|
get '/bank' do
|
69
|
-
|
70
|
-
|
324
|
+
gw2_call(:with_item_detail, 'v2/account/bank') do |items|
|
325
|
+
@items = items
|
326
|
+
@buy, @sell = sum_items(items)
|
327
|
+
render :items
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
get '/materials' do
|
332
|
+
gw2_call(:with_item_detail, 'v2/account/materials') do |items|
|
333
|
+
@items = items
|
334
|
+
@buy, @sell = sum_items(items)
|
335
|
+
render :items
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
get '/wallet' do
|
340
|
+
gw2_call(:wallet_with_detail) do |wallet|
|
341
|
+
@wallet = wallet
|
342
|
+
render :wallet
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
get '/transactions/buying' do
|
347
|
+
trans_call(:transactions_with_detail, 'current/buys')
|
348
|
+
end
|
349
|
+
|
350
|
+
get '/transactions/selling' do
|
351
|
+
trans_call(:transactions_with_detail, 'current/sells')
|
352
|
+
end
|
353
|
+
|
354
|
+
get '/transactions/bought' do
|
355
|
+
trans_call(:transactions_with_detail_compact, 'history/buys')
|
356
|
+
end
|
357
|
+
|
358
|
+
get '/transactions/sold' do
|
359
|
+
trans_call(:transactions_with_detail_compact, 'history/sells')
|
360
|
+
end
|
361
|
+
|
362
|
+
get '/tokeninfo' do
|
363
|
+
gw2_call(:get, 'v2/tokeninfo') do |info|
|
364
|
+
@info = info
|
365
|
+
render :info
|
366
|
+
end
|
71
367
|
end
|
72
368
|
end
|
73
369
|
|
@@ -0,0 +1,38 @@
|
|
1
|
+
|
2
|
+
module RestGW2
|
3
|
+
module Cache
|
4
|
+
module_function
|
5
|
+
def default logger
|
6
|
+
@cache ||= Cache.pick(logger)
|
7
|
+
end
|
8
|
+
|
9
|
+
def pick logger
|
10
|
+
memcache(logger) || lru_cache(logger)
|
11
|
+
end
|
12
|
+
|
13
|
+
def memcache logger
|
14
|
+
require 'dalli'
|
15
|
+
client = Dalli::Client.new
|
16
|
+
File.open(IO::NULL) do |null|
|
17
|
+
Dalli.logger = Logger.new(null)
|
18
|
+
client.alive!
|
19
|
+
Dalli.logger = logger
|
20
|
+
end
|
21
|
+
logger.info("Memcached connected to #{client.version.keys.join(', ')}")
|
22
|
+
client.extend(RestCore::DalliExtension)
|
23
|
+
client
|
24
|
+
rescue LoadError, Dalli::RingError => e
|
25
|
+
logger.debug("Skip memcached because: #{e}")
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def lru_cache logger
|
30
|
+
require 'lru_redux'
|
31
|
+
logger.info("LRU cache size: 100")
|
32
|
+
LruRedux::ThreadSafeCache.new(100)
|
33
|
+
rescue LoadError => e
|
34
|
+
logger.debug("Skip LRU cache because: #{e}")
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
|
2
|
+
require 'rest-gw2'
|
3
|
+
|
4
|
+
module RestGW2::Runner
|
5
|
+
module_function
|
6
|
+
def options
|
7
|
+
@options ||=
|
8
|
+
[['Options:' , '' ],
|
9
|
+
['-o, --host HOST' , 'Use HOST (default: 0.0.0.0)' ],
|
10
|
+
['-p, --port PORT' , 'Use PORT (default: 8080)' ],
|
11
|
+
['-s, --server SERVER', 'Use SERVER (default: webrick)'],
|
12
|
+
['-c, --configru' , 'Print where the config.ru is' ],
|
13
|
+
['-h, --help' , 'Print this message' ],
|
14
|
+
['-v, --version' , 'Print the version' ]]
|
15
|
+
end
|
16
|
+
|
17
|
+
def root
|
18
|
+
File.expand_path("#{__dir__}/../../../")
|
19
|
+
end
|
20
|
+
|
21
|
+
def config_ru_path
|
22
|
+
"#{root}/config.ru"
|
23
|
+
end
|
24
|
+
|
25
|
+
def run argv=ARGV
|
26
|
+
unused, host, port, server = parse(argv)
|
27
|
+
warn("Unused arguments: #{unused.inspect}") unless unused.empty?
|
28
|
+
load_rack
|
29
|
+
load_rack_handlers
|
30
|
+
handler = server && Rack::Handler.get(server) || Rack::Handler.default
|
31
|
+
handler.run(Rack::Builder.new{
|
32
|
+
eval(File.read(RestGW2::Runner.config_ru_path))
|
33
|
+
}.to_app, :Host => host, :Port => port, :config => root)
|
34
|
+
end
|
35
|
+
|
36
|
+
def parse argv
|
37
|
+
unused, host, port, server = [], '0.0.0.0', 8080, nil
|
38
|
+
until argv.empty?
|
39
|
+
case arg = argv.shift
|
40
|
+
when /^-o=?(.+)?/, /^--host=?(.+)?/
|
41
|
+
host = $1 || argv.shift
|
42
|
+
missing_arg('host') unless host
|
43
|
+
|
44
|
+
when /^-p=?(.+)?/, /^--port=?(.+)?/
|
45
|
+
port = $1 || argv.shift
|
46
|
+
missing_arg('port') unless port
|
47
|
+
|
48
|
+
when /^-s=?(.+)?/, /^--server=?(.+)?/
|
49
|
+
server = $1 || argv.shift
|
50
|
+
missing_arg('server') unless server
|
51
|
+
|
52
|
+
when /^-c/, '--configru'
|
53
|
+
puts(config_ru_path)
|
54
|
+
exit
|
55
|
+
|
56
|
+
when /^-h/, '--help'
|
57
|
+
puts(help)
|
58
|
+
exit
|
59
|
+
|
60
|
+
when /^-v/, '--version'
|
61
|
+
require 'rest-gw2/version'
|
62
|
+
puts(RestGW2::VERSION)
|
63
|
+
exit
|
64
|
+
|
65
|
+
else
|
66
|
+
unused << arg
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
[unused, host, port, server]
|
71
|
+
end
|
72
|
+
|
73
|
+
def parse_next argv, arg
|
74
|
+
argv.unshift("-#{arg[2..-1]}") if arg.size > 2
|
75
|
+
end
|
76
|
+
|
77
|
+
def missing_arg arg
|
78
|
+
warn("Missing argument: #{arg}")
|
79
|
+
exit(1)
|
80
|
+
end
|
81
|
+
|
82
|
+
def load_rack
|
83
|
+
require 'rack'
|
84
|
+
rescue LoadError => e
|
85
|
+
warn(e)
|
86
|
+
puts "Maybe you should install rack by running: gem install rack"
|
87
|
+
exit(1)
|
88
|
+
end
|
89
|
+
|
90
|
+
def load_rack_handlers
|
91
|
+
require 'rack-handlers'
|
92
|
+
rescue LoadError
|
93
|
+
end
|
94
|
+
|
95
|
+
def help
|
96
|
+
optt = options.transpose
|
97
|
+
maxn = optt.first.map(&:size).max
|
98
|
+
maxd = optt.last .map(&:size).max
|
99
|
+
"Usage: rest-gw2 [OPTIONS]\n" +
|
100
|
+
options.map{ |(name, desc)|
|
101
|
+
if name.end_with?(':')
|
102
|
+
name
|
103
|
+
else
|
104
|
+
sprintf(" %-*s %-*s", maxn, name, maxd, desc)
|
105
|
+
end
|
106
|
+
}.join("\n")
|
107
|
+
end
|
108
|
+
end
|