autoluv 0.2.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.
@@ -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: []