huginn_transilien_agent 0.1.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 31bdf2049491dfae488ac673939ec86567bfe1c814a1eef8c9317c837a54f8e2
4
+ data.tar.gz: 5bd6edb69cabab1cd9c7401837e6c6365675ff03115dfd9127bb2721b2788e98
5
+ SHA512:
6
+ metadata.gz: 10a2e0dfa054cab20dca053b762fd459d51c474a46fcc215cc3a94b53e68f3b282f41999c2be777b86e56416a84548e182d27fcb733b2795e63e072199aca26f
7
+ data.tar.gz: bf59730066c684555bbc24bdd1bfeb6850639f81ec5f00eb7a6f61468cb90d55ccbddafd696e102457b6c735dd7192c138a6878535a2446362c6303d40002b91
data/LICENSE.txt ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2023 Nicolas Germain
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,532 @@
1
+ module Agents
2
+ class TransilienAgent < Agent
3
+ include FormConfigurable
4
+ can_dry_run!
5
+ no_bulk_receive!
6
+ default_schedule 'every_1h'
7
+
8
+ description <<-MD
9
+ The Transilien Agent interacts with Transilien's API.
10
+
11
+ The `type` can be like itinerary.
12
+
13
+ `debug` is for adding verbosity.
14
+
15
+ `token` is needed for authentication.
16
+
17
+ `src` is for the departure.
18
+
19
+ `dst` is for the direction / arrival.
20
+
21
+ `expected_receive_period_in_days` is used to determine if the Agent is working. Set it to the maximum number of days
22
+ that you anticipate passing without this Agent receiving an incoming Event.
23
+ MD
24
+
25
+ event_description <<-MD
26
+ Events look like this:
27
+
28
+ {
29
+ "route": {
30
+ "id": "route:SNCF:C",
31
+ "name": "C",
32
+ "is_frequence": "False",
33
+ "direction_type": "forward",
34
+ "physical_modes": [
35
+ {
36
+ "id": "physical_mode:RapidTransit",
37
+ "name": "RER / Transilien"
38
+ }
39
+ ],
40
+ "direction": {
41
+ "id": "stop_area:SNCF:87393157",
42
+ "name": "Versailles Château Rive Gauche (Versailles)",
43
+ "quality": 0,
44
+ "stop_area": {
45
+ "id": "stop_area:SNCF:87393157",
46
+ "name": "Versailles Château Rive Gauche",
47
+ "codes": [
48
+ {
49
+ "type": "source",
50
+ "value": "87393157"
51
+ },
52
+ {
53
+ "type": "uic",
54
+ "value": "87393157"
55
+ }
56
+ ],
57
+ "timezone": "Europe/Paris",
58
+ "label": "Versailles Château Rive Gauche (Versailles)",
59
+ "coord": {
60
+ "lon": "2.128827",
61
+ "lat": "48.800364"
62
+ },
63
+ "links": [
64
+
65
+ ]
66
+ },
67
+ "embedded_type": "stop_area"
68
+ },
69
+ "geojson": {
70
+ "type": "MultiLineString",
71
+ "coordinates": [
72
+
73
+ ]
74
+ },
75
+ "links": [
76
+
77
+ ],
78
+ "line": {
79
+ "id": "line:SNCF:C",
80
+ "name": "C",
81
+ "code": "C",
82
+ "color": "FCD946",
83
+ "text_color": "FFFFFF",
84
+ "codes": [
85
+
86
+ ],
87
+ "physical_modes": [
88
+ {
89
+ "id": "physical_mode:RapidTransit",
90
+ "name": "RER / Transilien"
91
+ }
92
+ ],
93
+ "commercial_mode": {
94
+ "id": "commercial_mode:TNRER",
95
+ "name": "RER"
96
+ },
97
+ "opening_time": "033840",
98
+ "closing_time": "014100",
99
+ "geojson": {
100
+ "type": "MultiLineString",
101
+ "coordinates": [
102
+
103
+ ]
104
+ },
105
+ "links": [
106
+
107
+ ]
108
+ }
109
+ },
110
+ "stop_point": {
111
+ "id": "stop_point:SNCF:87545350:RapidTransit",
112
+ "name": "Saint-Martin d'Étampes",
113
+ "label": "Saint-Martin d'Étampes (Étampes)",
114
+ "coord": {
115
+ "lon": "2.145401",
116
+ "lat": "48.42747"
117
+ },
118
+ "links": [
119
+
120
+ ],
121
+ "commercial_modes": [
122
+ {
123
+ "id": "commercial_mode:TNRER",
124
+ "name": "RER"
125
+ }
126
+ ],
127
+ "physical_modes": [
128
+ {
129
+ "id": "physical_mode:RapidTransit",
130
+ "name": "RER / Transilien",
131
+ "co2_emission_rate": {
132
+ "value": 7.28,
133
+ "unit": "gEC/Km"
134
+ }
135
+ }
136
+ ],
137
+ "administrative_regions": [
138
+ {
139
+ "id": "admin:fr:91223",
140
+ "name": "Étampes",
141
+ "level": 8,
142
+ "zip_code": "91150",
143
+ "label": "Étampes (91150)",
144
+ "insee": "91223",
145
+ "coord": {
146
+ "lon": "2.1614464",
147
+ "lat": "48.4344621"
148
+ }
149
+ }
150
+ ],
151
+ "stop_area": {
152
+ "id": "stop_area:SNCF:87545350",
153
+ "name": "Saint-Martin d'Étampes",
154
+ "codes": [
155
+ {
156
+ "type": "source",
157
+ "value": "87545350"
158
+ },
159
+ {
160
+ "type": "uic",
161
+ "value": "87545350"
162
+ }
163
+ ],
164
+ "timezone": "Europe/Paris",
165
+ "label": "Saint-Martin d'Étampes (Étampes)",
166
+ "coord": {
167
+ "lon": "2.145401",
168
+ "lat": "48.42747"
169
+ },
170
+ "links": [
171
+
172
+ ],
173
+ "administrative_regions": [
174
+ {
175
+ "id": "admin:fr:91223",
176
+ "name": "Étampes",
177
+ "level": 8,
178
+ "zip_code": "91150",
179
+ "label": "Étampes (91150)",
180
+ "insee": "91223",
181
+ "coord": {
182
+ "lon": "2.1614464",
183
+ "lat": "48.4344621"
184
+ }
185
+ }
186
+ ]
187
+ },
188
+ "equipments": [
189
+
190
+ ]
191
+ },
192
+ "stop_date_time": {
193
+ "departure_date_time": "20230902T111900",
194
+ "base_departure_date_time": "20230902T111900",
195
+ "arrival_date_time": "20230902T111900",
196
+ "base_arrival_date_time": "20230902T111900",
197
+ "additional_informations": [
198
+
199
+ ],
200
+ "links": [
201
+
202
+ ],
203
+ "data_freshness": "base_schedule"
204
+ },
205
+ "display_informations": {
206
+ "commercial_mode": "RER",
207
+ "network": "RER",
208
+ "direction": "Musée d'Orsay (Paris)",
209
+ "label": "C",
210
+ "color": "FCD946",
211
+ "code": "C",
212
+ "name": "C",
213
+ "links": [
214
+
215
+ ],
216
+ "text_color": "FFFFFF",
217
+ "description": "",
218
+ "physical_mode": "RER / Transilien",
219
+ "equipments": [
220
+
221
+ ],
222
+ "headsign": "ORET",
223
+ "trip_short_name": "145406"
224
+ },
225
+ "links": [
226
+ {
227
+ "type": "line",
228
+ "id": "line:SNCF:C"
229
+ },
230
+ {
231
+ "type": "vehicle_journey",
232
+ "id": "vehicle_journey:SNCF:2023-09-02:145406:1187:RapidTransit"
233
+ },
234
+ {
235
+ "type": "route",
236
+ "id": "route:SNCF:C"
237
+ },
238
+ {
239
+ "type": "commercial_mode",
240
+ "id": "commercial_mode:TNRER"
241
+ },
242
+ {
243
+ "type": "physical_mode",
244
+ "id": "physical_mode:RapidTransit"
245
+ },
246
+ {
247
+ "type": "network",
248
+ "id": "network:SNCF:TNRER"
249
+ }
250
+ ]
251
+ }
252
+ MD
253
+
254
+ def default_options
255
+ {
256
+ 'type' => 'nextdeparture',
257
+ 'token' => '',
258
+ 'src' => '',
259
+ 'dst' => '',
260
+ 'date' => '',
261
+ 'time' => '',
262
+ 'debug' => 'false',
263
+ 'expected_receive_period_in_days' => '2',
264
+ 'changes_only' => 'true'
265
+ }
266
+ end
267
+
268
+ form_configurable :debug, type: :boolean
269
+ form_configurable :token, type: :string
270
+ form_configurable :src, type: :string
271
+ form_configurable :dst, type: :string
272
+ form_configurable :date, type: :string
273
+ form_configurable :time, type: :string
274
+ form_configurable :expected_receive_period_in_days, type: :string
275
+ form_configurable :changes_only, type: :boolean
276
+ form_configurable :type, type: :array, values: ['itinary', 'nextdeparture', 'traffic_reports']
277
+ def validate_options
278
+ errors.add(:base, "type has invalid value: should be 'itinary' 'nextdeparture' 'traffic_reports'") if interpolated['type'].present? && !%w(itinary nextdeparture traffic_reports).include?(interpolated['type'])
279
+
280
+ unless options['src'].present? || !['itinary', 'nextdeparture'].include?(options['type'])
281
+ errors.add(:base, "src is a required field")
282
+ end
283
+
284
+ unless options['dst'].present? || !['itinary', 'nextdeparture'].include?(options['type'])
285
+ errors.add(:base, "dst is a required field")
286
+ end
287
+
288
+ unless options['token'].present? || !['itinary', 'nextdeparture', 'traffic_reports'].include?(options['type'])
289
+ errors.add(:base, "token is a required field")
290
+ end
291
+
292
+ if options.has_key?('changes_only') && boolify(options['changes_only']).nil?
293
+ errors.add(:base, "if provided, changes_only must be true or false")
294
+ end
295
+
296
+ if options.has_key?('debug') && boolify(options['debug']).nil?
297
+ errors.add(:base, "if provided, debug must be true or false")
298
+ end
299
+
300
+ unless options['expected_receive_period_in_days'].present? && options['expected_receive_period_in_days'].to_i > 0
301
+ errors.add(:base, "Please provide 'expected_receive_period_in_days' to indicate how many days can pass before this Agent is considered to be not working")
302
+ end
303
+ end
304
+
305
+ def working?
306
+ event_created_within?(options['expected_receive_period_in_days']) && !recent_error_logs?
307
+ end
308
+
309
+ def check
310
+ trigger_action
311
+ end
312
+
313
+ private
314
+
315
+ def log_curl_output(code,body)
316
+
317
+ log "request status : #{code}"
318
+
319
+ if interpolated['debug'] == 'true'
320
+ log "body"
321
+ log body
322
+ end
323
+
324
+ end
325
+
326
+ def search_station(base_url,city)
327
+
328
+ uri = URI.parse("https://api.sncf.com/v1/coverage/sncf/pt_objects?q=#{city}'")
329
+ request = Net::HTTP::Get.new(uri)
330
+ request["Authorization"] = interpolated['token']
331
+
332
+ req_options = {
333
+ use_ssl: uri.scheme == "https",
334
+ }
335
+
336
+ response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
337
+ http.request(request)
338
+ end
339
+
340
+ log_curl_output(response.code,response.body)
341
+
342
+ log response.body.class
343
+ parsed = JSON.parse(response.body)
344
+ return parsed
345
+
346
+ end
347
+
348
+ def search_city(base_url,city)
349
+
350
+ uri = URI.parse("#{base_url}places?search=#{city}")
351
+ request = Net::HTTP::Get.new(uri)
352
+ request["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0"
353
+ request["Accept"] = "application/json, text/plain, */*"
354
+ request["Accept-Language"] = "fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3"
355
+ request["Connection"] = "keep-alive"
356
+ request["Sec-Fetch-Dest"] = "empty"
357
+ request["Sec-Fetch-Mode"] = "cors"
358
+ request["Sec-Fetch-Site"] = "same-origin"
359
+ request["Dnt"] = "1"
360
+ request["Pragma"] = "no-cache"
361
+ request["Cache-Control"] = "no-cache"
362
+ request["Te"] = "trailers"
363
+
364
+ req_options = {
365
+ use_ssl: uri.scheme == "https",
366
+ }
367
+
368
+ response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
369
+ http.request(request)
370
+ end
371
+
372
+ log_curl_output(response.code,response.body)
373
+
374
+ log response.body.class
375
+ parsed = JSON.parse(response.body)
376
+ return parsed['places'][0]
377
+
378
+ end
379
+
380
+
381
+ def nextdeparture(base_url)
382
+
383
+ src = search_station(base_url,interpolated['src'])
384
+ if interpolated['debug'] == 'true'
385
+ log "src"
386
+ log src
387
+ end
388
+ dst = search_station(base_url,interpolated['dst'])
389
+ if interpolated['debug'] == 'true'
390
+ log "dst"
391
+ log dst
392
+ end
393
+
394
+ stop_area = CGI.escape(src['pt_objects'][0]['id']) if src['pt_objects'][0]['id'].present?
395
+ log stop_area
396
+ uri = URI.parse("#{base_url}coverage/sncf/stop_areas/#{stop_area}/departures")
397
+ request = Net::HTTP::Get.new(uri)
398
+ request["Authorization"] = interpolated['token']
399
+
400
+ req_options = {
401
+ use_ssl: uri.scheme == "https",
402
+ }
403
+
404
+ response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
405
+ http.request(request)
406
+ end
407
+
408
+ log_curl_output(response.code,response.body)
409
+
410
+ payload = JSON.parse(response.body)
411
+ payload['departures'].each do | train |
412
+ log train['display_informations']['direction']
413
+ log dst['pt_objects'][0]['name']
414
+ if train['display_informations']['direction'] == dst['pt_objects'][0]['name']
415
+ create_event payload: train
416
+ end
417
+ end
418
+ end
419
+
420
+ def traffic_reports(base_url)
421
+
422
+ uri = URI.parse("#{base_url}coverage/sncf/traffic_reports")
423
+ request = Net::HTTP::Get.new(uri)
424
+ request["Authorization"] = interpolated['token']
425
+
426
+ req_options = {
427
+ use_ssl: uri.scheme == "https",
428
+ }
429
+
430
+ response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
431
+ http.request(request)
432
+ end
433
+
434
+ log_curl_output(response.code,response.body)
435
+
436
+ payload = JSON.parse(response.body)
437
+ payload['disruptions'].each do | disruption |
438
+ create_event payload: disruption
439
+ end
440
+
441
+ end
442
+
443
+ def itinary(base_url)
444
+
445
+ src = search_city(base_url,interpolated['src'])
446
+ if interpolated['debug'] == 'true'
447
+ log "src"
448
+ log src
449
+ end
450
+ dst = search_city(base_url,interpolated['dst'])
451
+ if interpolated['debug'] == 'true'
452
+ log "dst"
453
+ log dst
454
+ end
455
+
456
+ data = {}
457
+ data["departure"] = src['name'] if src['name'].present?
458
+ data["departureId"] = src['id'] if src['id'].present?
459
+ # data[""] = src[''] if src[''].present?
460
+ # data[""] = dst[''] if dst[''].present?
461
+ data["arrival"] = dst['name'] if dst['name'].present?
462
+ data["arrivalId"] = dst['id'] if dst['id'].present?
463
+ data["departureLatitude"] = src['coord']['lat'] if src['coord']['lat'].present?
464
+ data["departureLongitude"] = src['coord']['lon'] if src['coord']['lon'].present?
465
+ data["arrivalLatitude"] = dst['coord']['lat'] if dst['coord']['lat'].present?
466
+ data["arrivalLongitude"] = dst['coord']['lon'] if dst['coord']['lon'].present?
467
+ data["dateType"] = "DEPARTURE"
468
+ data["formAction"] = false
469
+ data["haveBusBypassed"] = false
470
+ date_now = DateTime.now
471
+ if interpolated['date'].empty?
472
+ data["date"] = date_now.strftime("%Y-%m-%d")
473
+ else
474
+ data["date"] = interpolated['date']
475
+ end
476
+ if interpolated['time'].empty?
477
+ data["time"] = date_now.strftime("%H:%M")
478
+ else
479
+ data["time"] = interpolated['time']
480
+ end
481
+
482
+ uri = URI.parse("#{base_url}itinerary/search")
483
+ request = Net::HTTP::Post.new(uri)
484
+ request.content_type = "application/json"
485
+ request["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0"
486
+ request["Accept"] = "application/json, text/plain, */*"
487
+ request["Accept-Language"] = "fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3"
488
+ request["Origin"] = "https://www.transilien.com"
489
+ request["Connection"] = "keep-alive"
490
+ request.body = data.to_json
491
+
492
+ req_options = {
493
+ use_ssl: uri.scheme == "https",
494
+ }
495
+
496
+ response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
497
+ http.request(request)
498
+ end
499
+
500
+ log_curl_output(response.code,response.body)
501
+
502
+ if interpolated['emit_events'] == 'true'
503
+ create_event payload: response.body
504
+ end
505
+ payload = JSON.parse(response.body)
506
+ if interpolated['date'].empty?
507
+ ttime = date_now.strftime("%Y-%m-%d")
508
+ else
509
+ ttime = interpolated['date']
510
+ end
511
+ payload['journeys'][ttime].each do | train |
512
+ create_event payload: train
513
+ end
514
+ end
515
+
516
+
517
+ def trigger_action()
518
+
519
+ base_url_auth = 'https://api.sncf.com/v1/'
520
+ case interpolated['type']
521
+ when "itinary"
522
+ itinary(base_url_auth)
523
+ when "nextdeparture"
524
+ nextdeparture(base_url_auth)
525
+ when "traffic_reports"
526
+ traffic_reports(base_url_auth)
527
+ else
528
+ log "Error: type has an invalid value (#{type})"
529
+ end
530
+ end
531
+ end
532
+ end
@@ -0,0 +1,4 @@
1
+ require 'huginn_agent'
2
+
3
+ #HuginnAgent.load 'huginn_transilien_agent/concerns/my_agent_concern'
4
+ HuginnAgent.register 'huginn_transilien_agent/transilien_agent'
@@ -0,0 +1,13 @@
1
+ require 'rails_helper'
2
+ require 'huginn_agent/spec_helper'
3
+
4
+ describe Agents::TransilienAgent do
5
+ before(:each) do
6
+ @valid_options = Agents::TransilienAgent.new.default_options
7
+ @checker = Agents::TransilienAgent.new(:name => "TransilienAgent", :options => @valid_options)
8
+ @checker.user = users(:bob)
9
+ @checker.save!
10
+ end
11
+
12
+ pending "add specs here"
13
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: huginn_transilien_agent
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Nicolas Germain
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-09-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 2.1.0
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 2.1.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 12.3.3
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 12.3.3
41
+ - !ruby/object:Gem::Dependency
42
+ name: huginn_agent
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Write a longer description or delete this line.
56
+ email:
57
+ - ngermain@hihouhou.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - LICENSE.txt
63
+ - lib/huginn_transilien_agent.rb
64
+ - lib/huginn_transilien_agent/transilien_agent.rb
65
+ - spec/transilien_agent_spec.rb
66
+ homepage: https://github.com/hihouhou/huginn_transilien_agent
67
+ licenses:
68
+ - MIT
69
+ metadata: {}
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubygems_version: 3.3.3
86
+ signing_key:
87
+ specification_version: 4
88
+ summary: Write a short summary, because Rubygems requires one.
89
+ test_files:
90
+ - spec/transilien_agent_spec.rb