snapsearch-client-ruby 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +83 -0
- data/LICENSE +20 -0
- data/README.md +109 -0
- data/Rakefile +20 -0
- data/VERSION +1 -0
- data/examples/rack/Gemfile +5 -0
- data/examples/rack/config.ru +88 -0
- data/examples/rack/public/index.html +15 -0
- data/examples/sinatra/Gemfile +5 -0
- data/examples/sinatra/Gemfile.lock +90 -0
- data/examples/sinatra/config.ru +15 -0
- data/examples/sinatra/lib/sinatra_snap_search.rb +19 -0
- data/examples/sinatra/public/index.html +15 -0
- data/lib/rack/snap_search.rb +143 -0
- data/lib/rack/snap_search/config.rb +85 -0
- data/lib/snap_search.rb +14 -0
- data/lib/snap_search/client.rb +147 -0
- data/lib/snap_search/connection_exception.rb +15 -0
- data/lib/snap_search/detector.rb +248 -0
- data/lib/snap_search/exception.rb +8 -0
- data/lib/snap_search/interceptor.rb +66 -0
- data/lib/snap_search/validation_exception.rb +17 -0
- data/resources/cacert.pem +3785 -0
- data/resources/extensions.json +26 -0
- data/resources/robots.json +208 -0
- data/snapsearch.gemspec +31 -0
- data/spec/lib/rack/qs_spec.rb +34 -0
- data/spec/lib/rack/snap_search/config_spec.rb +56 -0
- data/spec/lib/snap_search/detector_spec.rb +362 -0
- data/spec/lib/snap_search/interceptor_spec.rb +116 -0
- data/spec/spec_helper.rb +6 -0
- metadata +216 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
# Notes to run:
|
2
|
+
# gem install bundler
|
3
|
+
# bundle install
|
4
|
+
# rackup
|
5
|
+
#
|
6
|
+
# Testing:
|
7
|
+
# Visit http://localhost:9292/
|
8
|
+
# Visit http://localhost:9292/?_escaped_fragment_
|
9
|
+
|
10
|
+
require 'bundler/setup'
|
11
|
+
require 'pathname'
|
12
|
+
$:.unshift( Pathname.new(__FILE__).join('..', 'lib').expand_path.to_s )
|
13
|
+
require 'sinatra_snap_search'
|
14
|
+
|
15
|
+
run SinatraSnapSearch
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'sinatra/base'
|
3
|
+
require 'pathname'
|
4
|
+
require 'snap_search'
|
5
|
+
|
6
|
+
class SinatraSnapSearch < Sinatra::Base
|
7
|
+
|
8
|
+
configure do
|
9
|
+
set :root, SnapSearch.root.join('examples', 'sinatra')
|
10
|
+
enable :sessions, :logging, :method_override, :static
|
11
|
+
|
12
|
+
use Rack::SnapSearch, email: 'email', key: 'key'
|
13
|
+
end
|
14
|
+
|
15
|
+
get '/' do
|
16
|
+
redirect to('/index.html')
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang='en'>
|
3
|
+
<head>
|
4
|
+
<meta charset='utf-8'>
|
5
|
+
<title>SnapSearch Example</title>
|
6
|
+
<link href="/css/blah.css" media="all" rel="stylesheet" />
|
7
|
+
<!--[if lt IE 9]>
|
8
|
+
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
|
9
|
+
<![endif]-->
|
10
|
+
<!-- <script src="/js/blah.js"></script> -->
|
11
|
+
</head>
|
12
|
+
<body>
|
13
|
+
<h1>SnapSearch Example</h1>
|
14
|
+
</body>
|
15
|
+
</html>
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'snap_search'
|
2
|
+
require 'rack/snap_search/config'
|
3
|
+
|
4
|
+
module Rack
|
5
|
+
|
6
|
+
# Use to enable SnapSearch detection within your web application.
|
7
|
+
class SnapSearch
|
8
|
+
|
9
|
+
# Initialize the middleware.
|
10
|
+
#
|
11
|
+
# @param [#call] app The Rack application.
|
12
|
+
# @param [Hash, #to_h] options Options to configure this middleware with.
|
13
|
+
# @yield [Rack::SnapSearch::Config, SnapSearch::Detector] A block to further modify the middleware.
|
14
|
+
# @yieldparam [Rack::SnapSearch::Config] config Options to configure this middleware with, optionally preset with the `options` param.
|
15
|
+
# @yieldparam [SnapSearch::Detector] detector The instance of the Detector class which will be used for detecting whether requests are coming from a robot.
|
16
|
+
def initialize(app, options={}, &block)
|
17
|
+
raise TypeError, 'app must respond to #call' unless app.respond_to?(:call)
|
18
|
+
raise TypeError, 'options must be a Hash or respond to #to_h or #to_hash' unless options.is_a?(Hash) || options.respond_to?(:to_h) || options.respond_to?(:to_hash)
|
19
|
+
options = options.to_h rescue options.to_hash
|
20
|
+
|
21
|
+
@app = app
|
22
|
+
|
23
|
+
setup_config(options)
|
24
|
+
|
25
|
+
block.call(@config) if block_given?
|
26
|
+
|
27
|
+
setup_client
|
28
|
+
setup_detector
|
29
|
+
setup_interceptor
|
30
|
+
end
|
31
|
+
|
32
|
+
# Run the middleware.
|
33
|
+
#
|
34
|
+
# @param [Hash, to_h] app The Rack environment
|
35
|
+
def call(environment)
|
36
|
+
raise TypeError, 'environment must be a Hash or respond to #to_h or #to_hash' unless environment.is_a?(Hash) || environment.respond_to?(:to_h) || environment.respond_to?(:to_hash)
|
37
|
+
environment = environment.to_h rescue environment.to_hash
|
38
|
+
|
39
|
+
setup_x_forwarded_proto(environment) if @config.x_forwarded_proto
|
40
|
+
|
41
|
+
setup_api_response_from_environment(environment) # Will set @api_response if one is given from the API
|
42
|
+
|
43
|
+
if @api_response
|
44
|
+
setup_attributes_from_api_response
|
45
|
+
else
|
46
|
+
setup_attributes_from_app(environment)
|
47
|
+
end
|
48
|
+
|
49
|
+
@config.response_callback.nil? ? rack_response : @config.response_callback.call(*rack_response)
|
50
|
+
end
|
51
|
+
|
52
|
+
protected
|
53
|
+
|
54
|
+
# == Initialization Helpers
|
55
|
+
|
56
|
+
# Setup the Config instance from the given options.
|
57
|
+
def setup_config(options)
|
58
|
+
@config = Rack::SnapSearch::Config.new(options)
|
59
|
+
|
60
|
+
@config.x_forwarded_proto ||= true
|
61
|
+
end
|
62
|
+
|
63
|
+
# Setup the Client instance from the @config.
|
64
|
+
def setup_client
|
65
|
+
@client = ::SnapSearch::Client.new(
|
66
|
+
email: @config.email,
|
67
|
+
key: @config.key,
|
68
|
+
parameters: @config.parameters,
|
69
|
+
api_url: @config.api_url,
|
70
|
+
ca_cert_file: @config.ca_cert_file
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Setup the Detector instance from the @config.
|
75
|
+
def setup_detector
|
76
|
+
@detector = ::SnapSearch::Detector.new(
|
77
|
+
matched_routes: @config.matched_routes,
|
78
|
+
ignored_routes: @config.ignored_routes,
|
79
|
+
robots_json: @config.robots_json,
|
80
|
+
extensions_json: @config.extensions_json,
|
81
|
+
check_static_files: @config.check_static_files
|
82
|
+
)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Setup the Interceptor instance using the @client and @detector, then setup callbacks if needed.
|
86
|
+
def setup_interceptor
|
87
|
+
@interceptor = ::SnapSearch::Interceptor.new(@client, @detector)
|
88
|
+
|
89
|
+
@interceptor.before_intercept(&@config.before_intercept) unless @config.before_intercept.nil?
|
90
|
+
@interceptor.after_intercept(&@config.after_intercept) unless @config.before_intercept.nil?
|
91
|
+
end
|
92
|
+
|
93
|
+
# == Running Helpers
|
94
|
+
|
95
|
+
# Alter the environment if the X-FORWARDED-PROTO header is given.
|
96
|
+
def setup_x_forwarded_proto(environment)
|
97
|
+
# Check X-Forwarded-Proto because Heroku SSL Support terminates at the load balancer
|
98
|
+
if environment['X-FORWARDED-PROTO']
|
99
|
+
environment['HTTPS'] = true && environment['rack.url_scheme'] = 'https' && environment['SERVER_PORT'] = 443 if environment['X-FORWARDED-PROTO'] == 'https'
|
100
|
+
environment['HTTPS'] = false && environment['rack.url_scheme'] = 'http' && environment['SERVER_PORT'] = 80 if env['X-FORWARDED-PROTO'] == 'http'
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Intercept and return the response.
|
105
|
+
def setup_api_response_from_environment(environment)
|
106
|
+
begin
|
107
|
+
request = Rack::Request.new(environment.to_h)
|
108
|
+
@api_response = @interceptor.intercept(request: request)
|
109
|
+
rescue SnapSearch::Exception => exception
|
110
|
+
@config.on_exception.nil? ? raise(exception) : @config.on_exception.call(exception)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Setup the Location header in the response.
|
115
|
+
def setup_location_header
|
116
|
+
response_location_header = @api_response['headers'].find { |header| header['name'] == 'Location' }
|
117
|
+
|
118
|
+
@headers['Location'] = response_location_header['value'] unless response_location_header.nil?
|
119
|
+
end
|
120
|
+
|
121
|
+
# Setup the Status header and body in the response.
|
122
|
+
def setup_status_and_body
|
123
|
+
@status, @body = @api_response['status'], [ @api_response['html'] ] # Note that the body in the Rack response must be an interable containing Strings, we it's wrapped within an Array.
|
124
|
+
end
|
125
|
+
|
126
|
+
# Setup Location and Status headers, as well as teh body, if we got a response from SnapSearch.
|
127
|
+
def setup_attributes_from_api_response
|
128
|
+
setup_location_header
|
129
|
+
# TODO: Should setup_content_length_header?
|
130
|
+
setup_status_and_body
|
131
|
+
end
|
132
|
+
|
133
|
+
def setup_attributes_from_app(environment)
|
134
|
+
@status, @headers, @body = @app.call(environment)
|
135
|
+
end
|
136
|
+
|
137
|
+
def rack_response
|
138
|
+
[ @status, @headers, @body ]
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Rack
|
2
|
+
class SnapSearch
|
3
|
+
|
4
|
+
# The configuration class for the Rack middleware.
|
5
|
+
# Holds the attributes to initialize the Client, Interceptor, and Detector with.
|
6
|
+
class Config
|
7
|
+
|
8
|
+
ATTRIBUTES = [
|
9
|
+
:email, :key, :api_url, :ca_cert_file, :x_forwarded_proto, :parameters, # Client options
|
10
|
+
:matched_routes, :ignored_routes, :robots_json, :extensions_json, :check_static_files # Detector options
|
11
|
+
]
|
12
|
+
|
13
|
+
attr_accessor *ATTRIBUTES # Setup reader & writer instance methods for each attribute
|
14
|
+
|
15
|
+
# Create a new instance.
|
16
|
+
#
|
17
|
+
# @param [Hash] options The options to initialize this instance with.
|
18
|
+
# @option options [String] :email The email to authenticate with.
|
19
|
+
# @option options [String] :key The key to authenticate with.
|
20
|
+
# @option options [String] :api_url The API URL to send requests to.
|
21
|
+
# @option options [String] :ca_cert_file The CA Cert file to use when sending HTTPS requests to the API.
|
22
|
+
# @option options [String] :x_forwarded_proto Check X-Forwarded-Proto because Heroku SSL Support terminates at the load balancer
|
23
|
+
# @option options [String] :parameters Extra parameters to send to the API.
|
24
|
+
# @option options [String] :matched_routes Whitelisted routes. Should be an Array of Regexp instances.
|
25
|
+
# @option options [String] :ignored_routes Blacklisted routes. Should be an Array of Regexp instances.
|
26
|
+
# @option options [String] :robots_json A path of the JSON file containing the user agent whitelist & blacklist.
|
27
|
+
# @option options [String] :extensions_json A path to the JSON file containing a single Hash with the keys `ignore` and `match`. These keys contain Arrays of Strings (user agents)
|
28
|
+
# @option options [String] :check_static_files Set to `true` to ignore direct requests to files.
|
29
|
+
# @option options [Proc, #call] :on_exception The block to run when an exception within SnapSearch occurs.
|
30
|
+
# @option options [Proc, #call] :before_intercept A block to run before the interception of a bot.
|
31
|
+
# @option options [Proc, #call] :after_intercept A block to run after the interception of a bot.
|
32
|
+
# @option options [Proc, #call] :response_callback A block to manipulate the response from the SnapSearch API.
|
33
|
+
def initialize(options={})
|
34
|
+
raise TypeError, 'options must be a Hash or respond to #to_h' unless options.is_a?(Hash) || options.respond_to?(:to_h) || options.respond_to?(:to_hash)
|
35
|
+
options = options.to_h rescue options.to_hash
|
36
|
+
|
37
|
+
ATTRIBUTES.each do |attribute|
|
38
|
+
send( "#{attribute}=", options[attribute] ) if options.has_key?(attribute)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Getter/Setter for the `on_exception` attribute.
|
43
|
+
#
|
44
|
+
# @yield If given, the Proc or callable to set the attribute as.
|
45
|
+
# @return [Proc] The value of the attribute.
|
46
|
+
def on_exception(&block)
|
47
|
+
@on_exception = block if block_given?
|
48
|
+
|
49
|
+
@on_exception
|
50
|
+
end
|
51
|
+
|
52
|
+
# Getter/Setter for the `before_intercept` attribute on the Interceptor.
|
53
|
+
#
|
54
|
+
# @yield If given, the Proc or callable to set the attribute as.
|
55
|
+
# @return [Proc] The value of the attribute.
|
56
|
+
def before_intercept(&block)
|
57
|
+
@before_intercept = block if block_given?
|
58
|
+
|
59
|
+
@before_intercept
|
60
|
+
end
|
61
|
+
|
62
|
+
# Getter/Setter for the `after_intercept` attribute on the Interceptor.
|
63
|
+
#
|
64
|
+
# @yield If given, the Proc or callable to set the attribute as.
|
65
|
+
# @return [Proc] The value of the attribute.
|
66
|
+
def after_intercept(&block)
|
67
|
+
@after_intercept = block if block_given?
|
68
|
+
|
69
|
+
@after_intercept
|
70
|
+
end
|
71
|
+
|
72
|
+
# Getter/Setter for the `response_callback` attribute on the Interceptor.
|
73
|
+
#
|
74
|
+
# @yield If given, the Proc or callable to set the attribute as.
|
75
|
+
# @return [Proc] The value of the attribute.
|
76
|
+
def response_callback(&block)
|
77
|
+
@response_callback = block if block_given?
|
78
|
+
|
79
|
+
@response_callback
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
data/lib/snap_search.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module SnapSearch
|
4
|
+
|
5
|
+
def self.root
|
6
|
+
@root ||= Pathname.new(__FILE__).join('..', '..').expand_path
|
7
|
+
end
|
8
|
+
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'snap_search/client'
|
12
|
+
require 'snap_search/detector'
|
13
|
+
require 'snap_search/interceptor'
|
14
|
+
require 'rack/snap_search'
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'httpi'
|
3
|
+
require 'snap_search/connection_exception'
|
4
|
+
require 'snap_search/validation_exception'
|
5
|
+
|
6
|
+
module SnapSearch
|
7
|
+
|
8
|
+
# The Client sends an authenticated HTTP request to the SnapChat API and returns the `content`
|
9
|
+
# field from the JSON response body.
|
10
|
+
class Client
|
11
|
+
|
12
|
+
attr_reader :email, :key, :parameters, :api_url, :ca_cert_file
|
13
|
+
|
14
|
+
# Create a new Client instance.
|
15
|
+
#
|
16
|
+
# @param [Hash, #to_h] options The options to create the Client with.
|
17
|
+
# @option options [String, #to_s] :email The email to authenticate with.
|
18
|
+
# @option options [String, #to_s] :key The secret authentication key.
|
19
|
+
# @option options [Hash, #to_h] :parameters ({}) The parameters to send with the HTTP request.
|
20
|
+
# @option options [String, #to_s] :api_url (https://snapsearch.io/api/v1/robot) The URL to send the HTTP request to.
|
21
|
+
# @option options [String, #to_s] :ca_cert_file (ROOT/resources/cacert.pem) The CA cert file to use with request.
|
22
|
+
def initialize(options={})
|
23
|
+
initialize_attributes(options)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Validate and set the value of the `email` attribute.
|
27
|
+
#
|
28
|
+
# @param [String, #to_s] value The value to set the attribute to.
|
29
|
+
# @return [String] The new value of the attribute.
|
30
|
+
def email=(value)
|
31
|
+
raise TypeError, 'email must be a String or respond to #to_s' unless value.is_a?(String) || respond_to?(:to_s)
|
32
|
+
|
33
|
+
value = value.to_s
|
34
|
+
raise ArgumentError, 'email must be an email address' unless value.include?(?@)
|
35
|
+
|
36
|
+
@email = value
|
37
|
+
end
|
38
|
+
|
39
|
+
# Validate and set the value of the `key` attribute.
|
40
|
+
#
|
41
|
+
# @param [String, #to_s] value The value to set the attribute to.
|
42
|
+
# @return [String] The new value of the attribute.
|
43
|
+
def key=(value)
|
44
|
+
@key = value.nil? ? nil : value.to_s
|
45
|
+
end
|
46
|
+
|
47
|
+
# Validate and set the value of the `parameters` attribute.
|
48
|
+
#
|
49
|
+
# @param [Hash, #to_h] value The value to set the attribute to.
|
50
|
+
# @return [Hash] The new value of the attribute.
|
51
|
+
def parameters=(value)
|
52
|
+
raise TypeError, 'parameters must be a Hash or respond to #to_h or #to_hash' unless value.is_a?(Hash) || value.respond_to?(:to_h) || value.respond_to?(:to_hash)
|
53
|
+
value = value.to_h rescue value.to_hash
|
54
|
+
|
55
|
+
@parameters = value.to_h
|
56
|
+
end
|
57
|
+
|
58
|
+
# Validate and set the value of the `api_url` attribute.
|
59
|
+
#
|
60
|
+
# @param [String, #to_s] value The value to set the attribute to.
|
61
|
+
# @return [String] The new value of the attribute.
|
62
|
+
def api_url=(value)
|
63
|
+
raise TypeError, 'api_url must be a String or respond_to #to_s' unless value.is_a?(String) || value.respond_to?(:to_s)
|
64
|
+
|
65
|
+
@api_url = value.to_s
|
66
|
+
end
|
67
|
+
|
68
|
+
# Validate and set the value of the `ca_cert_file` attribute.
|
69
|
+
#
|
70
|
+
# @param [String, #to_s] value The value to set the attribute to.
|
71
|
+
# @return [String] The new value of the attribute.
|
72
|
+
def ca_cert_file=(value)
|
73
|
+
raise TypeError, 'ca_cert_file must be a String or respond_to #to_s' unless value.is_a?(String) || value.respond_to?(:to_s)
|
74
|
+
|
75
|
+
@ca_cert_file = value.to_s
|
76
|
+
end
|
77
|
+
|
78
|
+
# Send an authenticated HTTP request to the `api_url` and return the `content` field from the JSON response body.
|
79
|
+
#
|
80
|
+
# @param [String, #to_s] url The url to send in the parameters to the `api_url`.
|
81
|
+
# @return [String] The `content` field from the JSON response body.
|
82
|
+
def request(url)
|
83
|
+
raise TypeError, 'url must be a String or respond_to #to_s' unless url.is_a?(String) || url.respond_to?(:to_s)
|
84
|
+
@parameters['url'] = url.to_s # The URL must contain the entire URL with the _escaped_fragment_ parsed out
|
85
|
+
|
86
|
+
content_from_response(send_request)
|
87
|
+
end
|
88
|
+
|
89
|
+
protected
|
90
|
+
|
91
|
+
# Initialize this instance based on options passed.
|
92
|
+
#
|
93
|
+
# @param [Hash, #to_h] options The options to create the Client with.
|
94
|
+
# @option options [String, #to_s] :email The email to authenticate with.
|
95
|
+
# @option options [String, #to_s] :key The secret authentication key.
|
96
|
+
# @option options [Hash, #to_h] :parameters ({}) The parameters to send with the HTTP request.
|
97
|
+
# @option options [String, #to_s] :api_url (https://snapsearch.io/api/v1/robot) The URL to send the HTTP request to.
|
98
|
+
# @option options [String, #to_s] :ca_cert_file (ROOT/resources/cacert.pem) The CA cert file to use with request.
|
99
|
+
def initialize_attributes(options)
|
100
|
+
raise TypeError, 'options must be a Hash or respond to #to_h' unless options.is_a?(Hash) || options.respond_to?(:to_h) || options.respond_to?(:to_hash)
|
101
|
+
options = options.to_h rescue options.to_hash
|
102
|
+
|
103
|
+
options = {
|
104
|
+
parameters: {},
|
105
|
+
api_url: 'https://snapsearch.io/api/v1/robot',
|
106
|
+
ca_cert_file: SnapSearch.root.join('resources', 'cacert.pem')
|
107
|
+
}.merge(options.to_h)
|
108
|
+
|
109
|
+
self.email, self.key, self.parameters, self.api_url, self.ca_cert_file = options.values_at(:email, :key, :parameters, :api_url, :ca_cert_file)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Send an authenticated HTTP POST request encoded in JSON to the API URL
|
113
|
+
# using the HTTP client adapter of the developer's choice.
|
114
|
+
#
|
115
|
+
# @return [HTTPI::Response] The HTTP response object.
|
116
|
+
def send_request
|
117
|
+
request = HTTPI::Request.new
|
118
|
+
request.url = api_url
|
119
|
+
request.auth.basic(email, key)
|
120
|
+
request.auth.ssl.ca_cert_file = ca_cert_file
|
121
|
+
request.auth.ssl.verify_mode = :peer
|
122
|
+
request.open_timeout = 30
|
123
|
+
request.headers['Content-Type'] = 'application/json'
|
124
|
+
request.headers['Accept-Encoding'] = 'gzip, deflate, identity'
|
125
|
+
request.body = @parameters.to_json
|
126
|
+
|
127
|
+
HTTPI.post(request)
|
128
|
+
rescue
|
129
|
+
raise ConnectionException
|
130
|
+
end
|
131
|
+
|
132
|
+
# Retrieve the `content` or raise an error based on the `code` field in the JSON response.
|
133
|
+
#
|
134
|
+
# @return [HTTPI::Response] The HTTP response object.
|
135
|
+
def content_from_response(response)
|
136
|
+
body = JSON.parse(response.body)
|
137
|
+
|
138
|
+
case body['code']
|
139
|
+
when 'success' then body['content']
|
140
|
+
when 'validation_error' then raise( ValidationException, body['content'] )
|
141
|
+
else; false # System error on SnapSearch; Nothing we can do # TODO: Raise exception?
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|