rest-gw2 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,8 +1,14 @@
1
1
 
2
- require 'dalli'
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
- ERB.new(views(path)).result(binding)
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] ||= File.read("#{__dir__}/view/#{path}.erb")
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
- t = item['description']
58
- t && t.unpack('U*').map{ |c| "&##{c};" }.join
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 => ENV['ACCESS_TOKEN'],
63
- :log_method => env['rack.errors'].method(:puts),
64
- :cache => RestGW2.cache)
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
- @items = gw2.with_item_detail('account/bank')
70
- render 'bank'
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