-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
267 lines (253 loc) · 11.2 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
// The following program is used to helps users to secure their Revolution spin class spots with their favourite bikes. If the booking is available, it will be made immediately, otherwise it will be scheduled at the exact time in the future when it will be available.
// Language used: Javascript
// Libraries used: Puppeteer, Line Reader, Commander, Node Schedule
const puppeteer = require('puppeteer');
const lineReader = require('line-reader');
const { program } = require('commander');
const schedule = require('node-schedule');
const fs = require('fs');
const cookiesFilePath = 'cookies.json';
const url = "https://revolution.com.sg/reserve#/account"
var signedIn = false;
async function initBrowser(browser) {
const page = await browser.newPage();
// check if previous session exists, if it does, load cookies before page navigation
const prevSess = fs.existsSync(cookiesFilePath);
if (prevSess) {
const cookiesString = fs.readFileSync(cookiesFilePath);
const parsedCookies = JSON.parse(cookiesString);
if (parsedCookies.length !== 0) {
for (let cookie of parsedCookies) {
await page.setCookie(cookie);
}
console.log("Loaded session in browser, user is logged in");
signedIn = true;
}
}
await page.goto(url, { waitUntil: 'networkidle2' });
const title = await page.title();
console.log("Loaded page: " + title);
return page;
}
async function userLogin(page) {
try {
var email = "";
var password = "";
console.log("Obtaining user credentials...");
lineReader.eachLine('credentials.txt', function (line) {
var input = line.split(":").map(function (item) {
return item.trim();
})
if (input[0] == "email") {
email = input[1];
} else if (input[0] == "password") {
password = input[1];
}
})
await new Promise(resolve => setTimeout(resolve, 1000));
console.log("Entering user credentials...");
if (email == "" || password == "") {
throw "ERROR: Error in credentials provided. Please check that you have updated credentials.txt with your correct email and password!"
}
// difficulty with Revolution's webpage is the use of iframes throughout the entire class booking pages
var frameHandle = await page.waitForSelector('#zingfit-embed > iframe');
var frame = await frameHandle.contentFrame();
await frame.waitForSelector('#username');
await frame.type('#username', email);
await frame.waitForSelector('#password');
await frame.type('#password', password);
await page.waitForTimeout(1000);
page.keyboard.press('Enter');
// Save session so we do not have to login again next time
const cookiesObject = await page.cookies();
fs.writeFile(cookiesFilePath, JSON.stringify(cookiesObject), function(err) {
if (err) {
console.log("Unable to save cookies: ", err);
} else {
console.log("Cookies saved successfully!")
}
})
} catch (err) {
throw err;
}
}
async function reserveClass(page, options, diffDays) {
try {
const date = options.date;
const time = options.time;
const location = options.location;
const bikes = options.bikes;
var frameHandle = await page.waitForSelector('#zingfit-embed > iframe');
var frame = await frameHandle.contentFrame();
const element = await frame.$("ul > li:nth-child(2) > a");
await frame.evaluate(el => el.click(), element);
await page.waitForNavigation({ waitUntil: 'networkidle2' });
if (diffDays >= 7) { // navigate to next week
frameHandle = await page.waitForSelector('#zingfit-embed > iframe');
frame = await frameHandle.contentFrame();
const element = await frame.$("#reserveweeknav > li.next > a");
await frame.evaluate(el => el.click(), element);
await page.waitForNavigation({ waitUntil: 'networkidle2' });
}
frameHandle = await page.waitForSelector('#zingfit-embed > iframe');
frame = await frameHandle.contentFrame();
// find class using date, time and location
// filter by location first
console.log("Filtering classes by location...")
switch (location) {
case "cecil": {
let locale = await frame.$("#reserveFilterSites > ul > li:nth-child(1) > a");
await frame.evaluate(el => el.click(), locale);
await page.waitForNavigation({ waitUntil: 'networkidle2' });
break;
}
case "orchard": {
let locale = await frame.$("#reserveFilterSites > ul > li:nth-child(2) > a");
await frame.evaluate(el => el.click(), locale);
await page.waitForNavigation({ waitUntil: 'networkidle2' });
break;
}
case "tanjong": {
let locale = await frame.$("#reserveFilterSites > ul > li:nth-child(3) > a");
await frame.evaluate(el => el.click(), locale);
await page.waitForNavigation({ waitUntil: 'networkidle2' });
break;
}
default:
throw "ERROR: Invalid location provided. Make sure location is cecil / orchard / tanjong!"
}
// use date to select column
// determine column
console.log("Selecting class...");
frameHandle = await page.waitForSelector('#zingfit-embed > iframe');
frame = await frameHandle.contentFrame();
const allDates = await frame.$$eval('#reserve > thead > tr > td > span.thead-date', ths => ths.map((th) => {
return th.innerText;
}));
var idx = -1;
const desiredDate = date.substring(0, 2) + "." + date.substring(2, 4);
for (let i = 0; i < allDates.length; i++) {
if (allDates[i] == desiredDate) {
idx = i;
break;
}
}
// get all classes in a column
const allClassesInDay = await frame.$$('#reserve > tbody > tr > td.day' + idx + ' > div');
// then use time to select class
const desiredTime = convert24hTo12h(time);
var foundClass = false;
var instrName = "";
for (let i = 0; i < allClassesInDay.length; i++) {
instrName = await allClassesInDay[i].$eval("span.scheduleInstruc", n => n.innerText);
const classTime = await allClassesInDay[i].$eval("span.scheduleTime", ct => (ct.innerText).split('\n')[0]);
if (classTime == desiredTime) {
foundClass = true;
console.log("Selected class by " + instrName + " at " + classTime);
await allClassesInDay[i].$eval("a", a => a.click());
await page.waitForNavigation({ waitUntil: 'networkidle2' });
break;
}
}
if (!foundClass) {
throw "ERROR: Unable to find class at " + time + " on " + date + " @ " + location + ". Please check provided date, time and location!"
}
frameHandle = await page.waitForSelector('#zingfit-embed > iframe');
frame = await frameHandle.contentFrame();
// select bike using loop, if successful, break
console.log("Selecting bike...");
// check classname for "Enrolled"
var foundBike = false;
for (let i = 0; i < bikes.length; i++) {
var bikeNum = bikes[i];
console.log("Trying bike " + bikeNum + "...");
const enrolled = await frame.$eval('#spotcell' + bikeNum, e => e.className);
if (enrolled.toLowerCase().includes("enrolled")) {
console.log("Bike " + bikeNum + " is occupied");
continue;
} else {
console.log("Bike " + bikeNum + " is available");
foundBike = true;
await frame.$eval("#spotcell" + bikeNum, a => a.click());
break;
}
}
if (!foundBike) {
throw "ERROR: Unable to find any available bikes for class by " + instrName + " at " + time + " on " + date + " @ " + location + ". Please provide different bike numbers!"
} else {
console.log("SUCCESS! You have secured bike " + bikeNum + " for class by " + instrName + " at " + time + " on " + date + " @ " + location + ".");
}
} catch (err) {
throw err;
}
}
function convert24hTo12h(time) {
var timeString = time;
var H = timeString.substr(0, 2);
var h = H % 12 || 12;
var ampm = (H < 12 || H === 24) ? "AM" : "PM";
timeString = h + ":" + timeString.substr(2, 2) + " " + ampm;
return timeString;
}
async function reserve(options, diffDays) {
// proceed with webpage interaction
const browser = await puppeteer.launch({ headless: false, handleSIGINT: true }); // for testing
try {
const page = await initBrowser(browser);
if (!signedIn) {
await userLogin(page);
}
await page.waitForNavigation({ waitUntil: 'networkidle2' });
await reserveClass(page, options, diffDays);
} catch (err) {
console.log(err);
browser.close();
} finally {
browser.close();
}
}
async function main() {
// handle command line args
program
.version('0.0.1')
.requiredOption('-d, --date <date>', 'class date')
.requiredOption('-t, --time <time>', 'class time')
.requiredOption('-l, --location <location>', 'cecil / orchard / tanjong')
.requiredOption('-b, --bikes <bikes...>', 'bike numbers')
.parse();
const options = program.opts();
// determine if booking window open or we have to schedule booking in the future
const date = options.date;
const day = parseInt(date.substring(0, 2), 10);
const month = parseInt(date.substring(2, 4), 10) - 1;
const year = parseInt(date.substring(4, 6)) + 2000;
const wantedDate = new Date(year, month, day);
const currDate = new Date();
const diffDays = Math.ceil(Math.abs(wantedDate - currDate) / (1000 * 60 * 60 * 24));
if (diffDays == 7) {
if (currDate.getHours() >= 10 && currDate.getMinutes() >= 30) { // booking open
reserve(options, diffDays);
} else { // schedule class
var futureDate = new Date(year, month, day, 10, 30);
futureDate.setDate(futureDate.getDate() - 7);
console.log("Class not available for booking yet, scheduling booking on " + futureDate.toString() + "...");
schedule.scheduleJob(date, function () {
reserve(options, diffDays)
});
}
} else if (diffDays > 7) { // schedule class
var futureDate = new Date(year, month, day, 10, 30);
futureDate.setDate(futureDate.getDate() - 7);
console.log("Class not available for booking yet, scheduling booking on " + futureDate.toString() + "...");
schedule.scheduleJob(date, function () {
reserve(options, diffDays)
});
} else { // book now
reserve(options, diffDays);
}
}
// for graceful shutdown
process.on('SIGINT', () => {
process.exit();
});
main();