truenorth 0.2.6 → 0.2.8
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 +4 -4
- data/lib/truenorth/cli.rb +56 -0
- data/lib/truenorth/client.rb +91 -41
- data/lib/truenorth/version.rb +1 -1
- data/truenorth.gemspec +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ebf93538d8d794249ddcb8f4ddb2832eba001e6758d275c4ea59af595777d50e
|
|
4
|
+
data.tar.gz: e857b71120bf6c327fb83284ad901472656e12f0df542abd75223a953c6ea202
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c4b4e536e26ee506d3ee57b1cc9f186f9696c2f10ae61ab834e4dd40999d36381f1aa01b03096c1eb118ab92c720bef71d1b74f07c18c37fd6050545d13b67ad
|
|
7
|
+
data.tar.gz: '0619e6eb3fcb0220aebdcc6bb13f60c452d157c6e772dbd63596981c0dc4b6fed5cdbd8acf16a3c00916b853791875020651a0a3c5fe326fa8115a93fae5ab76'
|
data/lib/truenorth/cli.rb
CHANGED
|
@@ -97,6 +97,12 @@ module Truenorth
|
|
|
97
97
|
end
|
|
98
98
|
rescue Error => e
|
|
99
99
|
say "Error: #{e.message}", :red
|
|
100
|
+
|
|
101
|
+
# If no slot available, show nearby available times
|
|
102
|
+
if e.message.include?('No slot available')
|
|
103
|
+
show_nearby_availability(client, date, time, activity)
|
|
104
|
+
end
|
|
105
|
+
|
|
100
106
|
exit 1
|
|
101
107
|
end
|
|
102
108
|
|
|
@@ -226,6 +232,56 @@ module Truenorth
|
|
|
226
232
|
|
|
227
233
|
private
|
|
228
234
|
|
|
235
|
+
def show_nearby_availability(client, date, requested_time, activity)
|
|
236
|
+
say "\nChecking available times for #{date}...", :cyan
|
|
237
|
+
|
|
238
|
+
begin
|
|
239
|
+
result = client.availability(date, activity: activity)
|
|
240
|
+
|
|
241
|
+
if result[:slots].empty?
|
|
242
|
+
say "No #{activity} slots available on #{date}", :yellow
|
|
243
|
+
else
|
|
244
|
+
say "\nAvailable #{activity} times:", :green
|
|
245
|
+
|
|
246
|
+
# Parse requested time to find nearby slots
|
|
247
|
+
requested_hour = parse_time_to_minutes(requested_time)
|
|
248
|
+
|
|
249
|
+
# Sort slots by proximity to requested time
|
|
250
|
+
sorted_slots = result[:slots].sort_by do |time_str, _courts|
|
|
251
|
+
slot_minutes = parse_time_to_minutes(time_str)
|
|
252
|
+
(slot_minutes - requested_hour).abs
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Show up to 10 nearest slots
|
|
256
|
+
sorted_slots.first(10).each do |time_str, courts|
|
|
257
|
+
say " #{time_str}: #{courts.join(', ')}"
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
if result[:slots].length > 10
|
|
261
|
+
say "\n ... and #{result[:slots].length - 10} more times", :cyan
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
rescue Error => e
|
|
265
|
+
say "Could not fetch availability: #{e.message}", :yellow
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def parse_time_to_minutes(time_str)
|
|
270
|
+
# Convert "10:00 AM" or "10am" to minutes since midnight
|
|
271
|
+
if time_str =~ /(\d{1,2}):?(\d{2})?\s*(am|pm)/i
|
|
272
|
+
hour = ::Regexp.last_match(1).to_i
|
|
273
|
+
minute = ::Regexp.last_match(2)&.to_i || 0
|
|
274
|
+
period = ::Regexp.last_match(3).upcase
|
|
275
|
+
|
|
276
|
+
hour = 0 if hour == 12 && period == 'AM'
|
|
277
|
+
hour += 12 if period == 'PM' && hour != 12
|
|
278
|
+
|
|
279
|
+
hour * 60 + minute
|
|
280
|
+
else
|
|
281
|
+
0
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
229
285
|
def display_reservations_table(results)
|
|
230
286
|
# Get terminal width
|
|
231
287
|
term_width = begin
|
data/lib/truenorth/client.rb
CHANGED
|
@@ -5,6 +5,7 @@ require 'uri'
|
|
|
5
5
|
require 'nokogiri'
|
|
6
6
|
require 'json'
|
|
7
7
|
require 'base64'
|
|
8
|
+
require 'date'
|
|
8
9
|
|
|
9
10
|
module Truenorth
|
|
10
11
|
# HTTP client for NorthStar facility booking systems
|
|
@@ -283,7 +284,8 @@ module Truenorth
|
|
|
283
284
|
|
|
284
285
|
raise BookingError, 'Could not extract view state' unless view_state
|
|
285
286
|
|
|
286
|
-
#
|
|
287
|
+
# Step 1: Click the cancel button to open the confirmation dialog
|
|
288
|
+
# The dialog button will be enabled in the AJAX response
|
|
287
289
|
ajax_url = "#{@base_url}#{RESERVATIONS_PATH}?p_p_id=memberReservations_WAR_northstarportlet" \
|
|
288
290
|
'&p_p_lifecycle=2&p_p_state=normal&p_p_mode=view' \
|
|
289
291
|
'&p_p_cacheability=cacheLevelPage' \
|
|
@@ -301,50 +303,98 @@ module Truenorth
|
|
|
301
303
|
|
|
302
304
|
response = post_ajax(ajax_url, form_data)
|
|
303
305
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
]
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
306
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
307
|
+
return { success: false, error: "HTTP #{response.code}" }
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
body = response.body
|
|
311
|
+
log "Step 1 response length: #{body.length}"
|
|
312
|
+
|
|
313
|
+
# Check if a confirmation dialog was opened
|
|
314
|
+
if body.include?("PF('cancelReservationDialog').show()") || body.include?('.show()')
|
|
315
|
+
log "Confirmation dialog opened"
|
|
316
|
+
|
|
317
|
+
# Extract the updated ViewState from the AJAX response
|
|
318
|
+
new_view_state = extract_view_state_from_ajax(body) || view_state
|
|
319
|
+
|
|
320
|
+
# Parse the AJAX response to find the enabled YES button
|
|
321
|
+
response_html = Nokogiri::HTML(body)
|
|
322
|
+
|
|
323
|
+
# Look for the YES button in the dialog (not in the reservation list!)
|
|
324
|
+
# The YES button has specific characteristics:
|
|
325
|
+
# 1. Contains text "YES"
|
|
326
|
+
# 2. Has class "ui-area-btn-danger"
|
|
327
|
+
# 3. Is inside the cancel dialog (j_idt274)
|
|
328
|
+
confirm_button = response_html.css('div[id*="j_idt274"] a.ui-area-btn-danger').find do |link|
|
|
329
|
+
link.text.strip.upcase.include?('YES')
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Fallback: look for any YES button with the right classes
|
|
333
|
+
unless confirm_button
|
|
334
|
+
confirm_button = response_html.css('a.ui-commandlink').find do |link|
|
|
335
|
+
link_text = link.text.strip.upcase
|
|
336
|
+
link_class = link['class'].to_s
|
|
337
|
+
link_text == 'YES' && link_class.include?('ui-area-btn-danger') &&
|
|
338
|
+
!link_class.include?('disabled')
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
unless confirm_button
|
|
343
|
+
log "Could not find enabled YES button in response"
|
|
344
|
+
log "Response preview: #{body[0..2000]}"
|
|
345
|
+
return { success: false, error: 'Could not find confirmation button' }
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
confirm_button_id = confirm_button['id']
|
|
349
|
+
log "Found confirmation button in response: #{confirm_button_id}"
|
|
350
|
+
|
|
351
|
+
# Step 2: Click the "Yes" button to actually cancel
|
|
352
|
+
confirm_data = {
|
|
353
|
+
'javax.faces.partial.ajax' => 'true',
|
|
354
|
+
'javax.faces.source' => confirm_button_id,
|
|
355
|
+
'javax.faces.partial.execute' => '@all',
|
|
356
|
+
'javax.faces.partial.render' => form_id,
|
|
357
|
+
form_id => form_id,
|
|
358
|
+
'javax.faces.ViewState' => new_view_state,
|
|
359
|
+
confirm_button_id => confirm_button_id
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
log "Clicking confirmation button: #{confirm_button_id}"
|
|
363
|
+
confirm_response = post_ajax(ajax_url, confirm_data)
|
|
364
|
+
|
|
365
|
+
if confirm_response.is_a?(Net::HTTPSuccess)
|
|
366
|
+
confirm_body = confirm_response.body
|
|
367
|
+
log "Step 2 response length: #{confirm_body.length}"
|
|
368
|
+
log "Step 2 response preview: #{confirm_body[0..500]}"
|
|
369
|
+
|
|
370
|
+
# Check for success indicators
|
|
371
|
+
if confirm_body =~ /cancelled.*successfully/i ||
|
|
372
|
+
confirm_body =~ /reservation.*cancelled/i ||
|
|
373
|
+
confirm_body =~ /successfully.*cancelled/i ||
|
|
374
|
+
confirm_body.include?('growl') ||
|
|
375
|
+
confirm_body.length < 1000
|
|
376
|
+
log 'Cancellation confirmed successfully'
|
|
377
|
+
{ success: true, message: 'Reservation cancelled' }
|
|
378
|
+
else
|
|
379
|
+
log "Warning: Uncertain confirmation response"
|
|
380
|
+
{ success: true, message: 'Cancellation likely succeeded (please verify)' }
|
|
381
|
+
end
|
|
382
|
+
else
|
|
383
|
+
{ success: false, error: "Confirmation failed: HTTP #{confirm_response.code}" }
|
|
384
|
+
end
|
|
385
|
+
else
|
|
386
|
+
# No dialog - check if it was directly cancelled
|
|
387
|
+
log "No confirmation dialog detected"
|
|
388
|
+
|
|
389
|
+
if body =~ /cancelled.*successfully/i ||
|
|
390
|
+
body =~ /reservation.*cancelled/i ||
|
|
391
|
+
body.length < 500
|
|
392
|
+
log 'Direct cancellation successful'
|
|
339
393
|
{ success: true, message: 'Reservation cancelled' }
|
|
340
394
|
else
|
|
341
|
-
|
|
342
|
-
log "Ambiguous response (#{body.length} bytes)"
|
|
343
|
-
log "Full response: #{body}"
|
|
395
|
+
log "Uncertain result (#{body.length} bytes)"
|
|
344
396
|
{ success: false, error: 'Uncertain if cancellation succeeded - please verify' }
|
|
345
397
|
end
|
|
346
|
-
else
|
|
347
|
-
{ success: false, error: "HTTP #{response.code}" }
|
|
348
398
|
end
|
|
349
399
|
end
|
|
350
400
|
|
data/lib/truenorth/version.rb
CHANGED
data/truenorth.gemspec
CHANGED