autoluv 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,73 @@
1
+ require "autoluv/version"
2
+ require "autoluv/southwestclient"
3
+ require "pony"
4
+
5
+ require "dotenv"
6
+ Dotenv.load("#{Dir.home}/.autoluv.env")
7
+
8
+ module Autoluv
9
+ class Error < StandardError; end
10
+
11
+ PONY_OPTIONS = {
12
+ from: "#{ENV["LUV_FROM_EMAIL"]}",
13
+ via: :smtp,
14
+ via_options: {
15
+ address: "#{ENV["LUV_SMTP_SERVER"]}",
16
+ port: "#{ENV["LUV_PORT"]}",
17
+ user_name: "#{ENV["LUV_USER_NAME"]}",
18
+ password: "#{ENV["LUV_PASSWORD"]}",
19
+ authentication: :login,
20
+ },
21
+ }
22
+
23
+ LOG_DIR = File.expand_path("../logs/", __dir__)
24
+
25
+ def self.log(confirmation_number, first_name, last_name, message, exception)
26
+ log_path = "#{LOG_DIR}/#{first_name} #{last_name}"
27
+ FileUtils.mkdir_p(log_path) unless Dir.exist?(log_path)
28
+
29
+ logger = Logger.new("#{log_path}/#{confirmation_number}.log")
30
+
31
+ logger.error(message + "\n" + exception.backtrace.join("\n"))
32
+ end
33
+
34
+ def self.notify_user(success, confirmation_number, first_name, last_name, data = {})
35
+ subject = "#{first_name} #{last_name} (#{confirmation_number}): "
36
+ body = ""
37
+
38
+ if success
39
+ subject << "Succeeded at #{data[:metadata][:end_time]}. #{data[:metadata][:attempts]} attempt(s) in #{data[:metadata][:elapsed_time]} sec."
40
+ body = data[:boarding_positions]
41
+ else
42
+ subject << "Unsuccessful check-in."
43
+ body = data[:exception_message]
44
+ Autoluv::log(confirmation_number, first_name, last_name, body, data[:exception])
45
+ end
46
+
47
+ if data[:to].nil?
48
+ puts body
49
+ else
50
+ Autoluv::email(subject, body, data[:to], data[:bcc])
51
+ end
52
+ end
53
+
54
+ def self.email(subject, body, to, bcc = nil)
55
+ # only send an email if we have all the environmental variables set
56
+ return if PONY_OPTIONS.values.any? &:empty?
57
+
58
+ # if we're BCCing someone, swap the fields so they don't see your TO address.
59
+ # this is really for my personal use-case.
60
+ unless bcc.nil?
61
+ temp = bcc
62
+ to = bcc
63
+ bcc = temp
64
+ end
65
+
66
+ Pony.mail(PONY_OPTIONS.merge({
67
+ to: to,
68
+ bcc: bcc,
69
+ subject: subject,
70
+ body: body,
71
+ }))
72
+ end
73
+ end
@@ -0,0 +1,128 @@
1
+ require "rest-client"
2
+ require "securerandom"
3
+ require "json"
4
+ require "tzinfo"
5
+
6
+ module Autoluv
7
+ class SouthwestClient
8
+ @confirmation_number = @first_name = @last_name = @options = nil
9
+
10
+ # minimum required headers for all API calls
11
+ DEFAULT_HEADERS = {
12
+ "Content-Type": "application/json",
13
+ "X-API-Key": "l7xx0a43088fe6254712b10787646d1b298e",
14
+ "X-Channel-ID": "MWEB", # required now for viewing a reservation
15
+ }
16
+
17
+ CHECK_IN_URL = "https://mobile.southwest.com/api/mobile-air-operations/v1/mobile-air-operations/page/check-in"
18
+ RESERVATION_URL = "https://mobile.southwest.com/api/mobile-air-booking/v1/mobile-air-booking/page/view-reservation"
19
+
20
+ TIME_ZONES_PATH = File.expand_path("../../data/airport_time_zones.json", __dir__)
21
+
22
+ def self.schedule(confirmation_number, first_name, last_name, to = nil, bcc = nil)
23
+ flights = self.departing_flights(confirmation_number, first_name, last_name)
24
+
25
+ flights.each_with_index do |flight, x|
26
+ check_in_time = self.check_in_time(flight)
27
+
28
+ puts "Scheduling flight departing #{flight[:airport_code]} at #{flight[:departure_time]} on #{flight[:departure_date]}."
29
+
30
+ command = "echo 'autoluv checkin #{confirmation_number} #{first_name} #{last_name} #{to} #{bcc}' | at #{check_in_time.strftime('%I:%M %p %m/%d/%y')}"
31
+ `#{command}`
32
+
33
+ puts unless x == flights.size - 1
34
+ end
35
+ end
36
+
37
+ def self.check_in(confirmation_number, first_name, last_name, to = nil, bcc = nil)
38
+ check_in = attempt = nil
39
+
40
+ # try checking in multiple times in case the our server time is out of sync with Southwest's.
41
+ num_attempts = 10
42
+
43
+ start_time = Time.now
44
+
45
+ num_attempts.times do |x|
46
+ begin
47
+ attempt = x + 1
48
+ post_data = self.check_in_post_data(confirmation_number, first_name, last_name)
49
+ check_in = RestClient.post("#{CHECK_IN_URL}", post_data.to_json, self.headers)
50
+ break
51
+ rescue RestClient::ExceptionWithResponse => ewr
52
+ sleep(1)
53
+ next unless x == num_attempts - 1
54
+
55
+ raise
56
+ end
57
+ end
58
+
59
+ end_time = Time.now
60
+ boarding_positions = ""
61
+
62
+ check_in_json = JSON.parse(check_in)
63
+ flights = check_in_json["checkInConfirmationPage"]["flights"]
64
+
65
+ # make the output more user friendly
66
+ flights.each_with_index do |flight, x|
67
+ boarding_positions << flight["originAirportCode"] << "\n"
68
+
69
+ flight["passengers"].each do |passenger|
70
+ boarding_positions << "- #{passenger["name"]} (#{passenger["boardingGroup"]}#{passenger["boardingPosition"]})" << "\n"
71
+ end
72
+
73
+ boarding_positions << "\n" unless x == flights.size - 1
74
+ end
75
+
76
+ metadata = {
77
+ end_time: end_time.strftime("%I:%M.%L"),
78
+ elapsed_time: (end_time - start_time).round(2),
79
+ attempts: attempt,
80
+ }
81
+
82
+ Autoluv::notify_user(true, confirmation_number, first_name, last_name, { to: to, bcc: bcc, boarding_positions: boarding_positions, metadata: metadata })
83
+ end
84
+
85
+ private
86
+ def self.headers
87
+ # required now for all API calls
88
+ DEFAULT_HEADERS.merge({ "X-User-Experience-ID": SecureRandom.uuid })
89
+ end
90
+
91
+ def self.departing_flights(confirmation_number, first_name, last_name)
92
+ reservation = RestClient.get("#{RESERVATION_URL}/#{confirmation_number}?first-name=#{first_name}&last-name=#{last_name}", self.headers)
93
+ reservation_json = JSON.parse(reservation)
94
+
95
+ airport_time_zones = JSON.parse(File.read(TIME_ZONES_PATH))
96
+
97
+ departing_flights = reservation_json["viewReservationViewPage"]["bounds"].map do |bound|
98
+ airport_code = bound["departureAirport"]["code"]
99
+
100
+ {
101
+ airport_code: airport_code,
102
+ departure_date: bound["departureDate"],
103
+ departure_time: bound["departureTime"],
104
+ time_zone: airport_time_zones[airport_code],
105
+ }
106
+ end
107
+ end
108
+
109
+ def self.check_in_post_data(confirmation_number, first_name, last_name)
110
+ check_in = RestClient.get("#{CHECK_IN_URL}/#{confirmation_number}?first-name=#{first_name}&last-name=#{last_name}", self.headers)
111
+ check_in_json = JSON.parse(check_in)
112
+ check_in_json["checkInViewReservationPage"]["_links"]["checkIn"]["body"]
113
+ end
114
+
115
+ def self.check_in_time(flight)
116
+ tz_abbreviation = TZInfo::Timezone.get(flight[:time_zone]).current_period.offset.abbreviation.to_s
117
+
118
+ # 2020-09-21 13:15 CDT
119
+ departure_time = Time.parse("#{flight[:departure_date]} #{flight[:departure_time]} #{tz_abbreviation}")
120
+
121
+ # subtract a day (in seconds) to know when we can check in
122
+ check_in_time = departure_time - (24 * 60 * 60)
123
+
124
+ # compensate for our server time zone
125
+ check_in_time -= (departure_time.utc_offset - Time.now.utc_offset)
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,3 @@
1
+ module Autoluv
2
+ VERSION = "0.2.0"
3
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: autoluv
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Alex Tran
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-09-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rest-client
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 2.1.0
20
+ type: :runtime
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: pony
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 1.13.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 1.13.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: dotenv
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 2.7.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 2.7.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: tzinfo
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.3.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.3.0
69
+ description:
70
+ email:
71
+ - hello@alextran.org
72
+ executables:
73
+ - autoluv
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".env"
78
+ - ".gitignore"
79
+ - Gemfile
80
+ - Gemfile.lock
81
+ - LICENSE.txt
82
+ - README.md
83
+ - Rakefile
84
+ - autoluv.gemspec
85
+ - bin/autoluv
86
+ - bin/console
87
+ - bin/setup
88
+ - data/airport_time_zones.json
89
+ - lib/autoluv.rb
90
+ - lib/autoluv/southwestclient.rb
91
+ - lib/autoluv/version.rb
92
+ homepage: https://github.com/byalextran/southwest-checkin
93
+ licenses:
94
+ - MIT
95
+ metadata: {}
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: 2.3.0
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubygems_version: 3.1.2
112
+ signing_key:
113
+ specification_version: 4
114
+ summary: Easy-to-use gem to check in to Southwest flights automatically. Also supports
115
+ sending email notifications.
116
+ test_files: []