├── Rakefile ├── lib ├── autoluv │ ├── version.rb │ └── southwestclient.rb └── autoluv.rb ├── .gitignore ├── Gemfile ├── bin ├── setup ├── console └── autoluv ├── LICENSE.txt ├── autoluv.gemspec └── README.md /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | task :default => :spec 3 | -------------------------------------------------------------------------------- /lib/autoluv/version.rb: -------------------------------------------------------------------------------- 1 | module Autoluv 2 | VERSION = "0.3.2" 3 | end 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /*.gem 3 | /logs/ 4 | /pkg/ 5 | /Gemfile.lock 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "rake", "~> 13.0" 6 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "autoluv" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Alex Tran 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /bin/autoluv: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "autoluv" 4 | 5 | command = ARGV[0].to_s 6 | confirmation_number = ARGV[1] 7 | first_name = ARGV[2] 8 | last_name = ARGV[3] 9 | to = ARGV[4] 10 | bcc = ARGV[5] 11 | 12 | begin 13 | unless File.exist?(ENV["LUV_HEADERS_FILE"].to_s) 14 | abort "Please create a valid Southwest header file before continuing. Learn more: https://github.com/byalextran/southwest-headers" 15 | end 16 | 17 | case command.downcase 18 | when "schedule" 19 | Autoluv::SouthwestClient.schedule(confirmation_number, first_name, last_name, to, bcc) 20 | when "checkin" 21 | Autoluv::SouthwestClient.check_in(confirmation_number, first_name, last_name, to, bcc) 22 | else 23 | puts "Command not recognized." 24 | end 25 | rescue RestClient::ExceptionWithResponse => ewr 26 | Autoluv::notify_user(false, confirmation_number, first_name, last_name, { to: to, bcc: bcc, exception_message: JSON.parse(ewr.response)["message"], exception: ewr }) 27 | rescue JSON::ParserError => pe 28 | Autoluv::notify_user(false, confirmation_number, first_name, last_name, { to: to, bcc: bcc, exception_message: pe.message, exception: pe }) 29 | rescue => e 30 | Autoluv::notify_user(false, confirmation_number, first_name, last_name, { to: to, bcc: bcc, exception_message: e.message, exception: e }) 31 | end 32 | -------------------------------------------------------------------------------- /autoluv.gemspec: -------------------------------------------------------------------------------- 1 | require_relative 'lib/autoluv/version' 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "autoluv" 5 | spec.version = Autoluv::VERSION 6 | spec.authors = ["Alex Tran"] 7 | spec.email = ["hello@alextran.org"] 8 | 9 | spec.summary = "Easy-to-use gem to check in to Southwest flights automatically. Also supports sending email notifications." 10 | spec.homepage = "https://github.com/byalextran/southwest-checkin" 11 | spec.license = "MIT" 12 | spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0") 13 | 14 | spec.add_runtime_dependency "rest-client", "~> 2.1.0" 15 | spec.add_runtime_dependency "pony", "~> 1.13.1" 16 | spec.add_runtime_dependency "dotenv", "~> 2.7.6" 17 | spec.add_runtime_dependency "tzinfo", "~> 2.0.2" 18 | spec.add_runtime_dependency "json", "~> 2.3.1" 19 | spec.add_runtime_dependency "fileutils", "~> 1.4.1" 20 | spec.add_runtime_dependency "logger", "~> 1.4.2" 21 | 22 | # Specify which files should be added to the gem when it is released. 23 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 24 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 25 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 26 | end 27 | spec.bindir = "bin" 28 | spec.executables = ["autoluv"] 29 | spec.require_paths = ["lib"] 30 | end 31 | -------------------------------------------------------------------------------- /lib/autoluv.rb: -------------------------------------------------------------------------------- 1 | require "autoluv/version" 2 | require "autoluv/southwestclient" 3 | require "pony" 4 | require "logger" 5 | require "fileutils" 6 | require "dotenv" 7 | 8 | Dotenv.load("#{Dir.home}/.autoluv.env") 9 | 10 | module Autoluv 11 | class Error < StandardError; end 12 | 13 | PONY_OPTIONS = { 14 | from: "#{ENV["LUV_FROM_EMAIL"]}", 15 | via: :smtp, 16 | via_options: { 17 | address: "#{ENV["LUV_SMTP_SERVER"]}", 18 | port: "#{ENV["LUV_PORT"]}", 19 | user_name: "#{ENV["LUV_USER_NAME"]}", 20 | password: "#{ENV["LUV_PASSWORD"]}", 21 | authentication: :login, 22 | }, 23 | } 24 | 25 | LOG_DIR = File.expand_path("../logs/", __dir__) 26 | 27 | def self.log(confirmation_number, first_name, last_name, message, exception) 28 | log_path = "#{LOG_DIR}/#{first_name} #{last_name}" 29 | FileUtils.mkdir_p(log_path) unless Dir.exist?(log_path) 30 | 31 | logger = Logger.new("#{log_path}/#{confirmation_number}.log") 32 | 33 | logger.error(message + "\n" + exception.backtrace.join("\n")) 34 | end 35 | 36 | def self.notify_user(success, confirmation_number, first_name, last_name, data = {}) 37 | subject = "#{first_name} #{last_name} (#{confirmation_number}): " 38 | body = "" 39 | 40 | if success 41 | subject << "Succeeded at #{data[:metadata][:end_time]}. #{data[:metadata][:attempts]} attempt(s) in #{data[:metadata][:elapsed_time]} sec." 42 | body = data[:boarding_positions] 43 | else 44 | subject << "Unsuccessful check-in." 45 | body = data[:exception_message] 46 | Autoluv::log(confirmation_number, first_name, last_name, body, data[:exception]) 47 | end 48 | 49 | if data[:to].nil? 50 | puts body 51 | else 52 | Autoluv::email(subject, body, data[:to], data[:bcc]) 53 | end 54 | end 55 | 56 | def self.email(subject, body, to, bcc = nil) 57 | # only send an email if we have all the environmental variables set 58 | return if PONY_OPTIONS.values.any? &:empty? 59 | 60 | begin 61 | Pony.mail(PONY_OPTIONS.merge({ 62 | to: to, 63 | bcc: bcc, 64 | subject: subject, 65 | body: body, 66 | })) 67 | rescue => e 68 | puts e.message 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/autoluv/southwestclient.rb: -------------------------------------------------------------------------------- 1 | require "rest-client" 2 | require "securerandom" 3 | require "json" 4 | require "tzinfo" 5 | require "shellwords" 6 | 7 | module Autoluv 8 | class SouthwestClient 9 | # minimum required headers for all API calls 10 | DEFAULT_HEADERS = { 11 | "Content-Type": "application/json", 12 | "X-API-Key": "l7xx0a43088fe6254712b10787646d1b298e", 13 | "X-Channel-ID": "MWEB", # required now for viewing a reservation 14 | } 15 | 16 | CHECK_IN_URL = "https://mobile.southwest.com/api/mobile-air-operations/v1/mobile-air-operations/page/check-in" 17 | RESERVATION_URL = "https://mobile.southwest.com/api/mobile-air-booking/v1/mobile-air-booking/page/view-reservation" 18 | 19 | TIME_ZONES_PATH = File.expand_path("../../data/airport_time_zones.json", __dir__) 20 | 21 | def self.schedule(confirmation_number, first_name, last_name, to = nil, bcc = nil) 22 | flights = self.departing_flights(confirmation_number, first_name, last_name) 23 | 24 | flights.each_with_index do |flight, x| 25 | check_in_time = self.check_in_time(flight) 26 | 27 | puts "Scheduling flight departing #{flight[:airport_code]} at #{flight[:departure_time]} on #{flight[:departure_date]}." 28 | 29 | command = "echo 'autoluv checkin #{confirmation_number} #{Shellwords.shellescape(first_name)} #{Shellwords.shellescape(last_name)} #{to} #{bcc}' | at #{check_in_time.strftime('%I:%M %p %m/%d/%y')}" 30 | `#{command}` 31 | 32 | puts unless x == flights.size - 1 33 | end 34 | end 35 | 36 | def self.check_in(confirmation_number, first_name, last_name, to = nil, bcc = nil) 37 | check_in = attempt = nil 38 | 39 | # try checking in multiple times in case the our server time is out of sync with Southwest's. 40 | num_attempts = 10 41 | 42 | start_time = Time.now 43 | 44 | num_attempts.times do |x| 45 | begin 46 | attempt = x + 1 47 | post_data = self.check_in_post_data(confirmation_number, first_name, last_name) 48 | check_in = RestClient.post("#{CHECK_IN_URL}", post_data.to_json, JSON.parse(File.read(ENV["LUV_HEADERS_FILE"]))) 49 | break 50 | rescue RestClient::ExceptionWithResponse => ewr 51 | sleep(0.5) 52 | next unless x == num_attempts - 1 53 | 54 | raise 55 | end 56 | end 57 | 58 | end_time = Time.now 59 | boarding_positions = "" 60 | 61 | check_in_json = JSON.parse(check_in) 62 | flights = check_in_json["checkInConfirmationPage"]["flights"] 63 | 64 | # make the output more user friendly 65 | flights.each_with_index do |flight, x| 66 | boarding_positions << flight["originAirportCode"] << "\n" 67 | 68 | flight["passengers"].each do |passenger| 69 | boarding_positions << "- #{passenger["name"]} (#{passenger["boardingGroup"]}#{passenger["boardingPosition"]})" << "\n" 70 | end 71 | 72 | boarding_positions << "\n" unless x == flights.size - 1 73 | end 74 | 75 | metadata = { 76 | end_time: end_time.strftime("%I:%M:%S"), 77 | elapsed_time: (end_time - start_time).round(2), 78 | attempts: attempt, 79 | } 80 | 81 | Autoluv::notify_user(true, confirmation_number, first_name, last_name, { to: to, bcc: bcc, boarding_positions: boarding_positions, metadata: metadata }) 82 | end 83 | 84 | private 85 | def self.headers 86 | # required now for all API calls 87 | DEFAULT_HEADERS.merge({ "X-User-Experience-ID": SecureRandom.uuid }) 88 | end 89 | 90 | def self.departing_flights(confirmation_number, first_name, last_name) 91 | reservation = RestClient.get("#{RESERVATION_URL}/#{confirmation_number}?first-name=#{first_name}&last-name=#{last_name}", self.headers) 92 | reservation_json = JSON.parse(reservation) 93 | 94 | airport_time_zones = JSON.parse(File.read(TIME_ZONES_PATH)) 95 | 96 | departing_flights = reservation_json["viewReservationViewPage"]["bounds"].map do |bound| 97 | airport_code = bound["departureAirport"]["code"] 98 | 99 | { 100 | airport_code: airport_code, 101 | departure_date: bound["departureDate"], 102 | departure_time: bound["departureTime"], 103 | time_zone: airport_time_zones[airport_code], 104 | } 105 | end 106 | end 107 | 108 | def self.check_in_post_data(confirmation_number, first_name, last_name) 109 | check_in = RestClient.get("#{CHECK_IN_URL}/#{confirmation_number}?first-name=#{first_name}&last-name=#{last_name}", self.headers) 110 | check_in_json = JSON.parse(check_in) 111 | check_in_json["checkInViewReservationPage"]["_links"]["checkIn"]["body"] 112 | end 113 | 114 | def self.check_in_time(flight) 115 | tz_abbreviation = TZInfo::Timezone.get(flight[:time_zone]).current_period.offset.abbreviation.to_s 116 | 117 | # 2020-09-21 13:15 CDT 118 | departure_time = Time.parse("#{flight[:departure_date]} #{flight[:departure_time]} #{tz_abbreviation}") 119 | 120 | # subtract a day (in seconds) to know when we can check in 121 | check_in_time = departure_time - (24 * 60 * 60) 122 | 123 | # compensate for our server time zone 124 | check_in_time -= (departure_time.utc_offset - Time.now.utc_offset) 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Southwest Checkin 2 | 3 | Automatically check in to Southwest flights using this easy-to-use gem. It'll also email you the results so you know ASAP whether a check in was successful. Seconds count when you're fighting for that window or aisle seat! 4 | 5 | ## ⚠️ February 2023 Update ⚠️ 6 | 7 | It appears [southwest-headers](https://github.com/byalextran/southwest-headers) using the latest versions of Chrome (110) and undetected-chromedriver (3.4.6) produces an invalid header file. [Read this thread for a workaround](https://github.com/byalextran/southwest-headers/issues/6). 8 | 9 | ## Requirements 10 | 11 | * A *nix-based server that will be on when you need to check in 12 | * Ruby 2.3 or higher 13 | * The `at` command 14 | 15 | Tested and working on an Ubuntu 20.04 server (hosted by DigitalOcean). 16 | 17 | ## Installation 18 | 19 | ### Get Southwest Headers 20 | 21 | The Southwest API now requires some randomly generated headers to check in. Because these headers change at a set interval, they need to be constantly refreshed for any check-in script (like this one) to work. To get these headers, please follow the instructions here: 22 | 23 | https://github.com/byalextran/southwest-headers 24 | 25 | Once a cron job has been scheduled, do these steps: 26 | 27 | **Step 1:** Create the file `.autoluv.env` in your user's home directory. 28 | 29 | nano ~/.autoluv.env 30 | 31 | **Step 2:** Copy/paste the following into the text editor. 32 | 33 | LUV_HEADERS_FILE = /PATH/TO/southwest_headers.json 34 | 35 | **Step 3:** Update the path (and filename if changed from the default) to the correct path of the headers file. 36 | 37 | **Step 4:** Hit `Ctrl+O` to save the file and then `Ctrl+X` to exit the text editor. 38 | 39 | ### Install Gem 40 | 41 | gem install autoluv 42 | 43 | ## Usage 44 | 45 | ### Schedule a Check-In 46 | 47 | autoluv schedule ABCDEF John Doe 48 | 49 | Both departing and returning flights (if applicable) will be scheduled for all passengers tied to the confirmation number. After scheduling, there's no need to keep the terminal window open or active. The check in will happen behind the scenes at the appropriate time. 50 | 51 | **Note:** If a first or last name includes a space, wrap it in double quotes (e.g. "Mary Kate") 52 | 53 | ### Schedule a Check-In With Email Notification 54 | 55 | Before using this command, follow the instructions below to configure the required settings. 56 | 57 | autoluv schedule ABCDEF John Doe john.doe@email.com optional@bcc.com 58 | 59 | The second email address is optional and will be BCCed the results. 60 | 61 | ### Check In Immediately 62 | 63 | autoluv checkin ABCDEF John Doe 64 | 65 | ## Configure Email Notifications 66 | 67 | This is optional, however, highly recommended. Especially if a scheduled check-in fails, you'll get notified and can manually check in. Every second counts! 68 | 69 | Boarding positions are shown for each passenger when a check-in succeeds. 70 | 71 | **Step 1:** Open/create the file `.autoluv.env` in your user's home directory. 72 | 73 | nano ~/.autoluv.env 74 | 75 | **Step 2:** Copy/paste the following into the text editor. 76 | 77 | ``` 78 | LUV_FROM_EMAIL = from@email.com 79 | LUV_USER_NAME = from@email.com 80 | LUV_PASSWORD = supersecurepassword 81 | LUV_SMTP_SERVER = smtp.email.com 82 | LUV_PORT = 587 83 | ``` 84 | 85 | **Step 3:** Replace the values with the appropriate SMTP settings for your email provider. `LUV_FROM_EMAIL` should be the email address associated with `LUV_USER_NAME`. 86 | 87 | If your email account has two-factor authentication enabled, be sure to use an app-specific password and *not* your account password. 88 | 89 | **Step 4:** Hit `Ctrl+O` to save the file and then `Ctrl+X` to exit the text editor. 90 | 91 | ### Test Email Notifications 92 | 93 | To verify your SMTP settings, schedule a check-in with invalid information and a valid email address. 94 | 95 | autoluv schedule AAAAAA Fake Name valid@email.com 96 | 97 | If everything is set up correctly, you'll get an email notifying you of an unsuccessful check-in. 98 | 99 | ### Get Text Instead of Email Notifications 100 | 101 | [Use this Zap](https://zapier.com/apps/email/integrations/sms/9241/get-sms-alerts-for-new-email-messages) to get a custom Zapier email address that forwards emails as text messages. It's handy for people like me who don't have email notifications enabled on their phone or computer and want check-in results ASAP. 102 | 103 | ## Manage Check-Ins 104 | 105 | `autoluv` uses the `at` command behind the scenes to check in at a specific time. Use the related `atq` and `atrm` commands to manage check-ins. 106 | 107 | ### View Scheduled Check-Ins 108 | Make note of the first column's number. 109 | 110 | atq 111 | 11 Tue Sep 22 08:05:00 2020 a user 112 | 12 Mon Sep 28 15:45:00 2020 a user 113 | 7 Wed Sep 23 11:40:00 2020 a user 114 | 115 | ### Cancel a Check-In 116 | 117 | atrm 11 118 | 119 | ### View Check-In Details 120 | The last line in the output will show you the confirmation number and name. 121 | 122 | at -c 11 123 | 124 | ### View All Scheduled Check-In Details 125 | Display the list of all scheduled confirmation numbers and names. 126 | 127 | atq | awk '{ print "at -c " $1 }' | bash | grep autoluv 128 | 129 | ## Update Gem 130 | 131 | gem update autoluv --conservative 132 | 133 | ## Contributing 134 | 135 | Bug reports and pull requests are welcome. 136 | 137 | ## License 138 | 139 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 140 | --------------------------------------------------------------------------------