├── .gitignore ├── README.md └── bbdc_bot.py /.gitignore: -------------------------------------------------------------------------------- 1 | cache 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | Detached fork from https://github.com/rohit0718/BBDC-Bot. Refer to previous upstream. 4 | 5 | ## Background 6 | The upstream code does not work for class 2b (bike) and had logic built to mass book for class 3 (car). Revisited the logic flow and did not integrate auto booking as it requires cancellation step. Moved from hassle of installing selenium to using selenium-chrome docker image. Using podman as I prefer it over docker :) 7 | 8 | ## Changelog 9 | ### Added 10 | - Added notification to telegram 11 | ### Updated 12 | - Edited all code to be non deprecated 13 | - Edited logic flow as initial process does not match existing BBDC site 14 | ### Remove 15 | - Original code was meant for mass booking of class 3 (car). Not well suited for class 2b (bike) 16 | - Remove auto booking 17 | 18 | # Prereqs 19 | 1. Install and setup environment 20 | ``` 21 | dnf upgrade -y 22 | dnf install podman -y 23 | podman run --privileged -d --network host --name chrome docker.io/selenium/standalone-chrome 24 | cd /etc/systemd/user 25 | podman generate systemd --new --files --name chrome 26 | chmod 755 container-chrome.service 27 | ln -s /etc/systemd/user/container-chrome.service /etc/systemd/system/container-chrome.service 28 | systemctl daemon-reload 29 | systemctl enable container-chrome.service 30 | systemctl start container-chrome.service 31 | ``` 32 | 33 | 2. Append variables to /etc/environment 34 | ``` 35 | vi /etc/environment 36 | chatid= 37 | teleid= 38 | BOOKING_PASSWORD= 39 | BOOKING_USER= 40 | ``` 41 | 42 | 3. Setup Cron and run script every 30min 43 | ``` 44 | dnf install cronie cronie-anacron -y 45 | 46 | sudo crontab -e 47 | */30 * * * * /usr/bin/python3 /root/BBDC-Bot/bbdc_bot.py >> ~/cron.log 2>&1 48 | ``` 49 | -------------------------------------------------------------------------------- /bbdc_bot.py: -------------------------------------------------------------------------------- 1 | from selenium import webdriver 2 | from selenium.webdriver.common.by import By 3 | from selenium.webdriver.support.ui import WebDriverWait 4 | from selenium.webdriver.support import expected_conditions as EC 5 | from selenium.webdriver.chrome.service import Service 6 | from webdriver_manager.chrome import ChromeDriverManager 7 | from selenium.webdriver.common.desired_capabilities import DesiredCapabilities 8 | from selenium.webdriver.chrome.options import Options 9 | import os 10 | import subprocess 11 | from datetime import datetime, timedelta 12 | import urllib.request 13 | import ssl 14 | import json 15 | import hashlib 16 | 17 | server ="http://127.0.0.1:4444/wd/hub" 18 | 19 | # Import env values for telegram 20 | USER = os.getenv('BOOKING_USER') 21 | PASSWORD = os.getenv('BOOKING_PASSWORD') 22 | 23 | # Import env values for telegram 24 | chatid = os.getenv('chatid') #replace if hardcode needed 25 | teleid = os.getenv('teleid') #replace if hardcode needed 26 | telelink = "https://api.telegram.org/" + teleid + "/{}?{}" 27 | 28 | def broadcastMessage(elink,einfo): 29 | headers = {"Accept": "application/json"} 30 | myssl = ssl._create_unverified_context() 31 | params = {"text": einfo} 32 | params.update({"chat_id": chatid}) 33 | url = elink.format("sendMessage", urllib.parse.urlencode(params)) 34 | request = urllib.request.Request(url, None, headers) 35 | with urllib.request.urlopen(request, context=myssl) as r: 36 | r.read() 37 | 38 | 39 | def loginMainPage(browser): 40 | # Load Main Login Page 41 | browser.get('https://info.bbdc.sg/members-login/') 42 | print("Main Login Page Loaded...") 43 | 44 | # Input Username, Password and Login 45 | idLogin = browser.find_element(By.ID, "txtNRIC") 46 | idLogin.send_keys(USER) 47 | idLogin = browser.find_element(By.ID, "txtPassword") 48 | idLogin.send_keys(PASSWORD) 49 | loginButton = browser.find_element(By.ID, "loginbtn") 50 | loginButton.click() 51 | print("...Clicking Log In") 52 | 53 | # Accept Insecure and Choose default enrolled course 54 | proceedBtn = browser.find_element(By.ID, 'proceed-button') 55 | proceedBtn.click() 56 | submitBtn = browser.find_element(By.NAME,'btnSubmit') 57 | submitBtn.click() 58 | print("Main Booking Page Loaded...") 59 | 60 | def getAvailableBooking(browser): 61 | # Switching to Left Frame and accessing element by text 62 | browser.switch_to.default_content() 63 | frame = browser.find_element(By.NAME, 'leftFrame') 64 | browser.switch_to.frame(frame) 65 | bookingBtn = browser.find_element(By.CSS_SELECTOR, "a[href*='../b-2-pLessonBooking.asp?limit=pl']"); 66 | bookingBtn.click() 67 | print("...Clicking Practical Booking") 68 | 69 | # Selection menu 70 | browser.switch_to.default_content() 71 | wait = WebDriverWait(browser, 300) 72 | wait.until(EC.frame_to_be_available_and_switch_to_it(browser.find_element(By.NAME, 'mainFrame'))) 73 | wait.until(EC.visibility_of_element_located((By.ID, "checkMonth"))) 74 | print("Main Frame Practical Booking Page Loaded...") 75 | 76 | # 0 refers to first month, 1 refers to second month, and so on... 77 | months = browser.find_elements(By.ID, 'checkMonth') 78 | months[12].click() # all months 79 | # 0 refers to first session, 1 refers to second session, and so on... 80 | sessions = browser.find_elements(By.ID, 'checkSes') 81 | sessions[8].click() # all sessions 82 | # 0 refers to first day, 1 refers to second day, and so on... 83 | days = browser.find_elements(By.ID, 'checkDay') 84 | days[7].click() # all days 85 | # Selecting Search 86 | browser.find_element(By.NAME, 'btnSearch').click() 87 | print("...Clicking Search after checking all boxes") 88 | 89 | # Dismissing Prompt 90 | wait = WebDriverWait(browser, 300) 91 | wait.until(EC.alert_is_present()) 92 | alert_obj = browser.switch_to.alert 93 | alert_obj.accept() 94 | wait.until(EC.visibility_of_element_located((By.NAME, "slot"))) 95 | print("===Dismissed Prompt===") 96 | 97 | # 0 refers to first slot, 1 refers to second slot, and so on... 98 | slots = browser.find_elements(By.NAME, 'slot') 99 | timeinfoList = [] 100 | for slot in slots: # Selecting all checkboxes 101 | timeslot = slot.find_element(By.XPATH, './..').get_attribute('onmouseover') 102 | timeinfo = timeslot[20:58].replace('"', '').split(",") 103 | timeinfoList.append(timeinfo) 104 | return timeinfoList 105 | #print(timeinfoList) 106 | 107 | def getExistingBooking(browser): 108 | # Switching to Left Frame and accessing element by text 109 | browser.switch_to.default_content() 110 | frame = browser.find_element(By.NAME, 'leftFrame') 111 | browser.switch_to.frame(frame) 112 | bookingBtn = browser.find_element(By.CSS_SELECTOR, "a[href*='../b-bookTestStatement.asp']"); 113 | bookingBtn.click() 114 | print("...Clicking Booking Statement") 115 | 116 | # Booking Statement Page 117 | browser.switch_to.default_content() 118 | wait = WebDriverWait(browser, 300) 119 | wait.until(EC.frame_to_be_available_and_switch_to_it(browser.find_element(By.NAME, 'mainFrame'))) 120 | wait.until(EC.visibility_of_element_located((By.NAME, "btnHome"))) 121 | print("Main Frame for Booking Statement Page Loaded...") 122 | 123 | try: 124 | bookedDate = browser.find_element(By.XPATH, '/html/body/table/tbody/tr/td[2]/form/table/tbody/tr[5]/td/table/tbody/tr[2]/td[1]').text 125 | bookedSession = browser.find_element(By.XPATH, '/html/body/table/tbody/tr/td[2]/form/table/tbody/tr[5]/td/table/tbody/tr[2]/td[2]').text 126 | bookedTime = browser.find_element(By.XPATH, '/html/body/table/tbody/tr/td[2]/form/table/tbody/tr[5]/td/table/tbody/tr[2]/td[3]').text 127 | existingBooking = bookedDate + "," + bookedSession + "," + bookedTime 128 | print("My Existing Booking: " + existingBooking) 129 | return existingBooking 130 | except: 131 | return "None" 132 | 133 | def end(): 134 | # Restart container 135 | print("...Restarting Container...") 136 | subprocess.run(["systemctl", "restart", "container-chrome.service"]) 137 | quit() 138 | 139 | try: 140 | browser_options = webdriver.ChromeOptions() 141 | browser_options.accept_untrusted_certs = True 142 | browser = webdriver.Remote(command_executor=server,options=browser_options) 143 | 144 | loginMainPage(browser) 145 | existingBooking = getExistingBooking(browser) 146 | timeinfoList = getAvailableBooking(browser) 147 | 148 | # Dummy data for troubleshooting 149 | #timeinfoList = [] 150 | #timeinfoList.append(['23/01/2022 (Wed)', '6', '17:10', '18:50']) 151 | #timeinfoList.append(['02/02/2022 (Wed)', '6', '17:10', '18:50']) 152 | #timeinfoList.append(['29/01/2022 (Wed)', '6', '17:10', '18:50']) 153 | #timeinfoList.append(['30/01/2022 (Wed)', '6', '17:10', '18:50']) 154 | #timeinfoList.append(['31/01/2022 (Wed)', '6', '17:10', '18:50']) 155 | #timeinfoList.append(['01/02/2022 (Wed)', '6', '17:10', '18:50']) 156 | #timeinfoList.append(['02/02/2022 (Wed)', '6', '17:10', '18:50']) 157 | #timeinfoList.append(['03/02/2022 (Wed)', '6', '17:10', '18:50']) 158 | 159 | # Set the number of days with slots to return based on day the script runs 160 | now_date = datetime.today() 161 | eta_date_wkday = now_date + timedelta(6) # Return 6day of available slots if script run during weekday 162 | eta_date_wkend = now_date + timedelta(10) # Return 10day of available slots if script run during weekend 163 | if now_date.weekday() >= 0 and now_date.weekday() <= 4: 164 | eta_date = eta_date_wkday 165 | else: 166 | eta_date = eta_date_wkend 167 | print("Looking for bookings before: " + str(eta_date)) 168 | 169 | # Loop through all bookings and to create list of results within number of days 170 | resultList = [] 171 | for t in timeinfoList: 172 | dtobj = datetime.strptime(t[0][0:10], "%d/%m/%Y") 173 | if dtobj <= eta_date: 174 | resultList.append(' '.join(t)) 175 | else: 176 | break 177 | print("Results to send to notification: " + str(resultList)) 178 | 179 | # Read file for hash from previous run 180 | # Hash the current returned data results 181 | # Comparing previous and current run hash to prevent spaming notification if no new slot available 182 | try: 183 | f = open("cache", "r+") 184 | h1 = f.read() 185 | except: 186 | h1 = "" 187 | mptdata = '\n'.join(resultList) 188 | h2 = hashlib.sha1(mptdata.encode("UTF-8")).hexdigest() 189 | 190 | print("Hash, h1 is " + h1) 191 | print("Hash, h2 is " + h2) 192 | if len(resultList) > 0 and (h1 != h2): 193 | f = open("cache", "w+") 194 | f.write(h2) 195 | f.close() 196 | 197 | # Concate and send message to telegram 198 | mptdata = 'Script Run @ ' + str(datetime.today()) + '\nMy Booking @ ' + existingBooking + '\n\n' + mptdata 199 | broadcastMessage(telelink, mptdata) 200 | 201 | # # Use the following code block for troubleshooting and debugging 202 | # #ids = browser.find_elements_by_xpath('//*[@id]') 203 | # #for ii in ids: 204 | # #print(ii.get_attribute('id')) 205 | # #quit() 206 | 207 | except Exception as e: 208 | print(e) 209 | 210 | print("...Restarting Container...") 211 | subprocess.run(["systemctl", "restart", "container-chrome.service"]) 212 | quit() 213 | --------------------------------------------------------------------------------