hypertrack_v3 1.0.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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/hypertrack_v3.rb +262 -0
  3. metadata +72 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 74901d0ab99d98deb111dd1eb7352c51de27b953af93dc3bd9d839450fa8688e
4
+ data.tar.gz: bc8660120b533585ffedd5427709338d63c21d0db0a3587eedcf120a8e79ff8f
5
+ SHA512:
6
+ metadata.gz: 7cdfb4528c8ebcdbbf53b1397b75f2235e2d9019ddb126a863d847b44bb8c0c5a50e4e74c792c1dc9dbf94b97b51e520c2d89073f17937058feb7da4e50ebd6a
7
+ data.tar.gz: 90426a3d4a25d3f9ac401dadd05fcdaade615c1c88ab5220996d0b4b6f1602b3c378c6047078b0ec534b9d8bad89551ff2c49a7149d534f1597169c8b10ae8ff
@@ -0,0 +1,262 @@
1
+ # frozen_string_literal: true
2
+
3
+ class HypertrackV3
4
+ BASE_URL='https://v3.api.hypertrack.com'
5
+
6
+ RES_DEVICES = '/devices'
7
+ RES_DEVICE = "/devices/%{device_id}"
8
+ RES_TRIPS = "/trips"
9
+ RES_TRIP = "/trips/%{trip_id}"
10
+ RES_TRIP_COMPLETE = "/trips/%{trip_id}/complete"
11
+
12
+ class HttpError < StandardError
13
+ attr_reader :code
14
+ attr_reader :message
15
+
16
+ def initialize(code, message)
17
+ @code = code
18
+ @message = message
19
+ end
20
+ end
21
+
22
+ class InternalServerError < HttpError; end
23
+ class ClientError < HttpError; end
24
+
25
+ class RegisterHookError < StandardError; end
26
+
27
+ def initialize(account_id, secret_key)
28
+ @client = nil
29
+ @account_id = account_id
30
+ @secret_key = secret_key
31
+ end
32
+
33
+ def client
34
+ @client ||= Faraday.new url: self.class::BASE_URL do |conn|
35
+ conn.basic_auth(@account_id, @secret_key)
36
+ conn.request :json
37
+ conn.response :json, :content_type => /\bjson$/
38
+ conn.response :json, :parser_options => { :object_class => OpenStruct }
39
+ conn.use Faraday::Response::Logger, HypertrackV3.logger, bodies: true
40
+ conn.use :instrumentation
41
+ conn.adapter Faraday.default_adapter
42
+ end
43
+ end
44
+
45
+ def parse(res)
46
+ raise InternalServerError.new(res.status, res.body) if res.status >= 500
47
+ raise ClientError.new(res.status, res.body) if res.status >= 400
48
+ raise HttpError.new(res.status, res.body) unless res.success?
49
+ res.body
50
+ end
51
+
52
+ def device_list
53
+ parse(client.get(self.class::RES_DEVICES))
54
+ end
55
+
56
+ def device_get(id:, **)
57
+ parse(client.get(self.class::RES_DEVICE % {device_id: id}))
58
+ end
59
+
60
+ def device_del(id:, **)
61
+ parse(client.delete(self.class::RES_DEVICE % {device_id: id}))
62
+ end
63
+
64
+ def trip_create(
65
+ device_id:,
66
+ destination:,
67
+ geofences:,
68
+ metadata:, **)
69
+ parse(
70
+ client.post(self.class::RES_TRIPS) do |req|
71
+ req.body = {
72
+ device_id: device_id,
73
+ destination: destination,
74
+ geofences: geofences,
75
+ metadata: metadata,
76
+ }
77
+ end
78
+ )
79
+ end
80
+
81
+ def trip_list(limit=50, offset=0)
82
+ parse(client.get(self.class::RES_TRIPS, params={limit: limit, offset: offset}))
83
+ end
84
+
85
+ def trip_get(id:, **)
86
+ parse(client.get(self.class::RES_TRIP % {trip_id: id}))
87
+ end
88
+
89
+ def trip_set_complete(id:, **)
90
+ parse(client.post(self.class::RES_TRIP_COMPLETE % {trip_id: id}))
91
+ end
92
+
93
+ def self.logger
94
+ @@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT)
95
+ end
96
+
97
+ def self.logger=(logger)
98
+ @@logger = logger
99
+ end
100
+
101
+ def self.error_handler
102
+ @@error_handler ||= ->(*, **) { nil }
103
+ end
104
+
105
+ def self.error_handler=(error_handler)
106
+ @@error_handler = error_handler
107
+ end
108
+
109
+ def self.exception_handler
110
+ @@error_handler ||= ->(*, **) { nil }
111
+ end
112
+
113
+ def self.exception_handler=(error_handler)
114
+ @@error_handler = error_handler
115
+ end
116
+
117
+ def self.log_error(message, **data)
118
+ self.logger.error({message: message}.merge(data))
119
+ self.error_handler.(message, **data)
120
+ end
121
+
122
+ def self.log_exception(exception, **data)
123
+ self.logger.error({exception: exception.as_json}.merge(data))
124
+ self.exception_handler.(exception, **data)
125
+ end
126
+ end
127
+
128
+ if defined? Rails
129
+ class HypertrackV3::Engine < Rails::Engine
130
+ class WebhookParser
131
+ class LogHook
132
+ def self.call(type, device_id, data, created_at, recorded_at)
133
+ HypertrackV3.logger.debug("HypertrackV3::LogHook#{type}: #{device_id} -> #{data}")
134
+ end
135
+ end
136
+
137
+ def self.client
138
+ @@client ||= Faraday.new do |conn|
139
+ conn.use Faraday::Response::Logger, HypertrackV3.logger, bodies: true
140
+ conn.use :instrumentation
141
+ conn.adapter Faraday.default_adapter
142
+ end
143
+ end
144
+
145
+ @@client = nil
146
+
147
+ # Sets up default hooks
148
+ @@hooks = {
149
+ location: ->(*args) { LogHook.('Location', *args) },
150
+ device_status: ->(*args) { LogHook.('DeviceStatus', *args) },
151
+ battery: ->(*args) { LogHook.('Battery', *args) },
152
+ trip: ->(*args) { LogHook.('Trip', *args) },
153
+ }
154
+
155
+ def self.register_with_hypertrack(request)
156
+ register_data = JSON.parse(request.body)
157
+
158
+ resp = self.client.get register_data["SubscribeURL"]
159
+ data = Nokogiri::XML(resp.body)
160
+ data.remove_namespaces!
161
+ token = data.at_xpath('//SubscriptionArn')&.content
162
+ return self.serve(400, {error: 'SubscriptionArn not found'}) if token.empty?
163
+ Rails.cache.write('/hypertrack_v3/subscription_arn', token, expires_in: 100.years)
164
+
165
+ self.serve(200)
166
+ end
167
+
168
+ def self.dispatch(request)
169
+ if (cached = Rails.cache.fetch('/hypertrack_v3/subscription_arn')) != request.headers['x_amz_sns_subscription_arn']
170
+ HypertrackV3.log_error(
171
+ "invalid subscription-arn header",
172
+ {
173
+ subscription_arn: {
174
+ request: request.header,
175
+ cache: cached,
176
+ }
177
+ }.to_json
178
+ )
179
+ return self.serve(400, {error: "invalid subscription-arn header"})
180
+ end
181
+ if (cached = Rails.cache.fetch("/hypertrack_v3/#{request.headers['x_amz_sns_message_id']}")).present?
182
+ HypertrackV3.log_error(
183
+ "Message Id already seen",
184
+ {
185
+ sns_message_id: {
186
+ request: request.headers['x_amz_sns_message_id'],
187
+ cache: cached,
188
+ }
189
+ }
190
+ )
191
+ return self.serve(400)
192
+ end
193
+ Rails.cache.write(
194
+ "/hypertrack_v3/#{request.headers['x_amz_sns_message_id']}", true,
195
+ expires_in: 1.hour
196
+ )
197
+
198
+ begin
199
+ data = JSON.parse(request.body, object_class: OpenStruct)
200
+ rescue JSON::ParserError => err
201
+ HypertrackV3.log_exception(err)
202
+ end
203
+
204
+ res = true
205
+ data.each do |datum|
206
+ if @@hooks.include? datum.type.to_sym
207
+ res &= @@hooks[datum.type.to_sym].(datum.device_id, datum.data, datum.created_at, datum.recorded_at)
208
+ else
209
+ res = false
210
+ end
211
+ end
212
+
213
+ self.serve(res ? 200 : 400)
214
+ end
215
+
216
+ def self.register_hook(name, callable)
217
+ raise RegisterHookError("Invalid name argument: #{name}") unless @@hooks.keys.include? name.to_sym
218
+ raise RegisterHookError("Invalid callable argument: #{callable}") unless callable.respond_to? :call
219
+ @@hooks[name.to_sym] = callable
220
+ end
221
+
222
+ def self.call(env)
223
+ request = parse_request(env)
224
+
225
+ case request.headers['x_amz_sns_message_type']
226
+ when 'SubscriptionConfirmation'
227
+ self.register_with_hypertrack request
228
+ when 'Notification'
229
+ self.dispatch request
230
+ else
231
+ HypertrackV3.log_error(
232
+ "invalid message-type header",
233
+ {
234
+ message_type: request.header['x_amz_sns_messate_type'],
235
+ }.to_json
236
+ )
237
+ self.serve(400, {error: "invalid message-type header"})
238
+ end
239
+ end
240
+
241
+ def self.parse_request(env)
242
+ OpenStruct.new({
243
+ headers: env.select {|k,v| k.to_s.start_with? 'HTTP_'}
244
+ .collect {|key, val| [key.to_s.sub(/^HTTP_/, '').downcase, val]}.to_h,
245
+ params: env['rack.request.query_hash'],
246
+ body: env['rack.input']&.read
247
+ })
248
+ end
249
+
250
+ def self.serve(code, params={})
251
+ [
252
+ code,
253
+ {"Content-Type" => "application/json; charset=utf-8"},
254
+ [params.to_json]
255
+ ]
256
+ end
257
+ end
258
+
259
+ endpoint WebhookParser
260
+ end
261
+ end
262
+
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hypertrack_v3
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Bernard Pratz
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-10-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.15.4
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.15.4
27
+ - !ruby/object:Gem::Dependency
28
+ name: nokogiri
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.8'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.8'
41
+ description: Ruby wrapper around HyperTrack's API V3. Refer http://docs.hypertrack.com/
42
+ for more information.
43
+ email: guyzmo+pub@m0g.net
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - lib/hypertrack_v3.rb
49
+ homepage: http://rubygems.org/gems/hypertrack_v3
50
+ licenses:
51
+ - LGPL-3.0-only
52
+ metadata: {}
53
+ post_install_message:
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 2.0.0
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubygems_version: 3.0.6
69
+ signing_key:
70
+ specification_version: 4
71
+ summary: Ruby bindings for the HyperTrack V3 API!
72
+ test_files: []