@ -10,9 +10,6 @@ import os
import sys
import sys
import time
import time
import re
import re
import smtplib
from email . mime . text import MIMEText
from email . mime . multipart import MIMEMultipart
from datetime import datetime , timedelta , timezone
from datetime import datetime , timedelta , timezone
from typing import Dict , List , Optional , Any
from typing import Dict , List , Optional , Any
from dataclasses import dataclass , field
from dataclasses import dataclass , field
@ -87,18 +84,6 @@ class Reservation:
courts : List [ CourtReservation ]
courts : List [ CourtReservation ]
@dataclass
class EmailConfig :
enabled : bool
smtp_server : str
smtp_port : int
smtp_username : str
smtp_password : str
from_address : str
to_addresses : List [ str ]
use_tls : bool = True
@dataclass
@dataclass
class Config :
class Config :
base_api_url : str
base_api_url : str
@ -112,7 +97,6 @@ class Config:
courts : Dict [ str , Court ]
courts : Dict [ str , Court ]
reservation_behaviors : Dict [ str , ReservationBehavior ]
reservation_behaviors : Dict [ str , ReservationBehavior ]
reservations : List [ Reservation ]
reservations : List [ Reservation ]
email_config : Optional [ EmailConfig ] = None
@dataclass
@dataclass
@ -273,30 +257,6 @@ def load_config(config_path: str = "config.json") -> Config:
courts = court_reservations
courts = court_reservations
) )
) )
# Parse email configuration (optional)
email_config = None
if " Email " in data :
email_data = data [ " Email " ]
if email_data . get ( " Enabled " , False ) :
required_email_fields = [ " SmtpServer " , " SmtpPort " , " SmtpUsername " , " SmtpPassword " , " FromAddress " , " ToAddresses " ]
for field_name in required_email_fields :
if field_name not in email_data :
raise ValueError ( f " Email is enabled but missing required field: { field_name } " )
if not isinstance ( email_data [ " ToAddresses " ] , list ) or len ( email_data [ " ToAddresses " ] ) == 0 :
raise ValueError ( " Email ToAddresses must be a non-empty list of email addresses " )
email_config = EmailConfig (
enabled = True ,
smtp_server = email_data [ " SmtpServer " ] ,
smtp_port = email_data [ " SmtpPort " ] ,
smtp_username = email_data [ " SmtpUsername " ] ,
smtp_password = email_data [ " SmtpPassword " ] ,
from_address = email_data [ " FromAddress " ] ,
to_addresses = email_data [ " ToAddresses " ] ,
use_tls = email_data . get ( " UseTLS " , True )
)
return Config (
return Config (
base_api_url = data [ " BaseAPIUrl " ] ,
base_api_url = data [ " BaseAPIUrl " ] ,
company_id = data [ " CompanyId " ] ,
company_id = data [ " CompanyId " ] ,
@ -308,8 +268,7 @@ def load_config(config_path: str = "config.json") -> Config:
accounts = accounts ,
accounts = accounts ,
courts = courts ,
courts = courts ,
reservation_behaviors = behaviors ,
reservation_behaviors = behaviors ,
reservations = reservations ,
reservations = reservations
email_config = email_config
)
)
@ -386,106 +345,6 @@ class HarborBot:
with open ( self . success_file , ' w ' ) as f :
with open ( self . success_file , ' w ' ) as f :
json . dump ( self . success_tracking , f , indent = 2 )
json . dump ( self . success_tracking , f , indent = 2 )
def _send_success_email ( self , court : Court , account : Account , reservation_datetime : str ) :
"""
Send email notification for successful booking .
Args :
court : The court that was booked
account : The account the booking was made under
reservation_datetime : The datetime of the reservation
"""
if not self . config . email_config or not self . config . email_config . enabled :
return
email_cfg = self . config . email_config
# Parse the reservation datetime for display
try :
# Format: "2026-01-19T17:30:00-08:00"
dt_part = reservation_datetime [ : 19 ]
tz_part = reservation_datetime [ 19 : ]
dt = datetime . strptime ( dt_part , " % Y- % m- %d T % H: % M: % S " )
formatted_date = dt . strftime ( " % A, % B %d , % Y " )
formatted_time = dt . strftime ( " % I: % M % p " )
except :
formatted_date = reservation_datetime
formatted_time = " "
subject = f " ✅ Court Booked: { court . name } on { formatted_date } "
body_text = f """ Harbor Bot - Successful Court Reservation
Court : { court . name }
Date : { formatted_date }
Time : { formatted_time } ( Pacific )
Booked Under :
Name : { account . name }
Account ID : { account . id }
Reservation Details :
Full DateTime : { reservation_datetime }
Court ID : { court . id }
- - -
This is an automated message from Harbor Bot .
"""
body_html = f """
< html >
< body style = " font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; " >
< div style = " background-color: #4CAF50; color: white; padding: 20px; text-align: center; " >
< h1 style = " margin: 0; " > ✅ Court Booked ! < / h1 >
< / div >
< div style = " padding: 20px; background-color: #f9f9f9; " >
< h2 style = " color: #333; margin-top: 0; " > { court . name } < / h2 >
< table style = " width: 100 % ; border-collapse: collapse; " >
< tr >
< td style = " padding: 10px; border-bottom: 1px solid #ddd; " > < strong > Date : < / strong > < / td >
< td style = " padding: 10px; border-bottom: 1px solid #ddd; " > { formatted_date } < / td >
< / tr >
< tr >
< td style = " padding: 10px; border-bottom: 1px solid #ddd; " > < strong > Time : < / strong > < / td >
< td style = " padding: 10px; border-bottom: 1px solid #ddd; " > { formatted_time } ( Pacific ) < / td >
< / tr >
< tr >
< td style = " padding: 10px; border-bottom: 1px solid #ddd; " > < strong > Booked Under : < / strong > < / td >
< td style = " padding: 10px; border-bottom: 1px solid #ddd; " > { account . name } ( ID : { account . id } ) < / td >
< / tr >
< / table >
< / div >
< div style = " padding: 10px; text-align: center; color: #666; font-size: 12px; " >
This is an automated message from Harbor Bot .
< / div >
< / body >
< / html >
"""
try :
msg = MIMEMultipart ( " alternative " )
msg [ " Subject " ] = subject
msg [ " From " ] = email_cfg . from_address
msg [ " To " ] = " , " . join ( email_cfg . to_addresses )
msg . attach ( MIMEText ( body_text , " plain " ) )
msg . attach ( MIMEText ( body_html , " html " ) )
if email_cfg . use_tls :
server = smtplib . SMTP ( email_cfg . smtp_server , email_cfg . smtp_port )
server . starttls ( )
else :
server = smtplib . SMTP_SSL ( email_cfg . smtp_server , email_cfg . smtp_port )
server . login ( email_cfg . smtp_username , email_cfg . smtp_password )
server . sendmail ( email_cfg . from_address , email_cfg . to_addresses , msg . as_string ( ) )
server . quit ( )
print ( f " [EMAIL] Notification sent to { len ( email_cfg . to_addresses ) } recipient(s) " )
except Exception as e :
print ( f " [EMAIL ERROR] Failed to send notification: { e } " )
def _get_reservation_key ( self , reservation_date : str , court : str , account : str ) - > str :
def _get_reservation_key ( self , reservation_date : str , court : str , account : str ) - > str :
""" Generate unique key for tracking reservation success. """
""" Generate unique key for tracking reservation success. """
return f " { reservation_date } _ { court } _ { account } "
return f " { reservation_date } _ { court } _ { account } "
@ -831,7 +690,6 @@ This is an automated message from Harbor Bot.
if result . get ( " Success " ) :
if result . get ( " Success " ) :
print ( f " [SUCCESS] Booked { court . name } for { account . name } " )
print ( f " [SUCCESS] Booked { court . name } for { account . name } " )
self . _mark_reservation_successful ( reservation_date_str , court_res . court , court_res . account )
self . _mark_reservation_successful ( reservation_date_str , court_res . court , court_res . account )
self . _send_success_email ( court , account , reservation_datetime )
break
break
else :
else :
print ( f " [FAILED] { result . get ( ' Message ' , ' Unknown error ' ) } " )
print ( f " [FAILED] { result . get ( ' Message ' , ' Unknown error ' ) } " )
@ -866,9 +724,20 @@ This is an automated message from Harbor Bot.
self . _print_upcoming_reservations ( )
self . _print_upcoming_reservations ( )
last_status_print = datetime . now ( )
last_status_print = datetime . now ( )
last_date = datetime . now ( ) . date ( )
while True :
while True :
try :
try :
# Check for day change
current_date = datetime . now ( ) . date ( )
if current_date != last_date :
print ( f " \n [INFO] { ' = ' * 50 } " )
print ( f " [INFO] New day detected: { current_date . strftime ( ' % A, % B %d , % Y ' ) } " )
print ( f " [INFO] { ' = ' * 50 } " )
self . _print_upcoming_reservations ( )
last_date = current_date
last_status_print = datetime . now ( )
self . _daemon_iteration ( dry_run )
self . _daemon_iteration ( dry_run )
# Print status every 30 minutes
# Print status every 30 minutes
@ -888,6 +757,7 @@ This is an automated message from Harbor Bot.
traceback . print_exc ( )
traceback . print_exc ( )
print ( " [INFO] Sleeping 60s before retry... " )
print ( " [INFO] Sleeping 60s before retry... " )
time . sleep ( 60 )
time . sleep ( 60 )
time . sleep ( 60 )
def _print_upcoming_reservations ( self ) :
def _print_upcoming_reservations ( self ) :
""" Print status of upcoming reservations. """
""" Print status of upcoming reservations. """
@ -1013,7 +883,6 @@ This is an automated message from Harbor Bot.
if result . get ( " Success " ) :
if result . get ( " Success " ) :
print ( f " [SUCCESS] Booked { court . name } for { account . name } " )
print ( f " [SUCCESS] Booked { court . name } for { account . name } " )
self . _mark_reservation_successful ( reservation_date_str , court_res . court , court_res . account )
self . _mark_reservation_successful ( reservation_date_str , court_res . court , court_res . account )
self . _send_success_email ( court , account , reservation_datetime )
break
break
else :
else :
print ( f " [FAILED] { result . get ( ' Message ' , ' Unknown error ' ) } " )
print ( f " [FAILED] { result . get ( ' Message ' , ' Unknown error ' ) } " )
@ -1106,15 +975,6 @@ This is an automated message from Harbor Bot.
print ( f " - { res . day } @ { res . time } ( { res . reservation_behavior } ) { trigger_info } " )
print ( f " - { res . day } @ { res . time } ( { res . reservation_behavior } ) { trigger_info } " )
for cr in res . courts :
for cr in res . courts :
print ( f " { cr . court } -> { cr . account } " )
print ( f " { cr . court } -> { cr . account } " )
print ( f " \n Email Notifications: " )
if self . config . email_config and self . config . email_config . enabled :
print ( f " Status: ENABLED " )
print ( f " SMTP Server: { self . config . email_config . smtp_server } : { self . config . email_config . smtp_port } " )
print ( f " From: { self . config . email_config . from_address } " )
print ( f " To: { ' , ' . join ( self . config . email_config . to_addresses ) } " )
else :
print ( f " Status: DISABLED " )
def test_booking ( self , court_name : str , account_name : str , date_str : str , time_str : str , dry_run : bool = True ) :
def test_booking ( self , court_name : str , account_name : str , date_str : str , time_str : str , dry_run : bool = True ) :
"""
"""
@ -1157,132 +1017,10 @@ This is an automated message from Harbor Bot.
if result . get ( " Success " ) :
if result . get ( " Success " ) :
print ( f " \n [SUCCESS] Booked { court . name } for { account . name } " )
print ( f " \n [SUCCESS] Booked { court . name } for { account . name } " )
self . _send_success_email ( court , account , reservation_datetime )
else :
else :
print ( f " \n [FAILED] { result . get ( ' Message ' , ' Unknown error ' ) } " )
print ( f " \n [FAILED] { result . get ( ' Message ' , ' Unknown error ' ) } " )
print ( f " [DEBUG] Full response: { json . dumps ( result , indent = 2 ) } " )
print ( f " [DEBUG] Full response: { json . dumps ( result , indent = 2 ) } " )
def test_email ( self ) :
""" Send a test email to verify email configuration. """
if not self . config . email_config :
print ( " [ERROR] No email configuration found in config.json " )
print ( " [INFO] Add an ' Email ' section to your config.json file " )
return False
if not self . config . email_config . enabled :
print ( " [ERROR] Email is disabled in configuration " )
print ( " [INFO] Set ' Enabled ' : true in the Email section of config.json " )
return False
email_cfg = self . config . email_config
print ( f " \n [TEST EMAIL] Sending test email... " )
print ( f " SMTP Server: { email_cfg . smtp_server } : { email_cfg . smtp_port } " )
print ( f " From: { email_cfg . from_address } " )
print ( f " To: { ' , ' . join ( email_cfg . to_addresses ) } " )
print ( f " TLS: { email_cfg . use_tls } " )
# Create test email content
subject = " 🏓 Harbor Bot - Test Email "
body_text = f """ Harbor Bot - Email Test
This is a test email from Harbor Bot to verify your email configuration is working correctly .
Configuration :
SMTP Server : { email_cfg . smtp_server } : { email_cfg . smtp_port }
From : { email_cfg . from_address }
TLS Enabled : { email_cfg . use_tls }
If you received this email , your configuration is correct !
- - -
Sent at : { datetime . now ( ) . strftime ( ' % Y- % m- %d % H: % M: % S ' ) }
"""
body_html = f """
< html >
< body style = " font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; " >
< div style = " background-color: #2196F3; color: white; padding: 20px; text-align: center; " >
< h1 style = " margin: 0; " > 🏓 Harbor Bot < / h1 >
< p style = " margin: 10px 0 0 0; " > Email Test < / p >
< / div >
< div style = " padding: 20px; background-color: #f9f9f9; " >
< p > This is a test email from Harbor Bot to verify your email configuration is working correctly . < / p >
< h3 style = " color: #333; " > Configuration < / h3 >
< table style = " width: 100 % ; border-collapse: collapse; " >
< tr >
< td style = " padding: 8px; border-bottom: 1px solid #ddd; " > < strong > SMTP Server : < / strong > < / td >
< td style = " padding: 8px; border-bottom: 1px solid #ddd; " > { email_cfg . smtp_server } : { email_cfg . smtp_port } < / td >
< / tr >
< tr >
< td style = " padding: 8px; border-bottom: 1px solid #ddd; " > < strong > From : < / strong > < / td >
< td style = " padding: 8px; border-bottom: 1px solid #ddd; " > { email_cfg . from_address } < / td >
< / tr >
< tr >
< td style = " padding: 8px; border-bottom: 1px solid #ddd; " > < strong > TLS Enabled : < / strong > < / td >
< td style = " padding: 8px; border-bottom: 1px solid #ddd; " > { email_cfg . use_tls } < / td >
< / tr >
< / table >
< p style = " margin-top: 20px; padding: 15px; background-color: #e8f5e9; border-radius: 5px; color: #2e7d32; " >
✅ If you received this email , your configuration is correct !
< / p >
< / div >
< div style = " padding: 10px; text-align: center; color: #666; font-size: 12px; " >
Sent at : { datetime . now ( ) . strftime ( ' % Y- % m- %d % H: % M: % S ' ) }
< / div >
< / body >
< / html >
"""
try :
msg = MIMEMultipart ( " alternative " )
msg [ " Subject " ] = subject
msg [ " From " ] = email_cfg . from_address
msg [ " To " ] = " , " . join ( email_cfg . to_addresses )
msg . attach ( MIMEText ( body_text , " plain " ) )
msg . attach ( MIMEText ( body_html , " html " ) )
print ( f " \n [INFO] Connecting to SMTP server... " )
if email_cfg . use_tls :
server = smtplib . SMTP ( email_cfg . smtp_server , email_cfg . smtp_port )
server . set_debuglevel ( 1 ) # Enable debug output
print ( f " [INFO] Starting TLS... " )
server . starttls ( )
else :
server = smtplib . SMTP_SSL ( email_cfg . smtp_server , email_cfg . smtp_port )
server . set_debuglevel ( 1 )
print ( f " [INFO] Logging in as { email_cfg . smtp_username } ... " )
server . login ( email_cfg . smtp_username , email_cfg . smtp_password )
print ( f " [INFO] Sending email... " )
server . sendmail ( email_cfg . from_address , email_cfg . to_addresses , msg . as_string ( ) )
server . quit ( )
print ( f " \n [SUCCESS] Test email sent successfully! " )
print ( f " [INFO] Check inbox of: { ' , ' . join ( email_cfg . to_addresses ) } " )
return True
except smtplib . SMTPAuthenticationError as e :
print ( f " \n [ERROR] Authentication failed: { e } " )
print ( f " \n [HELP] For Gmail: " )
print ( f " 1. Enable 2-factor authentication on your Google account " )
print ( f " 2. Go to: https://myaccount.google.com/apppasswords " )
print ( f " 3. Generate an App Password for ' Mail ' " )
print ( f " 4. Use the 16-character app password (no spaces) as SmtpPassword " )
return False
except smtplib . SMTPException as e :
print ( f " \n [ERROR] SMTP error: { e } " )
return False
except Exception as e :
print ( f " \n [ERROR] Failed to send test email: { e } " )
return False
def main ( ) :
def main ( ) :
@ -1292,7 +1030,6 @@ def main():
parser = argparse . ArgumentParser ( description = " Harbor Isles Pickleball Court Reservation Bot " )
parser = argparse . ArgumentParser ( description = " Harbor Isles Pickleball Court Reservation Bot " )
parser . add_argument ( " --config " , default = " config.json " , help = " Path to configuration file " )
parser . add_argument ( " --config " , default = " config.json " , help = " Path to configuration file " )
parser . add_argument ( " --test-login " , action = " store_true " , help = " Test login functionality " )
parser . add_argument ( " --test-login " , action = " store_true " , help = " Test login functionality " )
parser . add_argument ( " --test-email " , action = " store_true " , help = " Send a test email to verify email configuration " )
parser . add_argument ( " --show-config " , action = " store_true " , help = " Display loaded configuration " )
parser . add_argument ( " --show-config " , action = " store_true " , help = " Display loaded configuration " )
parser . add_argument ( " --run " , action = " store_true " , help = " Run once and check for reservations (one-shot mode) " )
parser . add_argument ( " --run " , action = " store_true " , help = " Run once and check for reservations (one-shot mode) " )
parser . add_argument ( " --daemon " , action = " store_true " , help = " Run continuously as a daemon (recommended) " )
parser . add_argument ( " --daemon " , action = " store_true " , help = " Run continuously as a daemon (recommended) " )
@ -1319,9 +1056,6 @@ def main():
if args . test_login :
if args . test_login :
bot . test_login ( )
bot . test_login ( )
if args . test_email :
bot . test_email ( )
if args . test_book :
if args . test_book :
court , account , date , time_val = args . test_book
court , account , date , time_val = args . test_book
dry_run = not args . live
dry_run = not args . live
@ -1335,7 +1069,7 @@ def main():
dry_run = not args . live
dry_run = not args . live
bot . run_daemon ( dry_run = dry_run )
bot . run_daemon ( dry_run = dry_run )
if not any ( [ args . show_config , args . test_login , args . test_email, args . run, args . test_book , args . daemon ] ) :
if not any ( [ args . show_config , args . test_login , args . run, args . test_book , args . daemon ] ) :
parser . print_help ( )
parser . print_help ( )
except FileNotFoundError as e :
except FileNotFoundError as e :