telesign_lib 0.0.12

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7d531bc3040fa7c7d87a0d1c24c8e77cedf6c7bb
4
+ data.tar.gz: 2f0da94e24422ebaf88cf844bba37a842ba84810
5
+ SHA512:
6
+ metadata.gz: cb9380ab2921e864af25747cb6ac777bf7c23ba98308b4757a2eaebd98b7cc12fd307c32b2ad1a02fb935fb69d22c89e844d349c60e6d34e207a54c7406e5c41
7
+ data.tar.gz: 5109a12a692dbb580ca62eaaf1b8fa2d5c38fb9580ab9db76bbb53520945f9c8c2478fc2d5d3e873576ef6702a9a31f2aca28e9079a304b731fa7520e1a71b38
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+
15
+ # YARD artifacts
16
+ .yardoc
17
+ _yardoc
18
+ doc/
19
+ .ruby-version
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in telesign.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,65 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ telesign_lib (0.0.12)
5
+ faraday (~> 0.9.1)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ addressable (2.3.7)
11
+ coderay (1.1.0)
12
+ crack (0.4.2)
13
+ safe_yaml (~> 1.0.0)
14
+ faraday (0.9.1)
15
+ multipart-post (>= 1.2, < 3)
16
+ ffi (1.9.8-java)
17
+ json (1.8.1)
18
+ json (1.8.1-java)
19
+ method_source (0.8.2)
20
+ mimic (0.4.3)
21
+ json
22
+ plist
23
+ rack
24
+ sinatra
25
+ minitest (5.5.1)
26
+ multipart-post (2.0.0)
27
+ plist (3.1.0)
28
+ pry (0.10.1)
29
+ coderay (~> 1.1.0)
30
+ method_source (~> 0.8.1)
31
+ slop (~> 3.4)
32
+ pry (0.10.1-java)
33
+ coderay (~> 1.1.0)
34
+ method_source (~> 0.8.1)
35
+ slop (~> 3.4)
36
+ spoon (~> 0.0)
37
+ rack (1.5.2)
38
+ rack-protection (1.5.2)
39
+ rack
40
+ rake (10.4.2)
41
+ safe_yaml (1.0.4)
42
+ sinatra (1.4.4)
43
+ rack (~> 1.4)
44
+ rack-protection (~> 1.4)
45
+ tilt (~> 1.3, >= 1.3.4)
46
+ slop (3.6.0)
47
+ spoon (0.0.4)
48
+ ffi
49
+ tilt (1.4.1)
50
+ webmock (1.20.4)
51
+ addressable (>= 2.3.6)
52
+ crack (>= 0.3.2)
53
+
54
+ PLATFORMS
55
+ java
56
+ ruby
57
+
58
+ DEPENDENCIES
59
+ bundler (~> 1.3)
60
+ mimic (~> 0.4.3)
61
+ minitest (~> 5.5.1)
62
+ pry (~> 0.10.1)
63
+ rake (~> 10.4.2)
64
+ telesign_lib!
65
+ webmock (~> 1.20.4)
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 TeleSign Corp.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,104 @@
1
+ telesign
2
+ =============
3
+
4
+ Ruby TeleSign SDK
5
+
6
+ example
7
+ =============
8
+ ```ruby
9
+ require 'telesign'
10
+
11
+ customer_id = 'FFFFFFFF-EEEE-DDDD-1234-AB1234567890'
12
+ secret_key = 'EXAMPLE----TE8sTgg45yusumoN6BYsBVkh+yRJ5czgsnCehZaOYldPJdmFh6NeX8kunZ2zU1YWaUw/0wV6xfw=='
13
+ phone_number = '4445551212'
14
+
15
+ require 'TeleSign'
16
+
17
+ ta = TeleSign::Api.new customer_id: customer_id,
18
+ secret_key: secret_key
19
+
20
+ phone_info = ta.phone_id.standard phone_number
21
+
22
+ p "##################"
23
+ p phone_info.data
24
+ p phone_info.headers
25
+ p phone_info.status
26
+ p phone_info.raw_data
27
+ p "##################"
28
+
29
+ phone_info = ta.phone_id.contact phone_number, 'PWRT'
30
+
31
+ p "##################"
32
+ p phone_info.data
33
+ p phone_info.headers
34
+ p phone_info.status
35
+ p phone_info.raw_data
36
+ p "##################"
37
+
38
+ phone_info = ta.phone_id.score phone_number, 'PWRT'
39
+
40
+ p "##################"
41
+ p phone_info.data
42
+ p phone_info.headers
43
+ p phone_info.status
44
+ p phone_info.raw_data
45
+ p "##################"
46
+
47
+ begin
48
+ phone_info = ta.phone_id.live phone_number, 'RXPF'
49
+ rescue TeleSign::AuthorizationError => e
50
+ puts e.message
51
+ end
52
+
53
+ p "##################"
54
+ p phone_info.data
55
+ p phone_info.headers
56
+ p phone_info.status
57
+ p phone_info.raw_data
58
+ p "##################"
59
+
60
+ phone_info = ta.verify.sms phone_number: phone_number #, verify_code: '12345'
61
+
62
+ p "##################"
63
+ p phone_info.data
64
+ p phone_info.headers
65
+ p phone_info.status
66
+ p phone_info.raw_data
67
+ p phone_info.verify_code
68
+ p "##################"
69
+
70
+ status_info = ta.verify.status phone_info.data['reference_id'], phone_info.verify_code
71
+ # status_info = ta.verify.status phone_info.data['reference_id'], '12345'
72
+
73
+ p "\n\n\n"
74
+ p "##################"
75
+ p status_info.data
76
+ p status_info.headers
77
+ p status_info.status
78
+ p status_info.raw_data
79
+ p status_info.verify_code
80
+ p "##################"
81
+ ```
82
+
83
+ stubs mode
84
+ =============
85
+ If you're running this in a Rails app, there is a stubs mode Sinatra app available.
86
+
87
+ Enable it by setting ENV['TELESIGN_STUBBED'] to something truthy.
88
+
89
+ The Sinatra app will run on "http://localhost:11989".
90
+
91
+ Currently stubs mode supports:
92
+
93
+ post '/v1/verify/sms'
94
+ post '/v1/verify/call'
95
+
96
+ Both of which always return a success response.
97
+
98
+ get '/v1/verify/:reference_id'
99
+
100
+ Returns VALID for any verify_code which does not contain '666'.
101
+
102
+ get '/v1/phoneid/standard/:phone_number'
103
+
104
+ See codes for triggering different phone-type responses.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new(:test) do |t|
6
+ t.libs << 'test'
7
+ t.pattern = "test/**/*_test.rb"
8
+ end
9
+
10
+ task default: :test
@@ -0,0 +1,52 @@
1
+ require 'faraday'
2
+ require 'json'
3
+
4
+ module TeleSign
5
+ class Api
6
+ # * - Attributes
7
+ # -
8
+ # * - `customer_id`
9
+ # - A string value that identifies your TeleSign account.
10
+ # * - `secret_key`
11
+ # - A base64-encoded string value that validates your access to the TeleSign web services.
12
+ # * - `ssl`
13
+ # - Specifies whether to use a secure connection with the TeleSign server. Defaults to *true*.
14
+ # * - `api_host`
15
+ # - The Internet host used in the base URI for REST web services.
16
+ # The default is *rest.telesign.com* (and the base URI is https://rest.telesign.com/).
17
+ # * - `proxy_host`
18
+ # - The host and port when going through a proxy server. ex: "localhost:8080. The default to no proxy.
19
+
20
+ # NOTE
21
+ # You can obtain both your Customer ID and Secret Key from the
22
+ # TeleSign Customer Portal <https://portal.telesign.com/account_profile_api_auth.php>
23
+
24
+ attr_accessor :verify, :phone_id
25
+
26
+ def initialize opts = {}
27
+ @customer_id = opts[:customer_id]
28
+ @secret_key = opts[:secret_key]
29
+ api_host = opts[:api_host] || 'rest.telesign.com'
30
+ ssl = opts[:ssl].nil? ? true : opts[:ssl]
31
+ proxy_host = opts[:proxy_host] || nil
32
+
33
+ http_root = ssl ? 'https' : 'http'
34
+ proxy = proxy_host ? "#{http_root}://#{proxy_host}" : nil
35
+ url = "#{http_root}://#{api_host}"
36
+
37
+ @conn = Faraday.new(url: url) do |faraday|
38
+ faraday.request :url_encoded
39
+ if defined? Rails
40
+ faraday.response :logger, Rails.logger
41
+ else
42
+ faraday.response :logger # log requests to STDOUT
43
+ end
44
+ faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
45
+ end
46
+
47
+ @verify = Verify.new(conn: @conn, customer_id: opts[:customer_id], secret_key: opts[:secret_key])
48
+ @phone_id = PhoneId.new(conn: @conn, customer_id: opts[:customer_id], secret_key: opts[:secret_key])
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,67 @@
1
+ # Authorization header definitions
2
+
3
+ require 'securerandom'
4
+ require 'base64'
5
+ require 'uri'
6
+ require 'openssl'
7
+
8
+ # NOTE: the following is the TeleSign crew who made the python version
9
+ # from which this codes was inspired
10
+
11
+ # __author__ = "Jeremy Cunningham, Michael Fox, and Radu Maierean"
12
+ # __copyright__ = "Copyright 2012, TeleSign Corp."
13
+ # __credits__ = ["Jeremy Cunningham", "Radu Maierean", "Michael Fox", "Nancy Vitug", "Humberto Morales"]
14
+ # __license__ = "MIT"
15
+
16
+ AUTH_METHOD = {
17
+ sha1: {hash: OpenSSL::Digest::SHA1, name: 'HMAC-SHA1'},
18
+ sha256: {hash: OpenSSL::Digest::SHA256, name: 'HMAC-SHA256'}
19
+ }
20
+
21
+ module TeleSign
22
+ class Auth
23
+ def self.generate_auth_headers(opts = {})
24
+ content_type = opts[:content_type] ? opts[:content_type] : ''
25
+ auth_method = opts[:auth_method] ? opts[:auth_method] : :sha1
26
+ fields = opts[:fields] ? opts[:fields] : nil
27
+
28
+ customer_id = opts[:customer_id]
29
+ secret_key = opts[:secret_key]
30
+ resource = opts[:resource]
31
+ method = opts[:method]
32
+
33
+ current_date = Time.now.strftime("%a, %d %b %Y %H:%M:%S %z")
34
+ nonce = SecureRandom.uuid
35
+
36
+ if %w(POST PUT).include? method
37
+ content_type = "application/x-www-form-urlencoded"
38
+ end
39
+
40
+ string_to_sign = "%s\n%s\n\nx-ts-auth-method:%s\nx-ts-date:%s\nx-ts-nonce:%s" % [
41
+ method,
42
+ content_type,
43
+ AUTH_METHOD[auth_method][:name],
44
+ current_date,
45
+ nonce]
46
+
47
+ if fields
48
+ string_to_sign += "\n%s" % URI.encode_www_form(fields)
49
+ end
50
+
51
+ string_to_sign += "\n%s" % resource
52
+
53
+ digest = AUTH_METHOD[auth_method][:hash].new
54
+ signer = OpenSSL::HMAC.digest digest, Base64.decode64(secret_key), string_to_sign
55
+
56
+ signature = Base64.encode64 signer
57
+
58
+ {
59
+ 'Authorization' => "TSA %s:%s" % [customer_id, signature],
60
+ 'x-ts-date' => current_date,
61
+ 'x-ts-auth-method' => AUTH_METHOD[auth_method][:name],
62
+ 'x-ts-nonce' => nonce
63
+ }
64
+ end
65
+ end
66
+ end
67
+
@@ -0,0 +1,19 @@
1
+ module TeleSign
2
+ class AuthorizationError < TeleSignError
3
+ # """
4
+ # Either the client failed to authenticate with the REST API server, or the service cannot be executed using the specified credentials.
5
+
6
+ # * - Attributes
7
+ # -
8
+ # * - `data`
9
+ # - The data returned by the service, in a dictionary form.
10
+ # * - `http_response`
11
+ # - The full HTTP Response object, including the HTTP status code, headers, and raw returned data.
12
+
13
+ # """
14
+
15
+ def initialize errors, http_response
16
+ super
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ module TeleSign
2
+ module Helpers
3
+ def random_with_N_digits n
4
+ range_start = 10 ** (n - 1)
5
+ range_end = (10 ** n) - 1
6
+ Random.new.rand(range_start...range_end)
7
+ end
8
+
9
+ def validate_response response
10
+ resp_obj = JSON.load response.body
11
+ if response.status != 200
12
+ if response.status == 401
13
+ raise AuthorizationError.new resp_obj, response
14
+ else
15
+ raise TeleSignError.new resp_obj, response
16
+ end
17
+ end
18
+
19
+ resp_obj
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,215 @@
1
+ if defined? Rails
2
+ Rails.logger.info '***************************'
3
+ Rails.logger.info 'Starting fake telesign server'
4
+ Rails.logger.info '***************************'
5
+ end
6
+
7
+ require 'mimic'
8
+ require 'json'
9
+ require 'securerandom'
10
+
11
+ Mimic.mimic port: (ENV['TELESIGN_PORT'].to_i || 11989), host: (ENV['TELESIGN_URL'] || 'localhost'), fork: false do
12
+ # 'https://rest.telesign.com/v1/verify/sms'
13
+ # params['phone_number']
14
+ # params['language']
15
+ # params['verify_code']
16
+ # params['template']
17
+ post '/v1/verify/sms' do
18
+ http_status = 200
19
+ headers = StdHelpers.standard_headers
20
+
21
+ response_body = StdHelpers.std_response_body StdHelpers.std_reference_id
22
+
23
+ content_type :json
24
+ [http_status, headers, response_body.to_json]
25
+ end
26
+
27
+ # 'https://rest.telesign.com/v1/verify/call'
28
+ # params['phone_number']
29
+ # params['language']
30
+ # params['verify_code']
31
+ post '/v1/verify/call' do
32
+ http_status = 200
33
+ headers = StdHelpers.standard_headers
34
+ response_body = StdHelpers.std_response_body StdHelpers.std_reference_id
35
+
36
+ content_type :json
37
+ [http_status, headers, response_body.to_json]
38
+ end
39
+
40
+ # 'https://rest.telesign.com/v1/verify/%s' % @expected_ref_id
41
+ # params[:reference_id]
42
+ # params[:verify_code]
43
+ get '/v1/verify/:reference_id' do
44
+ http_status = 200
45
+ headers = StdHelpers.standard_headers
46
+
47
+ status_code = if params[:verify_code] =~ /666/
48
+ 'INVALID'
49
+ else
50
+ 'VALID'
51
+ end
52
+
53
+ response_body = StdHelpers.std_response_body params[:reference_id], status_code
54
+
55
+ content_type :json
56
+ [http_status, headers, response_body.to_json]
57
+ end
58
+
59
+ # not using phoneid calls yet, but here are the stubs
60
+ # # 'https://rest.telesign.com/v1/phoneid/%s/%s'
61
+
62
+ # '/v1/phoneid/standard/%s' % phone_number
63
+ get '/v1/phoneid/standard/:phone_number' do
64
+ http_status = 200
65
+ headers = StdHelpers.standard_headers
66
+
67
+ phone_type_hash = if params[:phone_number] =~ /111/
68
+ {'code' =>'1', 'description' =>'FIXED'}
69
+ elsif params[:phone_number] =~ /222/
70
+ {'code' =>'2', 'description' =>'MOBILE'}
71
+ elsif params[:phone_number] =~ /333/
72
+ {'code' =>'3', 'description' =>'PREPAID'}
73
+ elsif params[:phone_number] =~ /444/
74
+ {'code' =>'4', 'description' =>'TOLLFREE'}
75
+ elsif params[:phone_number] =~ /555/ # this could be problematic
76
+ {'code' =>'5', 'description' =>'VOIP'}
77
+ elsif params[:phone_number] =~ /666/
78
+ {'code' =>'6', 'description' =>'PAGER'}
79
+ elsif params[:phone_number] =~ /777/
80
+ {'code' =>'7', 'description' =>'PAYPHONE'}
81
+ elsif params[:phone_number] =~ /888/
82
+ {'code' =>'8', 'description' =>'INVALID'}
83
+ elsif params[:phone_number] =~ /999/
84
+ {'code' =>'9', 'description' =>'RESTRICTED'}
85
+ elsif params[:phone_number] =~ /101010/
86
+ {'code' =>'10', 'description' =>'PERSONAL'}
87
+ elsif params[:phone_number] =~ /110110/
88
+ {'code' =>'11', 'description' =>'VOICEMAIL'}
89
+ else
90
+ {'code' =>'20', 'description' =>'OTHER'}
91
+ end
92
+
93
+ response_body = StdHelpers.std_standard_body params[:phone_number], phone_type_hash
94
+
95
+ content_type :json
96
+ [http_status, headers, response_body.to_json]
97
+ end
98
+
99
+ # # /v1/phoneid/score/%s' % phone_number
100
+ # get '/v1/phoneid/score/:phone_number' do
101
+
102
+ # end
103
+
104
+ # # /v1/phoneid/contact/%s' % phone_number
105
+ # get '/v1/phoneid/contact/:phone_number' do
106
+
107
+ # end
108
+
109
+ # # /v1/phoneid/live/%s' % phone_number
110
+ # get '/v1/phoneid/live/:phone_number' do
111
+
112
+ # end
113
+ end
114
+
115
+ BEGIN {
116
+ class StdHelpers
117
+ class << self
118
+ def standard_headers
119
+ # scraped from TeleSign api response
120
+ { 'date' =>Time.now.strftime('%a, %d %b %Y %H:%M:%S %z'),
121
+ 'server' =>'Apache',
122
+ 'allow' =>'GET,POST,HEAD',
123
+ 'connection' =>'close',
124
+ 'content-type' =>'application/json'}
125
+ end
126
+
127
+ def std_response_body reference_id, status_code = 'UNKNOWN'
128
+ # scraped from TeleSign api response
129
+ { 'reference_id' => reference_id,
130
+ 'resource_uri' => '/v1/verify/#{reference_id}',
131
+ 'sub_resource' => 'sms',
132
+ 'errors' => [],
133
+ 'status' => {
134
+ 'updated_on' => standard_updated_on,
135
+ 'code' => 290,
136
+ 'description' => 'Message in progress'
137
+ },
138
+ 'verify' => {
139
+ 'code_state' => status_code,
140
+ 'code_entered' => ''
141
+ }
142
+ }
143
+ end
144
+
145
+ def std_standard_body phone_number, phone_type_hash
146
+ simple_phone = phone_number[0]=='1' ? phone_number.slice(1..-1) : phone_number
147
+ { 'reference_id' => std_reference_id,
148
+ 'resource_uri' => nil,
149
+ 'sub_resource' => 'standard',
150
+ 'status' => {
151
+ 'updated_on' => standard_updated_on,
152
+ 'code' => 300,
153
+ 'description' => 'Transaction successfully completed'
154
+ },
155
+ 'errors' => [],
156
+ 'location' => {
157
+ 'city' => 'Reno',
158
+ 'state' => 'NV',
159
+ 'zip' => '89501',
160
+ 'metro_code' => '6720',
161
+ 'county' => '',
162
+ 'country' => {
163
+ 'name' => 'United States',
164
+ 'iso2' => 'US',
165
+ 'iso3' => 'USA'
166
+ },
167
+ 'coordinates' => {
168
+ 'latitude' => 39.52598,
169
+ 'longitude' => -119.80796
170
+ },
171
+ 'time_zone' => {
172
+ 'name' =>' America/Los_Angeles',
173
+ 'utc_offset_min' => '-8',
174
+ 'utc_offset_max' => '-8'
175
+ }
176
+ },
177
+ 'numbering' => {
178
+ 'original' => {
179
+ 'complete_phone_number' => '1'+simple_phone,
180
+ 'country_code' => '1',
181
+ 'phone_number' => simple_phone
182
+ },
183
+ 'cleansing' => {
184
+ 'call' => {
185
+ 'country_code' => '1',
186
+ 'phone_number' => simple_phone,
187
+ 'cleansed_code' => 100,
188
+ 'min_length' => 10,
189
+ 'max_length' => 10},
190
+ 'sms' => {
191
+ 'country_code' => '1',
192
+ 'phone_number' => simple_phone,
193
+ 'cleansed_code' => 100,
194
+ 'min_length' => 10,
195
+ 'max_length' => 10
196
+ }
197
+ }
198
+ },
199
+ 'phone_type' => phone_type_hash,
200
+ 'carrier' => {
201
+ 'name' => 'AT&T - PSTN'
202
+ }
203
+ }
204
+ end
205
+
206
+ def std_reference_id
207
+ SecureRandom.uuid.gsub('-','').upcase
208
+ end
209
+
210
+ def standard_updated_on
211
+ Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.%6N')
212
+ end
213
+ end
214
+ end
215
+ }
@@ -0,0 +1,9 @@
1
+ module TeleSign
2
+ module MockService
3
+ class Railtie < ::Rails::Railtie
4
+ initializer 'telesign_railtie.configure_rails_initialization' do
5
+ require 'telesign/mock_service/spin_up'
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,18 @@
1
+ module TeleSign
2
+ module MockService
3
+ module SpinUp
4
+ if ENV['TELESIGN_STUBBED']
5
+ port = (ENV['TELESIGN_PORT'].to_i || 11989)
6
+ if `lsof -i :#{port}`.blank? # no process running on #port
7
+ Process.detach(pid = Process.fork do
8
+ require 'telesign/mock_service/fake_server'
9
+ end)
10
+ else
11
+ Rails.logger.warn "TELESIGN STUB MODE FAILED TO START\nProcess already listening on #{port}"
12
+ end
13
+ end
14
+
15
+ at_exit { pid && `kill #{pid}` }
16
+ end
17
+ end
18
+ end