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.
- checksums.yaml +7 -0
- data/.env +6 -0
- data/.gitignore +3 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +45 -0
- data/LICENSE.txt +21 -0
- data/README.md +97 -0
- data/Rakefile +2 -0
- data/autoluv.gemspec +27 -0
- data/bin/autoluv +27 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/data/airport_time_zones.json +9056 -0
- data/lib/autoluv.rb +73 -0
- data/lib/autoluv/southwestclient.rb +128 -0
- data/lib/autoluv/version.rb +3 -0
- metadata +116 -0
data/lib/autoluv.rb
ADDED
@@ -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
|
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: []
|