truenorth 0.3.0 → 0.5.0
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 +22 -3
- data/lib/truenorth/client.rb +247 -15
- data/lib/truenorth/config.rb +35 -0
- data/lib/truenorth/version.rb +1 -1
- data/lib/truenorth.rb +1 -0
- data/truenorth.gemspec +4 -1
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1ae8cfc6540c62e513428be9bf6b460cba5f1207f405d5dd075d2f3754fe9976
|
|
4
|
+
data.tar.gz: 20382b0505099606ebf214359796a956947ee6cf3667b7ac8d5797debf7e995b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 38de51f859b0c3c6f20a2c23d2bffabc91de6de3d4e8f065a9e9f25bacfa2b1ac6d53d5aed5e8755a67ba8b99ed46909a63f8ea891929958e0e0e020bdb37949
|
|
7
|
+
data.tar.gz: e8372584aaa758b89b29c620e0c31ff63d97740e1a8bfd3ccaec52e88a063b2934e05aad1c465995454d0ffa1171596d4fd74d3dc26160c85f1e5b2aa1a3249c
|
data/lib/truenorth/cli.rb
CHANGED
|
@@ -35,9 +35,17 @@ module Truenorth
|
|
|
35
35
|
desc 'availability [DATE]', 'Check available slots'
|
|
36
36
|
option :activity, aliases: '-a', default: 'squash', desc: 'Activity type (squash, golf, music, meeting)'
|
|
37
37
|
option :json, type: :boolean, desc: 'Output as JSON'
|
|
38
|
+
option :debug, type: :boolean, desc: 'Show debug output'
|
|
39
|
+
option :http, type: :boolean, desc: 'Use HTTP mode (faster, but only 2 courts)'
|
|
38
40
|
def availability(date = nil)
|
|
39
41
|
date = parse_date(date)
|
|
40
|
-
|
|
42
|
+
|
|
43
|
+
client = if options[:http]
|
|
44
|
+
say 'Using HTTP mode (fast, 2 courts only)...', :yellow if !options[:json]
|
|
45
|
+
Client.new(debug: options[:debug])
|
|
46
|
+
else
|
|
47
|
+
BrowserClient.new(debug: options[:debug])
|
|
48
|
+
end
|
|
41
49
|
|
|
42
50
|
say "Checking availability for #{date}...", :cyan
|
|
43
51
|
result = client.availability(date, activity: options[:activity])
|
|
@@ -64,6 +72,8 @@ module Truenorth
|
|
|
64
72
|
option :court, aliases: '-c', desc: 'Preferred court (e.g., "Court 1", "Squash Court 2")'
|
|
65
73
|
option :activity, aliases: '-a', default: 'squash', desc: 'Activity type'
|
|
66
74
|
option :dry_run, type: :boolean, aliases: '-n', desc: 'Test without actually booking'
|
|
75
|
+
option :http, type: :boolean, desc: 'Use HTTP mode (faster, but only 2 courts)'
|
|
76
|
+
option :debug, type: :boolean, desc: 'Show debug output'
|
|
67
77
|
def book(time_or_description)
|
|
68
78
|
# Parse natural language input
|
|
69
79
|
parsed = parse_booking_request(time_or_description, options[:date], options[:activity])
|
|
@@ -71,7 +81,13 @@ module Truenorth
|
|
|
71
81
|
date = parsed[:date]
|
|
72
82
|
time = parsed[:time]
|
|
73
83
|
activity = parsed[:activity]
|
|
74
|
-
|
|
84
|
+
|
|
85
|
+
# Use browser mode unless --http flag is set (needed to see all 3 courts)
|
|
86
|
+
client = if options[:http]
|
|
87
|
+
Client.new(debug: options[:debug])
|
|
88
|
+
else
|
|
89
|
+
BrowserClient.new(debug: options[:debug])
|
|
90
|
+
end
|
|
75
91
|
|
|
76
92
|
mode = options[:dry_run] ? ' (DRY RUN)' : ''
|
|
77
93
|
say "Booking #{activity} at #{time} on #{date}#{mode}...", :cyan
|
|
@@ -109,8 +125,10 @@ module Truenorth
|
|
|
109
125
|
desc 'reservations', 'List your current reservations'
|
|
110
126
|
option :json, type: :boolean, desc: 'Output as JSON'
|
|
111
127
|
option :all, type: :boolean, aliases: '-a', desc: 'Show all family members (default: only you)'
|
|
128
|
+
option :debug, type: :boolean, desc: 'Show debug output'
|
|
112
129
|
def reservations
|
|
113
|
-
|
|
130
|
+
# Use HTTP mode for reservations (works reliably)
|
|
131
|
+
client = Client.new(debug: options[:debug])
|
|
114
132
|
|
|
115
133
|
say 'Fetching reservations...', :cyan
|
|
116
134
|
results = client.reservations
|
|
@@ -153,6 +171,7 @@ module Truenorth
|
|
|
153
171
|
option :all, type: :boolean, aliases: '-a', desc: 'Cancel from full list (use with --all view)'
|
|
154
172
|
option :debug, type: :boolean, desc: 'Show debug output'
|
|
155
173
|
def cancel(index)
|
|
174
|
+
# Use HTTP mode for cancellation (works reliably)
|
|
156
175
|
client = Client.new(debug: options[:debug])
|
|
157
176
|
|
|
158
177
|
say 'Fetching reservations...', :cyan
|
data/lib/truenorth/client.rb
CHANGED
|
@@ -15,7 +15,8 @@ module Truenorth
|
|
|
15
15
|
BOOKING_PATH = '/group/pages/facility-booking'
|
|
16
16
|
RESERVATIONS_PATH = '/group/pages/my-reservations'
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
# Use a desktop user agent - server may detect mobile based on UA
|
|
19
|
+
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' \
|
|
19
20
|
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
|
|
20
21
|
|
|
21
22
|
# Activity type IDs
|
|
@@ -37,16 +38,27 @@ module Truenorth
|
|
|
37
38
|
'32' => 'Court 3'
|
|
38
39
|
}.freeze
|
|
39
40
|
|
|
41
|
+
# Court IDs by activity
|
|
42
|
+
COURT_IDS_BY_ACTIVITY = {
|
|
43
|
+
'squash' => %w[16 17 18],
|
|
44
|
+
'golf' => %w[30 31 32],
|
|
45
|
+
'music' => %w[16 17 18], # May need to update these
|
|
46
|
+
'room' => %w[30 31 32], # May need to update these
|
|
47
|
+
'meeting' => %w[30 31 32] # May need to update these
|
|
48
|
+
}.freeze
|
|
49
|
+
|
|
40
50
|
attr_reader :cookies, :debug_log, :logged_in
|
|
41
51
|
|
|
42
52
|
def initialize(base_url: nil, debug: false)
|
|
43
53
|
@base_url = base_url || Config.base_url
|
|
44
54
|
raise Error, 'No base URL configured. Run: truenorth configure' unless @base_url
|
|
45
55
|
|
|
46
|
-
@cookies = {}
|
|
56
|
+
@cookies = Config.cookies || {}
|
|
47
57
|
@debug = debug
|
|
48
58
|
@debug_log = StringIO.new
|
|
49
|
-
@logged_in =
|
|
59
|
+
@logged_in = !@cookies.empty? # If we have cookies, assume logged in
|
|
60
|
+
|
|
61
|
+
log "Loaded #{@cookies.length} cookies from cache" if @logged_in && @debug
|
|
50
62
|
end
|
|
51
63
|
|
|
52
64
|
# Login to the booking system
|
|
@@ -91,7 +103,8 @@ module Truenorth
|
|
|
91
103
|
|
|
92
104
|
if response.body.include?('Sign Out') || response.body.include?('My Reservations')
|
|
93
105
|
@logged_in = true
|
|
94
|
-
|
|
106
|
+
Config.save_cookies(@cookies)
|
|
107
|
+
log 'Login successful (cookies saved)'
|
|
95
108
|
true
|
|
96
109
|
else
|
|
97
110
|
html = Nokogiri::HTML(response.body)
|
|
@@ -128,8 +141,52 @@ module Truenorth
|
|
|
128
141
|
html = change_activity(html, activity_id)
|
|
129
142
|
end
|
|
130
143
|
|
|
131
|
-
|
|
132
|
-
|
|
144
|
+
# Debug: Save HTML after activity change
|
|
145
|
+
if @debug
|
|
146
|
+
debug_file = "/tmp/truenorth_after_activity_#{Time.now.to_i}.html"
|
|
147
|
+
File.write(debug_file, html.to_s)
|
|
148
|
+
log "Saved HTML after activity change to: #{debug_file}"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Court dropdown not available in HTML response - use hardcoded IDs
|
|
152
|
+
court_ids = COURT_IDS_BY_ACTIVITY[activity.to_s.downcase] || []
|
|
153
|
+
|
|
154
|
+
if court_ids.length >= 3
|
|
155
|
+
log "Querying #{court_ids.length} courts individually (IDs: #{court_ids.join(', ')})"
|
|
156
|
+
all_slots = {}
|
|
157
|
+
|
|
158
|
+
court_ids.each do |court_id|
|
|
159
|
+
court_name = COURTS[court_id] || "Court #{court_id}"
|
|
160
|
+
log "Querying #{court_name} (ID: #{court_id})"
|
|
161
|
+
|
|
162
|
+
# Set activityAreaId and refresh
|
|
163
|
+
form_id = extract_form_id(html)
|
|
164
|
+
view_state = extract_view_state(html)
|
|
165
|
+
form_fields = extract_all_form_fields(html, form_id)
|
|
166
|
+
form_fields["#{form_id}:activityAreaId"] = court_id
|
|
167
|
+
|
|
168
|
+
# Make AJAX request to update table for this court
|
|
169
|
+
result = change_date_ajax(form_id, view_state, requested_date, form_fields)
|
|
170
|
+
if result[:success]
|
|
171
|
+
court_html = Nokogiri::HTML(result[:body].scan(/<!\[CDATA\[(.*?)\]\]>/m).flatten.join("\n"))
|
|
172
|
+
court_slots = parse_slots(court_html)
|
|
173
|
+
|
|
174
|
+
# Merge slots
|
|
175
|
+
court_slots.each do |time, courts|
|
|
176
|
+
all_slots[time] ||= []
|
|
177
|
+
all_slots[time].concat(courts)
|
|
178
|
+
all_slots[time].uniq!
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
slots = all_slots
|
|
184
|
+
log "Combined #{slots.count} time slots from all courts"
|
|
185
|
+
else
|
|
186
|
+
# Fallback to original parsing
|
|
187
|
+
slots = parse_slots(html)
|
|
188
|
+
log "Found #{slots.count} available time slots"
|
|
189
|
+
end
|
|
133
190
|
|
|
134
191
|
{
|
|
135
192
|
success: true,
|
|
@@ -141,7 +198,8 @@ module Truenorth
|
|
|
141
198
|
|
|
142
199
|
|
|
143
200
|
# Book a slot
|
|
144
|
-
|
|
201
|
+
# If slot_info is provided, it should have: { area_id:, start_time:, end_time:, court: }
|
|
202
|
+
def book(time, date: Date.today, court: nil, activity: 'squash', dry_run: false, slot_info: nil)
|
|
145
203
|
ensure_logged_in!
|
|
146
204
|
|
|
147
205
|
log "\n=== BOOK SLOT ==="
|
|
@@ -172,11 +230,22 @@ module Truenorth
|
|
|
172
230
|
form_fields = extract_all_form_fields(html, form_id)
|
|
173
231
|
end
|
|
174
232
|
|
|
175
|
-
# Find the slot
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
233
|
+
# Find the slot (or use provided slot_info)
|
|
234
|
+
if slot_info
|
|
235
|
+
log "Using provided slot info: #{slot_info[:court]} at #{slot_info[:start_time]}"
|
|
236
|
+
# Convert slot_info keys from symbols if needed and ensure we have an id
|
|
237
|
+
slot = {
|
|
238
|
+
id: nil, # We'll generate this or it's not needed for AJAX
|
|
239
|
+
area_id: slot_info[:area_id],
|
|
240
|
+
court: slot_info[:court],
|
|
241
|
+
start_time: slot_info[:start_time] || slot_info[:time],
|
|
242
|
+
end_time: slot_info[:end_time]
|
|
243
|
+
}
|
|
244
|
+
else
|
|
245
|
+
slot = find_slot(html, time, court)
|
|
246
|
+
raise BookingError, "No slot available at #{time}" unless slot
|
|
247
|
+
log "Found slot: #{slot[:court]} at #{slot[:start_time]}"
|
|
248
|
+
end
|
|
180
249
|
|
|
181
250
|
# Select slot via AJAX
|
|
182
251
|
select_result = select_slot_ajax(form_id, view_state, slot, components, form_fields)
|
|
@@ -417,6 +486,18 @@ module Truenorth
|
|
|
417
486
|
def parse_slots(html)
|
|
418
487
|
slots = {}
|
|
419
488
|
|
|
489
|
+
# Debug: Count total columns found
|
|
490
|
+
headers = html.css('thead th[role="columnheader"]')
|
|
491
|
+
log "Found #{headers.length} column headers: #{headers.map { |h| h['aria-label'] }.join(', ')}"
|
|
492
|
+
|
|
493
|
+
# Debug: Count slots by area-id
|
|
494
|
+
area_counts = Hash.new(0)
|
|
495
|
+
html.css('td.slot div[data-start-time]').each do |div|
|
|
496
|
+
area_id = div['data-area-id']
|
|
497
|
+
area_counts[area_id] += 1
|
|
498
|
+
end
|
|
499
|
+
log "Slots by area ID: #{area_counts.inspect}"
|
|
500
|
+
|
|
420
501
|
# Find ALL slots with data-start-time
|
|
421
502
|
html.css('td.slot div[data-start-time]').each do |div|
|
|
422
503
|
td = div.parent
|
|
@@ -590,9 +671,66 @@ module Truenorth
|
|
|
590
671
|
result = change_activity_ajax(form_id, view_state, activity_id, form_fields, components)
|
|
591
672
|
return html unless result[:success]
|
|
592
673
|
|
|
674
|
+
# Parse the response and check for Court 3
|
|
675
|
+
parsed = Nokogiri::HTML(result[:body])
|
|
676
|
+
court3_elements = parsed.css('th[aria-label*="Court 3"], div[data-area-id="18"], div[data-area-id="32"]')
|
|
677
|
+
log "After change_activity: Found #{court3_elements.length} Court 3 elements in response"
|
|
678
|
+
|
|
679
|
+
parsed
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
def change_court_dropdown(html, court_id)
|
|
683
|
+
form_id = extract_form_id(html)
|
|
684
|
+
view_state = extract_view_state(html)
|
|
685
|
+
form_fields = extract_all_form_fields(html, form_id)
|
|
686
|
+
|
|
687
|
+
# Find the court dropdown field dynamically (in .activity-areas div)
|
|
688
|
+
court_dropdown = html.at_css('.activity-areas select, div[class*="area"] select[id*="j_idt"]')
|
|
689
|
+
return html unless court_dropdown
|
|
690
|
+
|
|
691
|
+
dropdown_id = court_dropdown['id']
|
|
692
|
+
dropdown_source = dropdown_id.gsub(/_input$/, '')
|
|
693
|
+
|
|
694
|
+
result = change_court_dropdown_ajax(form_id, view_state, court_id, dropdown_id, dropdown_source, form_fields)
|
|
695
|
+
return html unless result[:success]
|
|
696
|
+
|
|
697
|
+
# Parse CDATA content from response
|
|
698
|
+
cdata_content = result[:body].scan(/<!\[CDATA\[(.*?)\]\]>/m).flatten.join("\n")
|
|
699
|
+
if !cdata_content.empty?
|
|
700
|
+
cdata_html = Nokogiri::HTML(cdata_content)
|
|
701
|
+
slots = cdata_html.css('div[data-start-time]')
|
|
702
|
+
log "Court #{court_id}: Found #{slots.length} slots in CDATA response"
|
|
703
|
+
return cdata_html if slots.length > 0
|
|
704
|
+
end
|
|
705
|
+
|
|
593
706
|
Nokogiri::HTML(result[:body])
|
|
594
707
|
end
|
|
595
708
|
|
|
709
|
+
def change_court_area(html, area_id)
|
|
710
|
+
form_id = extract_form_id(html)
|
|
711
|
+
view_state = extract_view_state(html)
|
|
712
|
+
form_fields = extract_all_form_fields(html, form_id)
|
|
713
|
+
|
|
714
|
+
result = change_court_area_ajax(form_id, view_state, area_id, form_fields)
|
|
715
|
+
return html unless result[:success]
|
|
716
|
+
|
|
717
|
+
# Debug: Check response
|
|
718
|
+
parsed = Nokogiri::HTML(result[:body])
|
|
719
|
+
slot_divs = parsed.css('div[data-start-time]')
|
|
720
|
+
log "Court area #{area_id} response: #{slot_divs.length} slot divs found"
|
|
721
|
+
|
|
722
|
+
# Also check CDATA content
|
|
723
|
+
cdata_content = result[:body].scan(/<!\[CDATA\[(.*?)\]\]>/m).flatten.join("\n")
|
|
724
|
+
if !cdata_content.empty?
|
|
725
|
+
cdata_html = Nokogiri::HTML(cdata_content)
|
|
726
|
+
cdata_slots = cdata_html.css('div[data-start-time]')
|
|
727
|
+
log "Court area #{area_id} CDATA: #{cdata_slots.length} slot divs in CDATA"
|
|
728
|
+
return cdata_html if cdata_slots.length > 0
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
parsed
|
|
732
|
+
end
|
|
733
|
+
|
|
596
734
|
def change_date(html, date_str, activity_id = nil)
|
|
597
735
|
# Extract what we can from the HTML
|
|
598
736
|
form_id = extract_form_id(html)
|
|
@@ -609,7 +747,17 @@ module Truenorth
|
|
|
609
747
|
cdata_content = ajax_body.scan(/<!\[CDATA\[(.*?)\]\]>/m).flatten.join("\n")
|
|
610
748
|
|
|
611
749
|
if !cdata_content.empty? && cdata_content.include?('slot')
|
|
612
|
-
Nokogiri::HTML(cdata_content)
|
|
750
|
+
parsed_html = Nokogiri::HTML(cdata_content)
|
|
751
|
+
|
|
752
|
+
# Debug: Check if Court 3 is in the response
|
|
753
|
+
court3_headers = parsed_html.css('th[aria-label*="Court 3"]')
|
|
754
|
+
log "Court 3 headers found in AJAX response: #{court3_headers.length}"
|
|
755
|
+
|
|
756
|
+
# Debug: Check for area ID 18 or 32 (Court 3 IDs)
|
|
757
|
+
court3_slots = parsed_html.css('div[data-area-id="18"], div[data-area-id="32"]')
|
|
758
|
+
log "Court 3 slots (area 18/32) found in AJAX response: #{court3_slots.length}"
|
|
759
|
+
|
|
760
|
+
parsed_html
|
|
613
761
|
else
|
|
614
762
|
html
|
|
615
763
|
end
|
|
@@ -681,7 +829,22 @@ module Truenorth
|
|
|
681
829
|
|
|
682
830
|
selected = select.at_css('option[selected]')
|
|
683
831
|
fields[select['name']] = selected['value'] if selected
|
|
832
|
+
|
|
833
|
+
# Debug: Log select options
|
|
834
|
+
if select['name']&.include?('area') || select['name']&.include?('court') || select['name']&.include?('trainer')
|
|
835
|
+
options = select.css('option').map { |opt| "#{opt.text.strip}=#{opt['value']}" }
|
|
836
|
+
log "Found selector #{select['name']}: #{options.join(', ')}"
|
|
837
|
+
end
|
|
684
838
|
end
|
|
839
|
+
|
|
840
|
+
# Force parameters to get all courts
|
|
841
|
+
fields["#{form_id}:mobileViewDisplay"] = '0' # 0 = full view
|
|
842
|
+
|
|
843
|
+
# Debug: Log all field names and key values
|
|
844
|
+
log "Form fields: #{fields.keys.join(', ')}"
|
|
845
|
+
log "activityAreaId current value: #{fields["#{form_id}:activityAreaId"]}"
|
|
846
|
+
log "showAllAreasOrTrainers: #{fields["#{form_id}:showAllAreasOrTrainers"]}"
|
|
847
|
+
|
|
685
848
|
fields
|
|
686
849
|
end
|
|
687
850
|
|
|
@@ -710,6 +873,12 @@ module Truenorth
|
|
|
710
873
|
form_data = form_fields.dup
|
|
711
874
|
form_data["#{form_id}:j_idt51_input"] = activity_id
|
|
712
875
|
form_data["#{form_id}:activityId"] = activity_id
|
|
876
|
+
form_data["#{form_id}:showAllAreasOrTrainers"] = 'true' # Show all courts!
|
|
877
|
+
|
|
878
|
+
# Debug: Log what we're sending
|
|
879
|
+
log "Sending to #{form_id}:showAllAreasOrTrainers = #{form_data["#{form_id}:showAllAreasOrTrainers"]}"
|
|
880
|
+
log "Sending to #{form_id}:mobileViewDisplay = #{form_data["#{form_id}:mobileViewDisplay"]}"
|
|
881
|
+
|
|
713
882
|
form_data.merge!(
|
|
714
883
|
'javax.faces.partial.ajax' => 'true',
|
|
715
884
|
'javax.faces.source' => activity_dropdown,
|
|
@@ -730,6 +899,56 @@ module Truenorth
|
|
|
730
899
|
end
|
|
731
900
|
end
|
|
732
901
|
|
|
902
|
+
def change_court_dropdown_ajax(form_id, view_state, court_id, dropdown_input_id, dropdown_source, form_fields)
|
|
903
|
+
ajax_url = build_ajax_url
|
|
904
|
+
encoded_url = URI.encode_www_form_component(ajax_url)
|
|
905
|
+
|
|
906
|
+
form_data = form_fields.dup
|
|
907
|
+
form_data[dropdown_input_id] = court_id
|
|
908
|
+
form_data.merge!(
|
|
909
|
+
'javax.faces.partial.ajax' => 'true',
|
|
910
|
+
'javax.faces.source' => dropdown_source,
|
|
911
|
+
'javax.faces.partial.execute' => dropdown_source,
|
|
912
|
+
'javax.faces.partial.render' => form_id,
|
|
913
|
+
'javax.faces.behavior.event' => 'change',
|
|
914
|
+
'javax.faces.partial.event' => 'change',
|
|
915
|
+
form_id => form_id,
|
|
916
|
+
'javax.faces.encodedURL' => encoded_url,
|
|
917
|
+
'javax.faces.ViewState' => view_state
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
response = post_ajax(ajax_url, form_data)
|
|
921
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
922
|
+
{ success: true, view_state: extract_view_state_from_ajax(response.body), body: response.body }
|
|
923
|
+
else
|
|
924
|
+
{ success: false, error: "HTTP #{response.code}" }
|
|
925
|
+
end
|
|
926
|
+
end
|
|
927
|
+
|
|
928
|
+
def change_court_area_ajax(form_id, view_state, area_id, form_fields)
|
|
929
|
+
ajax_url = build_ajax_url
|
|
930
|
+
encoded_url = URI.encode_www_form_component(ajax_url)
|
|
931
|
+
|
|
932
|
+
form_data = form_fields.dup
|
|
933
|
+
form_data["#{form_id}:activityAreaId"] = area_id
|
|
934
|
+
form_data.merge!(
|
|
935
|
+
'javax.faces.partial.ajax' => 'true',
|
|
936
|
+
'javax.faces.source' => form_id,
|
|
937
|
+
'javax.faces.partial.execute' => form_id,
|
|
938
|
+
'javax.faces.partial.render' => form_id,
|
|
939
|
+
form_id => form_id,
|
|
940
|
+
'javax.faces.encodedURL' => encoded_url,
|
|
941
|
+
'javax.faces.ViewState' => view_state
|
|
942
|
+
)
|
|
943
|
+
|
|
944
|
+
response = post_ajax(ajax_url, form_data)
|
|
945
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
946
|
+
{ success: true, view_state: extract_view_state_from_ajax(response.body), body: response.body }
|
|
947
|
+
else
|
|
948
|
+
{ success: false, error: "HTTP #{response.code}" }
|
|
949
|
+
end
|
|
950
|
+
end
|
|
951
|
+
|
|
733
952
|
def change_date_ajax(form_id, view_state, date_str, form_data)
|
|
734
953
|
return { success: false, error: 'No form_id' } unless form_id
|
|
735
954
|
|
|
@@ -742,6 +961,7 @@ module Truenorth
|
|
|
742
961
|
form_data = form_data.dup
|
|
743
962
|
form_data["#{form_id}:sheetDate"] = date_str
|
|
744
963
|
form_data["#{form_id}:j_idt57_input"] = date_str
|
|
964
|
+
form_data["#{form_id}:showAllAreasOrTrainers"] = 'true' # Show all courts!
|
|
745
965
|
|
|
746
966
|
form_data.merge!(
|
|
747
967
|
'javax.faces.partial.ajax' => 'true',
|
|
@@ -815,10 +1035,22 @@ module Truenorth
|
|
|
815
1035
|
|
|
816
1036
|
response = post_ajax(ajax_url, form_data)
|
|
817
1037
|
if response.is_a?(Net::HTTPSuccess)
|
|
818
|
-
body = response.body
|
|
819
|
-
|
|
1038
|
+
body = response.body
|
|
1039
|
+
body_lower = body.downcase
|
|
1040
|
+
|
|
1041
|
+
# Check for explicit error indicators
|
|
1042
|
+
if body_lower.include?('error') || body_lower.include?('failed') ||
|
|
1043
|
+
body_lower.include?('unable') || body_lower.include?('invalid')
|
|
1044
|
+
log "Booking save returned error indicators in response"
|
|
1045
|
+
{ success: false, error: 'Booking save failed' }
|
|
1046
|
+
# Check for success indicators OR assume success if reasonable response
|
|
1047
|
+
elsif body_lower.include?('success') || body_lower.include?('confirmed') ||
|
|
1048
|
+
body_lower.include?('booked') || body_lower.include?('reserved') ||
|
|
1049
|
+
(body.length < 5000 && !body_lower.include?('exception'))
|
|
1050
|
+
log "Booking save appears successful (HTTP 200)"
|
|
820
1051
|
{ success: true, confirmation: 'Booking confirmed' }
|
|
821
1052
|
else
|
|
1053
|
+
log "Uncertain booking result, response length: #{body.length}"
|
|
822
1054
|
{ success: false, error: 'No confirmation in response' }
|
|
823
1055
|
end
|
|
824
1056
|
else
|
data/lib/truenorth/config.rb
CHANGED
|
@@ -8,6 +8,7 @@ module Truenorth
|
|
|
8
8
|
CONFIG_DIR = File.expand_path('~/.config/truenorth')
|
|
9
9
|
CONFIG_FILE = File.join(CONFIG_DIR, 'config.yml')
|
|
10
10
|
CREDENTIALS_FILE = File.join(CONFIG_DIR, 'credentials.yml')
|
|
11
|
+
COOKIES_FILE = File.join(CONFIG_DIR, 'cookies.yml')
|
|
11
12
|
|
|
12
13
|
class << self
|
|
13
14
|
def load
|
|
@@ -62,8 +63,42 @@ module Truenorth
|
|
|
62
63
|
CONFIG_DIR
|
|
63
64
|
end
|
|
64
65
|
|
|
66
|
+
def cookies
|
|
67
|
+
load_cookies
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def save_cookies(cookies_hash)
|
|
71
|
+
FileUtils.mkdir_p(CONFIG_DIR)
|
|
72
|
+
File.write(COOKIES_FILE, YAML.dump({
|
|
73
|
+
'cookies' => cookies_hash,
|
|
74
|
+
'timestamp' => Time.now.to_i
|
|
75
|
+
}))
|
|
76
|
+
FileUtils.chmod(0o600, COOKIES_FILE)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def clear_cookies
|
|
80
|
+
File.delete(COOKIES_FILE) if File.exist?(COOKIES_FILE)
|
|
81
|
+
end
|
|
82
|
+
|
|
65
83
|
private
|
|
66
84
|
|
|
85
|
+
def load_cookies
|
|
86
|
+
return {} unless File.exist?(COOKIES_FILE)
|
|
87
|
+
|
|
88
|
+
data = YAML.safe_load(File.read(COOKIES_FILE)) || {}
|
|
89
|
+
timestamp = data['timestamp']
|
|
90
|
+
|
|
91
|
+
# Cookies expire after 24 hours
|
|
92
|
+
if timestamp && (Time.now.to_i - timestamp) < 86400
|
|
93
|
+
data['cookies'] || {}
|
|
94
|
+
else
|
|
95
|
+
clear_cookies
|
|
96
|
+
{}
|
|
97
|
+
end
|
|
98
|
+
rescue StandardError
|
|
99
|
+
{}
|
|
100
|
+
end
|
|
101
|
+
|
|
67
102
|
def load_config
|
|
68
103
|
return {} unless File.exist?(CONFIG_FILE)
|
|
69
104
|
|
data/lib/truenorth/version.rb
CHANGED
data/lib/truenorth.rb
CHANGED
data/truenorth.gemspec
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'lib/truenorth/version'
|
|
4
|
+
|
|
3
5
|
Gem::Specification.new do |spec|
|
|
4
6
|
spec.name = 'truenorth'
|
|
5
|
-
spec.version =
|
|
7
|
+
spec.version = Truenorth::VERSION
|
|
6
8
|
spec.authors = ['usiegj00']
|
|
7
9
|
spec.email = ['112138+usiegj00@users.noreply.github.com']
|
|
8
10
|
|
|
@@ -28,6 +30,7 @@ Gem::Specification.new do |spec|
|
|
|
28
30
|
spec.require_paths = ['lib']
|
|
29
31
|
|
|
30
32
|
spec.add_dependency 'base64', '~> 0.2'
|
|
33
|
+
spec.add_dependency 'ferrum', '~> 0.15'
|
|
31
34
|
spec.add_dependency 'nokogiri', '~> 1.15'
|
|
32
35
|
spec.add_dependency 'thor', '~> 1.3'
|
|
33
36
|
spec.add_dependency 'tty-table', '~> 0.12'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: truenorth
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- usiegj00
|
|
@@ -24,6 +24,20 @@ dependencies:
|
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '0.2'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: ferrum
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0.15'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0.15'
|
|
27
41
|
- !ruby/object:Gem::Dependency
|
|
28
42
|
name: nokogiri
|
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|