cloudrail_si 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +3 -0
- data/examples/README.md +3 -0
- data/examples/simple/index.rb +11 -0
- data/lib/cloudrail_si.rb +41 -0
- data/lib/cloudrail_si/RedirectReceivers.rb +40 -0
- data/lib/cloudrail_si/Settings.rb +12 -0
- data/lib/cloudrail_si/errors/DetailErrors.rb +13 -0
- data/lib/cloudrail_si/errors/InternalError.rb +10 -0
- data/lib/cloudrail_si/errors/UserError.rb +11 -0
- data/lib/cloudrail_si/helpers/Helper.rb +151 -0
- data/lib/cloudrail_si/helpers/LimitedReadableStream.rb +55 -0
- data/lib/cloudrail_si/helpers/SequenceReadableStream.rb +59 -0
- data/lib/cloudrail_si/servicecode/InitSelfTest.rb +92 -0
- data/lib/cloudrail_si/servicecode/Interpreter.rb +213 -0
- data/lib/cloudrail_si/servicecode/Sandbox.rb +361 -0
- data/lib/cloudrail_si/servicecode/VarAddress.rb +11 -0
- data/lib/cloudrail_si/servicecode/commands/AwaitCodeRedirect.rb +48 -0
- data/lib/cloudrail_si/servicecode/commands/Break.rb +16 -0
- data/lib/cloudrail_si/servicecode/commands/CallFunc.rb +26 -0
- data/lib/cloudrail_si/servicecode/commands/Clone.rb +18 -0
- data/lib/cloudrail_si/servicecode/commands/Concat.rb +22 -0
- data/lib/cloudrail_si/servicecode/commands/Conditional.rb +27 -0
- data/lib/cloudrail_si/servicecode/commands/Create.rb +56 -0
- data/lib/cloudrail_si/servicecode/commands/Delete.rb +21 -0
- data/lib/cloudrail_si/servicecode/commands/Get.rb +25 -0
- data/lib/cloudrail_si/servicecode/commands/GetMimeType.rb +1005 -0
- data/lib/cloudrail_si/servicecode/commands/JumpRel.rb +18 -0
- data/lib/cloudrail_si/servicecode/commands/Pull.rb +34 -0
- data/lib/cloudrail_si/servicecode/commands/Push.rb +32 -0
- data/lib/cloudrail_si/servicecode/commands/Return.rb +16 -0
- data/lib/cloudrail_si/servicecode/commands/Set.rb +23 -0
- data/lib/cloudrail_si/servicecode/commands/Size.rb +26 -0
- data/lib/cloudrail_si/servicecode/commands/ThrowError.rb +17 -0
- data/lib/cloudrail_si/servicecode/commands/array/Uint8ToBase64.rb +25 -0
- data/lib/cloudrail_si/servicecode/commands/debug/Out.rb +24 -0
- data/lib/cloudrail_si/servicecode/commands/hash/md5.rb +29 -0
- data/lib/cloudrail_si/servicecode/commands/hash/sha1.rb +30 -0
- data/lib/cloudrail_si/servicecode/commands/http/RequestCall.rb +51 -0
- data/lib/cloudrail_si/servicecode/commands/json/Parse.rb +40 -0
- data/lib/cloudrail_si/servicecode/commands/json/Stringify.rb +21 -0
- data/lib/cloudrail_si/servicecode/commands/math/Floor.rb +27 -0
- data/lib/cloudrail_si/servicecode/commands/math/MathCombine.rb +35 -0
- data/lib/cloudrail_si/servicecode/commands/object/GetKeyArray.rb +21 -0
- data/lib/cloudrail_si/servicecode/commands/object/GetKeyValueArrays.rb +29 -0
- data/lib/cloudrail_si/servicecode/commands/stream/MakeJoinedStream.rb +38 -0
- data/lib/cloudrail_si/servicecode/commands/stream/MakeLimitedStream.rb +23 -0
- data/lib/cloudrail_si/servicecode/commands/stream/StreamToString.rb +25 -0
- data/lib/cloudrail_si/servicecode/commands/stream/StringToStream.rb +20 -0
- data/lib/cloudrail_si/servicecode/commands/string/Base64Encode.rb +42 -0
- data/lib/cloudrail_si/servicecode/commands/string/Format.rb +43 -0
- data/lib/cloudrail_si/servicecode/commands/string/IndexOf.rb +29 -0
- data/lib/cloudrail_si/servicecode/commands/string/LastIndexOf.rb +29 -0
- data/lib/cloudrail_si/servicecode/commands/string/Split.rb +29 -0
- data/lib/cloudrail_si/servicecode/commands/string/StringTransform.rb +29 -0
- data/lib/cloudrail_si/servicecode/commands/string/Substr.rb +30 -0
- data/lib/cloudrail_si/servicecode/commands/string/Substring.rb +30 -0
- data/lib/cloudrail_si/services/Facebook.rb +528 -0
- data/lib/cloudrail_si/services/FacebookPage.rb +310 -0
- data/lib/cloudrail_si/services/Foursquare.rb +314 -0
- data/lib/cloudrail_si/services/GitHub.rb +354 -0
- data/lib/cloudrail_si/services/GooglePlaces.rb +309 -0
- data/lib/cloudrail_si/services/GooglePlus.rb +367 -0
- data/lib/cloudrail_si/services/Instagram.rb +342 -0
- data/lib/cloudrail_si/services/LinkedIn.rb +363 -0
- data/lib/cloudrail_si/services/MicrosoftLive.rb +346 -0
- data/lib/cloudrail_si/services/Nexmo.rb +173 -0
- data/lib/cloudrail_si/services/Slack.rb +318 -0
- data/lib/cloudrail_si/services/Twilio.rb +173 -0
- data/lib/cloudrail_si/services/Twitter.rb +795 -0
- data/lib/cloudrail_si/services/Yahoo.rb +408 -0
- data/lib/cloudrail_si/services/Yelp.rb +389 -0
- data/lib/cloudrail_si/statistics/Statistics.rb +125 -0
- data/lib/cloudrail_si/types/Address.rb +9 -0
- data/lib/cloudrail_si/types/Charge.rb +33 -0
- data/lib/cloudrail_si/types/CloudMetaData.rb +19 -0
- data/lib/cloudrail_si/types/CreditCard.rb +76 -0
- data/lib/cloudrail_si/types/Date.rb +49 -0
- data/lib/cloudrail_si/types/DateOfBirth.rb +16 -0
- data/lib/cloudrail_si/types/Error.rb +21 -0
- data/lib/cloudrail_si/types/Location.rb +8 -0
- data/lib/cloudrail_si/types/POI.rb +17 -0
- data/lib/cloudrail_si/types/Refund.rb +25 -0
- data/lib/cloudrail_si/types/SandboxObject.rb +20 -0
- data/lib/cloudrail_si/types/SpaceAllocation.rb +13 -0
- data/lib/cloudrail_si/types/Subscription.rb +27 -0
- data/lib/cloudrail_si/types/SubscriptionPlan.rb +26 -0
- data/lib/cloudrail_si/types/Types.rb +43 -0
- data/lib/cloudrail_si/version.rb +3 -0
- metadata +205 -0
@@ -0,0 +1,125 @@
|
|
1
|
+
require_relative '../Settings'
|
2
|
+
require_relative '../helpers/Helper'
|
3
|
+
require_relative '../servicecode/InitSelfTest'
|
4
|
+
require_relative '../servicecode/Interpreter'
|
5
|
+
require_relative '../servicecode/Sandbox'
|
6
|
+
|
7
|
+
module CloudRailSi
|
8
|
+
module ServiceCode
|
9
|
+
class Statistics
|
10
|
+
class << self
|
11
|
+
@@CR_VERSION = @get_cr_ver
|
12
|
+
@@SERVER_URL = 'https://developers.cloudrail.com/api/entries'
|
13
|
+
@@next = 1
|
14
|
+
@@count = 0
|
15
|
+
@@data = {}
|
16
|
+
@@entry_id = nil
|
17
|
+
|
18
|
+
def add_call(service, method)
|
19
|
+
message = 'A valid CloudRail license key is required. You can get one for free at https://developers.cloudrail.com'
|
20
|
+
keyMatch = /^[a-f\d]{24}$/i
|
21
|
+
r = Regexp.new(keyMatch).freeze
|
22
|
+
raise message if (!Settings.license_key || !Settings.license_key.match(r))
|
23
|
+
|
24
|
+
calls = get_method_calls(service, method)
|
25
|
+
calls['count'] += 1
|
26
|
+
|
27
|
+
@@count += 1
|
28
|
+
if (@@count === @@next)
|
29
|
+
@@next *= 2
|
30
|
+
end
|
31
|
+
send_statistics()
|
32
|
+
end
|
33
|
+
|
34
|
+
def add_error(service, method)
|
35
|
+
calls = get_method_calls(service, method)
|
36
|
+
calls['error'] += 1
|
37
|
+
end
|
38
|
+
|
39
|
+
def send_statistics
|
40
|
+
begin
|
41
|
+
return if (@@count == 0)
|
42
|
+
|
43
|
+
body = {
|
44
|
+
data: @@data
|
45
|
+
}
|
46
|
+
|
47
|
+
if (@@entry_id)
|
48
|
+
body.id = @@entry_id
|
49
|
+
else
|
50
|
+
mac = InitSelfTest.get_mac
|
51
|
+
app = InitSelfTest.get_name_version
|
52
|
+
client = {
|
53
|
+
'os' => InitSelfTest.get_os
|
54
|
+
}
|
55
|
+
|
56
|
+
app_hash = hash_string([app['name'], app['version']].to_json)
|
57
|
+
client_hash = hash_string([mac, client['os']].to_json)
|
58
|
+
|
59
|
+
body['app'] = app
|
60
|
+
body['client'] = client
|
61
|
+
body['appKey'] = Settings.license_key unless (Settings.license_key.nil?)
|
62
|
+
body['libraryVersion'] = @@CR_VERSION
|
63
|
+
body['appHash'] = app_hash
|
64
|
+
body['clientHash'] = client_hash
|
65
|
+
body['platform'] = "Ruby"
|
66
|
+
|
67
|
+
res = Helper.make_request(
|
68
|
+
@@SERVER_URL,
|
69
|
+
{ "Content-Type" => "application/json" },
|
70
|
+
Helper.streamify_string(body.to_json, nil),
|
71
|
+
"POST")
|
72
|
+
raise StandardError.new("Unexpected response when sending stats: #{res.code}") if (res.code != '200')
|
73
|
+
res_str = Helper.dump_stream(res.body)
|
74
|
+
if !@@entry_id
|
75
|
+
obj = JSON.parse(res_str)
|
76
|
+
@@entry_id = obj.id
|
77
|
+
end
|
78
|
+
@@data = {}
|
79
|
+
@@count = 0
|
80
|
+
end
|
81
|
+
rescue => ex
|
82
|
+
Helper.log(ex)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def get_method_calls(service, method)
|
87
|
+
@@data[service] = @@data[service] || {}
|
88
|
+
calls_to_service = @@data[service]
|
89
|
+
if (!calls_to_service[method])
|
90
|
+
calls_to_service[method] = {
|
91
|
+
'count' => 0,
|
92
|
+
'error' => 0
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
calls_to_service[method]
|
97
|
+
end
|
98
|
+
|
99
|
+
def hash_string(str)
|
100
|
+
service_code = {
|
101
|
+
"hashString" => [
|
102
|
+
["hash.md5", "$L0", "$P1"],
|
103
|
+
["size", "$L1", "$L0"],
|
104
|
+
["set", "$L2", 0],
|
105
|
+
["set", "$P0", ""],
|
106
|
+
["get", "$L3", "$L0", "$L2"],
|
107
|
+
["string.format", "$L4", "%02X", "$L3"],
|
108
|
+
["string.concat", "$P0", "$P0", "$L4"],
|
109
|
+
["math.add", "$L2", "$L2", 1],
|
110
|
+
["if>=than", "$L2", "$L1", -5]
|
111
|
+
]
|
112
|
+
}
|
113
|
+
|
114
|
+
interpreter = Interpreter.new(Sandbox.new(service_code, [], {}))
|
115
|
+
interpreter.call_function_sync("hashString", nil, str)
|
116
|
+
interpreter.get_parameter(0)
|
117
|
+
end
|
118
|
+
|
119
|
+
def get_cr_ver
|
120
|
+
Cloudrail::VERSION
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative './SandboxObject'
|
2
|
+
|
3
|
+
module CloudRailSi
|
4
|
+
module Types
|
5
|
+
class Charge < SandboxObject
|
6
|
+
attr_reader :amount, :created, :currency, :id, :refunded, :source, :status
|
7
|
+
|
8
|
+
def initialize(amount, created, currency, id, refunded, source, status)
|
9
|
+
super()
|
10
|
+
@amount = amount
|
11
|
+
@created = created
|
12
|
+
@currency = currency.upcase
|
13
|
+
@id = id
|
14
|
+
@refunded = !!refunded
|
15
|
+
@source = source
|
16
|
+
@status = status
|
17
|
+
|
18
|
+
@currency = currency.upcase
|
19
|
+
@refunded = !!refunded
|
20
|
+
|
21
|
+
if (currency.nil? || id.nil? || source.nil? || status.nil? || refunded.nil?)
|
22
|
+
raise Errors::IllegalArgumentError.new('One or more parameters are nil.')
|
23
|
+
elsif (amount < 0)
|
24
|
+
raise Errors::IllegalArgumentError.new('The amount can not be less than 0.')
|
25
|
+
elsif (currency.length != 3)
|
26
|
+
raise Errors::IllegalArgumentError.new('The passed currency is invalid.')
|
27
|
+
elsif (["pending", "succeeded", "failed"].index(status) < 0)
|
28
|
+
raise Errors::IllegalArgumentError.new("The passed state should be one of: 'pending', 'succeeded' or 'failed'.")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require_relative './SandboxObject'
|
2
|
+
|
3
|
+
module CloudRailSi
|
4
|
+
module Types
|
5
|
+
class CloudMetaData < SandboxObject
|
6
|
+
attr_accessor :folder, :name, :path, :size, :modified_at
|
7
|
+
|
8
|
+
def to_s
|
9
|
+
s = ""
|
10
|
+
s += "name -> #{@name}\n"
|
11
|
+
s += "path -> #{@path}\n"
|
12
|
+
s += "size -> #{@size}\n"
|
13
|
+
s += "folder -> #{@folder}\n"
|
14
|
+
s += "modified_at -> #{@modified_at}"
|
15
|
+
return s
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require_relative './SandboxObject'
|
2
|
+
require_relative './Address'
|
3
|
+
require_relative '../errors/DetailErrors'
|
4
|
+
require_relative './Types'
|
5
|
+
require_relative '../errors/InternalError'
|
6
|
+
require_relative '../helpers/Helper'
|
7
|
+
|
8
|
+
module CloudRailSi
|
9
|
+
module Types
|
10
|
+
class CreditCard < SandboxObject
|
11
|
+
include Comparable
|
12
|
+
|
13
|
+
attr_accessor :expire_month, :expire_year, :number, :type, :address, :firstName, :lastName, :cvc
|
14
|
+
|
15
|
+
def initialize(cvc, expire_month, expire_year, number, type, first_name, last_name, address)
|
16
|
+
super()
|
17
|
+
@cvc = cvc
|
18
|
+
@expire_month = expire_month
|
19
|
+
@expire_year = expire_year
|
20
|
+
@number = number
|
21
|
+
@type = type
|
22
|
+
@firstName = first_name
|
23
|
+
@lastName = last_name
|
24
|
+
@address = address
|
25
|
+
end
|
26
|
+
|
27
|
+
def expire_month=(value)
|
28
|
+
raise Errors::IllegalArgumentError.new('Expiration month shouldn''t be nil') if value.nil?
|
29
|
+
raise Errors::IllegalArgumentError.new('Expiration month needs to be between 1 and 12.') if (value <= 0 || value > 12)
|
30
|
+
@expire_month = value
|
31
|
+
end
|
32
|
+
|
33
|
+
def expire_year=(value)
|
34
|
+
raise Errors::IllegalArgumentError.new('Expiration year shouldn''t be nil') if (value.nil?)
|
35
|
+
raise Errors::IllegalArgumentError.new('Expiration year needs to be a four digit number.') if ((value < 1970) || (value.to_s.length != 4))
|
36
|
+
@expire_year = value
|
37
|
+
end
|
38
|
+
|
39
|
+
def number=(value)
|
40
|
+
raise Errors::IllegalArgumentError.new('Card number is not allowed to be nil.') if (value.nil?)
|
41
|
+
@number = value
|
42
|
+
end
|
43
|
+
|
44
|
+
def type=(value)
|
45
|
+
raise Errors::IllegalArgumentError.new('Card type is not allowed to be nil.') if (value.nil?)
|
46
|
+
raise Errors::IllegalArgumentError.new('Unknown card type. Allowed values are: ''visa'', ''mastercard'', ''discover'' or ''amex''.') if (['visa', 'mastercard', 'discover', 'amex'].index(value) < 0)
|
47
|
+
this._type = value
|
48
|
+
end
|
49
|
+
|
50
|
+
def <=> (obj)
|
51
|
+
raise Errors::InternalError.new('CreditCards must only be compared with other non-nil CreditCards') if (obj.nil? || !(obj.instance_of?(CreditCard)))
|
52
|
+
|
53
|
+
another = obj
|
54
|
+
|
55
|
+
compare = CloudRailSi::ServiceCode::Helper.compare(@firstName, another.firstName)
|
56
|
+
return compare if (compare)
|
57
|
+
|
58
|
+
compare = CloudRailSi::ServiceCode::Helper.compare(@lastName, another.lastName)
|
59
|
+
return compare if (compare)
|
60
|
+
|
61
|
+
compare = CloudRailSi::ServiceCode::Helper.compare(@number.substring[12..-1], another.number[12..-1])
|
62
|
+
return compare if (compare)
|
63
|
+
|
64
|
+
compare = CloudRailSi::ServiceCode::Helper.compare(@expire_month, another.expire_month)
|
65
|
+
return compare if (compare)
|
66
|
+
|
67
|
+
compare = CloudRailSi::ServiceCode::Helper.compare(@expire_year, another.expire_year)
|
68
|
+
return compare if (compare)
|
69
|
+
|
70
|
+
return CloudRailSi::ServiceCode::Helper.compare(@type, another.type)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require_relative './SandboxObject'
|
2
|
+
require_relative './Types'
|
3
|
+
require_relative '../errors/InternalError'
|
4
|
+
require 'date'
|
5
|
+
|
6
|
+
module CloudRailSi
|
7
|
+
module Types
|
8
|
+
class CustomDate < SandboxObject
|
9
|
+
include Comparable
|
10
|
+
|
11
|
+
attr_accessor :date
|
12
|
+
|
13
|
+
def initialize(date_string=nil)
|
14
|
+
@date = (!date_string.nil? && !date_string.empty?) ? DateTime.parse(date_string).to_time.utc.to_datetime : DateTime.now
|
15
|
+
end
|
16
|
+
|
17
|
+
def time
|
18
|
+
@date.to_time.to_i * 1000
|
19
|
+
end
|
20
|
+
|
21
|
+
def time=(value)
|
22
|
+
@date = Time.at(value / 1000).to_datetime
|
23
|
+
end
|
24
|
+
|
25
|
+
def rfc_time
|
26
|
+
@date.to_time.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
27
|
+
end
|
28
|
+
|
29
|
+
def <=> (obj)
|
30
|
+
raise Errors::InternalError.new("Comparing a Date with a non-Date") if (!(obj.instance_of?(CustomDate)))
|
31
|
+
|
32
|
+
return -1 if (time < obj.time)
|
33
|
+
return 1 if (obj.time < time)
|
34
|
+
return 0 if (time === obj.time)
|
35
|
+
|
36
|
+
raise Errors::InternalError.new("Comparing a Date with a non-Date")
|
37
|
+
end
|
38
|
+
|
39
|
+
class << self
|
40
|
+
def json_create(object)
|
41
|
+
byebug
|
42
|
+
cd = CustomDate.new
|
43
|
+
cd.time = Integer(object['time'])
|
44
|
+
return cd
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require_relative './SandboxObject'
|
2
|
+
|
3
|
+
module CloudRailSi
|
4
|
+
module Types
|
5
|
+
class Error < SandboxObject
|
6
|
+
attr_accessor :message, :type
|
7
|
+
|
8
|
+
def initialize(message, type)
|
9
|
+
updated_message = message.nil? ? nil : message.gsub('null', 'nil')
|
10
|
+
|
11
|
+
@message = updated_message
|
12
|
+
@type = type
|
13
|
+
super()
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
@message
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative './SandboxObject'
|
2
|
+
require_relative './Location'
|
3
|
+
|
4
|
+
module CloudRailSi
|
5
|
+
module Types
|
6
|
+
class POI < SandboxObject
|
7
|
+
def initialize(categories, image_url, location, name, phone)
|
8
|
+
super()
|
9
|
+
@categories = categories
|
10
|
+
@image_url = image_url
|
11
|
+
@location = location
|
12
|
+
@name = name
|
13
|
+
@phone = phone
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require_relative './SandboxObject'
|
2
|
+
require_relative '../errors/DetailErrors'
|
3
|
+
|
4
|
+
module CloudRailSi
|
5
|
+
module Types
|
6
|
+
class Refund < SandboxObject
|
7
|
+
attr_reader :id, :amount, :created, :currency, :charge_id, :state
|
8
|
+
|
9
|
+
def initialize(amount, charge_id, created, id, state, currency)
|
10
|
+
super()
|
11
|
+
@amount = amount
|
12
|
+
@charge_id = charge_id
|
13
|
+
@created = created
|
14
|
+
@id = id
|
15
|
+
@state = state
|
16
|
+
@currency = currency
|
17
|
+
raise Errors::IllegalArgumentError.new("At least one of the parameters is nil.") if (charge_id.nil? || id.nil? || state.nil?)
|
18
|
+
raise Errors::IllegalArgumentError.new("Unknown state. Allowed values are: 'succeeded', 'failed' or 'pending'.") if (["pending", "succeeded", "failed"].index(@state) < 0)
|
19
|
+
raise Errors::IllegalArgumentError.new("The passed currency is invalid.") if (@currency.length != 3)
|
20
|
+
|
21
|
+
@currency = @currency.upcase
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require_relative './SandboxObject'
|
2
|
+
require_relative '../helpers/Helper'
|
3
|
+
|
4
|
+
module CloudRailSi
|
5
|
+
module Types
|
6
|
+
class SandboxObject
|
7
|
+
def get(key)
|
8
|
+
entry = instance_variable_get("#{Helper.lowerCaseFirstLetter(key)}")
|
9
|
+
if (Helper.is_boolean(entry))
|
10
|
+
entry = entry ? 1 : 0
|
11
|
+
end
|
12
|
+
entry
|
13
|
+
end
|
14
|
+
|
15
|
+
def set(key, value)
|
16
|
+
instance_variable_set("#{Helper.lowerCaseFirstLetter(key)}", value)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|