Automatically checks for appointment slots, auto-fills forms, and books appointments on BLS Spain Morocco visa website
Size
25.8 KB
Version
1.1.1
Created
Dec 6, 2025
Updated
6 days ago
1// ==UserScript==
2// @name BLS Spain Morocco Auto Appointment Booker
3// @description Automatically checks for appointment slots, auto-fills forms, and books appointments on BLS Spain Morocco visa website
4// @version 1.1.1
5// @match https://www.blsspainmorocco.net/*
6// @icon https://robomonkey.io/favicon.ico
7// @grant GM.getValue
8// @grant GM.setValue
9// @grant GM.deleteValue
10// @grant GM.listValues
11// @grant GM.notification
12// ==/UserScript==
13(function() {
14 'use strict';
15
16 // Configuration and State Management
17 const CONFIG = {
18 CHECK_INTERVAL: 30000, // Check every 30 seconds
19 AUTO_BOOK_ENABLED: false,
20 NOTIFICATION_SOUND: true,
21 MAX_RETRIES: 3
22 };
23
24 let isChecking = false;
25 let checkInterval = null;
26
27 // Utility Functions
28 function log(message, data = null) {
29 const timestamp = new Date().toLocaleTimeString();
30 console.log(`[BLS Auto Booker ${timestamp}] ${message}`, data || '');
31 }
32
33 function debounce(func, wait) {
34 let timeout;
35 return function executedFunction(...args) {
36 const later = () => {
37 clearTimeout(timeout);
38 func(...args);
39 };
40 clearTimeout(timeout);
41 timeout = setTimeout(later, wait);
42 };
43 }
44
45 // Storage Functions
46 async function saveCredentials(email, password) {
47 try {
48 await GM.setValue('bls_email', email);
49 await GM.setValue('bls_password', password);
50 log('Credentials saved successfully');
51 return true;
52 } catch (error) {
53 console.error('Error saving credentials:', error);
54 return false;
55 }
56 }
57
58 async function getCredentials() {
59 try {
60 const email = await GM.getValue('bls_email', '');
61 const password = await GM.getValue('bls_password', '');
62 return { email, password };
63 } catch (error) {
64 console.error('Error getting credentials:', error);
65 return { email: '', password: '' };
66 }
67 }
68
69 async function saveFormData(data) {
70 try {
71 await GM.setValue('bls_form_data', JSON.stringify(data));
72 log('Form data saved successfully');
73 return true;
74 } catch (error) {
75 console.error('Error saving form data:', error);
76 return false;
77 }
78 }
79
80 async function getFormData() {
81 try {
82 const data = await GM.getValue('bls_form_data', '{}');
83 return JSON.parse(data);
84 } catch (error) {
85 console.error('Error getting form data:', error);
86 return {};
87 }
88 }
89
90 async function getConfig() {
91 try {
92 const autoBook = await GM.getValue('auto_book_enabled', false);
93 const checkInterval = await GM.getValue('check_interval', 30000);
94 const notificationSound = await GM.getValue('notification_sound', true);
95 return { autoBook, checkInterval, notificationSound };
96 } catch (error) {
97 console.error('Error getting config:', error);
98 return CONFIG;
99 }
100 }
101
102 async function saveConfig(config) {
103 try {
104 await GM.setValue('auto_book_enabled', config.autoBook);
105 await GM.setValue('check_interval', config.checkInterval);
106 await GM.setValue('notification_sound', config.notificationSound);
107 log('Config saved successfully');
108 return true;
109 } catch (error) {
110 console.error('Error saving config:', error);
111 return false;
112 }
113 }
114
115 // Auto-fill Login Form
116 async function autoFillLogin() {
117 log('Attempting to auto-fill login form');
118
119 const emailSelectors = [
120 'input[type="email"]',
121 'input[name*="email" i]',
122 'input[id*="email" i]',
123 'input[placeholder*="email" i]',
124 'input[name*="username" i]',
125 'input[id*="username" i]'
126 ];
127
128 const passwordSelectors = [
129 'input[type="password"]',
130 'input[name*="password" i]',
131 'input[id*="password" i]'
132 ];
133
134 let emailInput = null;
135 let passwordInput = null;
136
137 // Find email input
138 for (const selector of emailSelectors) {
139 emailInput = document.querySelector(selector);
140 if (emailInput) {
141 log('Found email input with selector:', selector);
142 break;
143 }
144 }
145
146 // Find password input
147 for (const selector of passwordSelectors) {
148 passwordInput = document.querySelector(selector);
149 if (passwordInput) {
150 log('Found password input with selector:', selector);
151 break;
152 }
153 }
154
155 if (emailInput && passwordInput) {
156 const credentials = await getCredentials();
157
158 if (credentials.email && credentials.password) {
159 emailInput.value = credentials.email;
160 passwordInput.value = credentials.password;
161
162 // Trigger input events
163 emailInput.dispatchEvent(new Event('input', { bubbles: true }));
164 emailInput.dispatchEvent(new Event('change', { bubbles: true }));
165 passwordInput.dispatchEvent(new Event('input', { bubbles: true }));
166 passwordInput.dispatchEvent(new Event('change', { bubbles: true }));
167
168 log('Login form auto-filled successfully');
169 return true;
170 } else {
171 log('No saved credentials found');
172 }
173 } else {
174 log('Login form inputs not found');
175 }
176
177 return false;
178 }
179
180 // Check for Available Appointments
181 async function checkAppointmentSlots() {
182 log('Checking for available appointment slots...');
183
184 const slotSelectors = [
185 'button[class*="available"]',
186 'div[class*="available"]',
187 'td[class*="available"]',
188 'span[class*="available"]',
189 '.appointment-slot:not(.disabled)',
190 '.date-available',
191 '[data-available="true"]'
192 ];
193
194 let availableSlots = [];
195
196 for (const selector of slotSelectors) {
197 const elements = document.querySelectorAll(selector);
198 if (elements.length > 0) {
199 availableSlots = Array.from(elements);
200 log(`Found ${availableSlots.length} available slots with selector: ${selector}`);
201 break;
202 }
203 }
204
205 if (availableSlots.length > 0) {
206 log('Available appointment slots found!', availableSlots.length);
207 await notifyUser('Appointment Available!', `Found ${availableSlots.length} available slot(s)`);
208
209 const config = await getConfig();
210 if (config.autoBook) {
211 await bookFirstAvailableSlot(availableSlots[0]);
212 }
213
214 return availableSlots;
215 } else {
216 log('No available slots found');
217 return [];
218 }
219 }
220
221 // Book First Available Slot
222 async function bookFirstAvailableSlot(slotElement) {
223 log('Attempting to book first available slot');
224
225 try {
226 // Click the slot
227 slotElement.click();
228 log('Clicked on available slot');
229
230 // Wait for form to appear
231 await new Promise(resolve => setTimeout(resolve, 2000));
232
233 // Auto-fill the booking form
234 await autoFillBookingForm();
235
236 // Find and click submit button
237 const submitSelectors = [
238 'button[type="submit"]',
239 'input[type="submit"]',
240 'button[class*="submit"]',
241 'button[class*="book"]',
242 'button[class*="confirm"]'
243 ];
244
245 let submitButton = null;
246 for (const selector of submitSelectors) {
247 submitButton = document.querySelector(selector);
248 if (submitButton) {
249 log('Found submit button with selector:', selector);
250 break;
251 }
252 }
253
254 if (submitButton) {
255 submitButton.click();
256 log('Clicked submit button');
257 await notifyUser('Booking Submitted!', 'Appointment booking has been submitted');
258 return true;
259 } else {
260 log('Submit button not found');
261 return false;
262 }
263 } catch (error) {
264 console.error('Error booking slot:', error);
265 return false;
266 }
267 }
268
269 // Auto-fill Booking Form
270 async function autoFillBookingForm() {
271 log('Auto-filling booking form');
272
273 const formData = await getFormData();
274
275 if (Object.keys(formData).length === 0) {
276 log('No saved form data found');
277 return false;
278 }
279
280 // Fill text inputs
281 for (const [key, value] of Object.entries(formData)) {
282 const input = document.querySelector(`input[name="${key}"], input[id="${key}"]`);
283 if (input && value) {
284 input.value = value;
285 input.dispatchEvent(new Event('input', { bubbles: true }));
286 input.dispatchEvent(new Event('change', { bubbles: true }));
287 log(`Filled field: ${key}`);
288 }
289 }
290
291 log('Booking form auto-filled');
292 return true;
293 }
294
295 // Send Notification
296 async function notifyUser(title, message) {
297 log(`Notification: ${title} - ${message}`);
298
299 try {
300 // Browser notification
301 if (typeof GM.notification !== 'undefined') {
302 GM.notification({
303 title: title,
304 text: message,
305 timeout: 10000
306 });
307 }
308
309 // Visual notification on page
310 showPageNotification(title, message);
311
312 // Sound notification
313 const config = await getConfig();
314 if (config.notificationSound) {
315 playNotificationSound();
316 }
317 } catch (error) {
318 console.error('Error sending notification:', error);
319 }
320 }
321
322 // Show Page Notification
323 function showPageNotification(title, message) {
324 const notification = document.createElement('div');
325 notification.style.cssText = `
326 position: fixed;
327 top: 20px;
328 right: 20px;
329 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
330 color: white;
331 padding: 20px;
332 border-radius: 10px;
333 box-shadow: 0 10px 30px rgba(0,0,0,0.3);
334 z-index: 999999;
335 max-width: 350px;
336 font-family: Arial, sans-serif;
337 animation: slideIn 0.3s ease-out;
338 `;
339
340 notification.innerHTML = `
341 <div style="font-size: 18px; font-weight: bold; margin-bottom: 8px;">${title}</div>
342 <div style="font-size: 14px; opacity: 0.9;">${message}</div>
343 `;
344
345 document.body.appendChild(notification);
346
347 setTimeout(() => {
348 notification.style.animation = 'slideOut 0.3s ease-in';
349 setTimeout(() => notification.remove(), 300);
350 }, 5000);
351 }
352
353 // Play Notification Sound
354 function playNotificationSound() {
355 try {
356 const audioContext = new (window.AudioContext || window.webkitAudioContext)();
357 const oscillator = audioContext.createOscillator();
358 const gainNode = audioContext.createGain();
359
360 oscillator.connect(gainNode);
361 gainNode.connect(audioContext.destination);
362
363 oscillator.frequency.value = 800;
364 oscillator.type = 'sine';
365
366 gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
367 gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5);
368
369 oscillator.start(audioContext.currentTime);
370 oscillator.stop(audioContext.currentTime + 0.5);
371 } catch (error) {
372 console.error('Error playing sound:', error);
373 }
374 }
375
376 // Create Control Panel UI
377 function createControlPanel() {
378 log('Creating control panel');
379
380 // Check if panel already exists
381 if (document.getElementById('bls-control-panel')) {
382 return;
383 }
384
385 const panel = document.createElement('div');
386 panel.id = 'bls-control-panel';
387 panel.style.cssText = `
388 position: fixed;
389 bottom: 20px;
390 right: 20px;
391 width: 350px;
392 background: white;
393 border-radius: 12px;
394 box-shadow: 0 10px 40px rgba(0,0,0,0.2);
395 z-index: 999998;
396 font-family: Arial, sans-serif;
397 overflow: hidden;
398 `;
399
400 panel.innerHTML = `
401 <style>
402 @keyframes slideIn {
403 from { transform: translateX(400px); opacity: 0; }
404 to { transform: translateX(0); opacity: 1; }
405 }
406 @keyframes slideOut {
407 from { transform: translateX(0); opacity: 1; }
408 to { transform: translateX(400px); opacity: 0; }
409 }
410 .bls-btn {
411 padding: 10px 20px;
412 border: none;
413 border-radius: 6px;
414 cursor: pointer;
415 font-size: 14px;
416 font-weight: 600;
417 transition: all 0.3s;
418 width: 100%;
419 margin-top: 8px;
420 }
421 .bls-btn-primary {
422 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
423 color: white;
424 }
425 .bls-btn-primary:hover {
426 transform: translateY(-2px);
427 box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
428 }
429 .bls-btn-secondary {
430 background: #f0f0f0;
431 color: #333;
432 }
433 .bls-btn-secondary:hover {
434 background: #e0e0e0;
435 }
436 .bls-input {
437 width: 100%;
438 padding: 10px;
439 border: 2px solid #e0e0e0;
440 border-radius: 6px;
441 font-size: 14px;
442 margin-top: 8px;
443 box-sizing: border-box;
444 }
445 .bls-input:focus {
446 outline: none;
447 border-color: #667eea;
448 }
449 .bls-checkbox {
450 margin-right: 8px;
451 }
452 .bls-status {
453 display: inline-block;
454 width: 10px;
455 height: 10px;
456 border-radius: 50%;
457 margin-right: 8px;
458 }
459 .bls-status-active {
460 background: #4caf50;
461 box-shadow: 0 0 10px #4caf50;
462 }
463 .bls-status-inactive {
464 background: #ccc;
465 }
466 </style>
467 <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 15px;">
468 <div style="display: flex; justify-content: space-between; align-items: center;">
469 <h3 style="margin: 0; font-size: 16px;">BLS Auto Booker</h3>
470 <button id="bls-toggle-panel" style="background: rgba(255,255,255,0.2); border: none; color: white; padding: 5px 10px; border-radius: 4px; cursor: pointer;">−</button>
471 </div>
472 </div>
473 <div id="bls-panel-content" style="padding: 15px;">
474 <div style="margin-bottom: 15px;">
475 <div style="display: flex; align-items: center; margin-bottom: 10px;">
476 <span class="bls-status bls-status-inactive" id="bls-status-indicator"></span>
477 <span style="font-size: 14px; color: #666;" id="bls-status-text">Monitoring: Inactive</span>
478 </div>
479 </div>
480
481 <div style="margin-bottom: 15px;">
482 <label style="display: block; font-size: 14px; font-weight: 600; color: #333; margin-bottom: 5px;">Login Credentials</label>
483 <input type="email" id="bls-email" class="bls-input" placeholder="Email/Username">
484 <input type="password" id="bls-password" class="bls-input" placeholder="Password">
485 <button id="bls-save-credentials" class="bls-btn bls-btn-secondary">Save Credentials</button>
486 </div>
487
488 <div style="margin-bottom: 15px;">
489 <label style="display: flex; align-items: center; font-size: 14px; color: #333; cursor: pointer;">
490 <input type="checkbox" id="bls-auto-book" class="bls-checkbox">
491 <span>Enable Auto-Booking</span>
492 </label>
493 <label style="display: flex; align-items: center; font-size: 14px; color: #333; cursor: pointer; margin-top: 8px;">
494 <input type="checkbox" id="bls-notification-sound" class="bls-checkbox" checked>
495 <span>Enable Notification Sound</span>
496 </label>
497 </div>
498
499 <div style="margin-bottom: 15px;">
500 <label style="display: block; font-size: 14px; font-weight: 600; color: #333; margin-bottom: 5px;">Check Interval (seconds)</label>
501 <input type="number" id="bls-check-interval" class="bls-input" value="30" min="10" max="300">
502 </div>
503
504 <button id="bls-start-monitoring" class="bls-btn bls-btn-primary">Start Monitoring</button>
505 <button id="bls-stop-monitoring" class="bls-btn bls-btn-secondary" style="display: none;">Stop Monitoring</button>
506 <button id="bls-check-now" class="bls-btn bls-btn-secondary">Check Now</button>
507 <button id="bls-auto-fill-login" class="bls-btn bls-btn-secondary">Auto-Fill Login</button>
508 </div>
509 `;
510
511 document.body.appendChild(panel);
512 attachPanelEventListeners();
513 loadPanelSettings();
514 }
515
516 // Attach Event Listeners to Panel
517 function attachPanelEventListeners() {
518 // Toggle panel
519 document.getElementById('bls-toggle-panel').addEventListener('click', () => {
520 const content = document.getElementById('bls-panel-content');
521 const button = document.getElementById('bls-toggle-panel');
522 if (content.style.display === 'none') {
523 content.style.display = 'block';
524 button.textContent = '−';
525 } else {
526 content.style.display = 'none';
527 button.textContent = '+';
528 }
529 });
530
531 // Save credentials
532 document.getElementById('bls-save-credentials').addEventListener('click', async () => {
533 const email = document.getElementById('bls-email').value;
534 const password = document.getElementById('bls-password').value;
535
536 if (email && password) {
537 await saveCredentials(email, password);
538 showPageNotification('Success', 'Credentials saved successfully!');
539 } else {
540 showPageNotification('Error', 'Please enter both email and password');
541 }
542 });
543
544 // Start monitoring
545 document.getElementById('bls-start-monitoring').addEventListener('click', async () => {
546 await startMonitoring();
547 });
548
549 // Stop monitoring
550 document.getElementById('bls-stop-monitoring').addEventListener('click', () => {
551 stopMonitoring();
552 });
553
554 // Check now
555 document.getElementById('bls-check-now').addEventListener('click', async () => {
556 await checkAppointmentSlots();
557 });
558
559 // Auto-fill login
560 document.getElementById('bls-auto-fill-login').addEventListener('click', async () => {
561 await autoFillLogin();
562 });
563
564 // Save config on change
565 document.getElementById('bls-auto-book').addEventListener('change', saveConfigFromPanel);
566 document.getElementById('bls-notification-sound').addEventListener('change', saveConfigFromPanel);
567 document.getElementById('bls-check-interval').addEventListener('change', saveConfigFromPanel);
568 }
569
570 // Load Panel Settings
571 async function loadPanelSettings() {
572 const credentials = await getCredentials();
573 const config = await getConfig();
574
575 document.getElementById('bls-email').value = credentials.email || '';
576 document.getElementById('bls-password').value = credentials.password || '';
577 document.getElementById('bls-auto-book').checked = config.autoBook || false;
578 document.getElementById('bls-notification-sound').checked = config.notificationSound !== false;
579 document.getElementById('bls-check-interval').value = (config.checkInterval || 30000) / 1000;
580 }
581
582 // Save Config from Panel
583 async function saveConfigFromPanel() {
584 const config = {
585 autoBook: document.getElementById('bls-auto-book').checked,
586 notificationSound: document.getElementById('bls-notification-sound').checked,
587 checkInterval: parseInt(document.getElementById('bls-check-interval').value) * 1000
588 };
589
590 await saveConfig(config);
591 log('Config updated from panel');
592 }
593
594 // Start Monitoring
595 async function startMonitoring() {
596 if (isChecking) {
597 log('Monitoring already active');
598 return;
599 }
600
601 const config = await getConfig();
602 const interval = config.checkInterval || 30000;
603
604 isChecking = true;
605 updateMonitoringStatus(true);
606
607 log(`Starting monitoring with ${interval/1000}s interval`);
608 showPageNotification('Monitoring Started', `Checking every ${interval/1000} seconds`);
609
610 // Initial check
611 await checkAppointmentSlots();
612
613 // Set up interval
614 checkInterval = setInterval(async () => {
615 await checkAppointmentSlots();
616 }, interval);
617 }
618
619 // Stop Monitoring
620 function stopMonitoring() {
621 if (!isChecking) {
622 log('Monitoring not active');
623 return;
624 }
625
626 isChecking = false;
627 updateMonitoringStatus(false);
628
629 if (checkInterval) {
630 clearInterval(checkInterval);
631 checkInterval = null;
632 }
633
634 log('Monitoring stopped');
635 showPageNotification('Monitoring Stopped', 'Appointment checking has been stopped');
636 }
637
638 // Update Monitoring Status UI
639 function updateMonitoringStatus(active) {
640 const statusIndicator = document.getElementById('bls-status-indicator');
641 const statusText = document.getElementById('bls-status-text');
642 const startButton = document.getElementById('bls-start-monitoring');
643 const stopButton = document.getElementById('bls-stop-monitoring');
644
645 if (active) {
646 statusIndicator.className = 'bls-status bls-status-active';
647 statusText.textContent = 'Monitoring: Active';
648 startButton.style.display = 'none';
649 stopButton.style.display = 'block';
650 } else {
651 statusIndicator.className = 'bls-status bls-status-inactive';
652 statusText.textContent = 'Monitoring: Inactive';
653 startButton.style.display = 'block';
654 stopButton.style.display = 'none';
655 }
656 }
657
658 // Capture Form Data for Future Auto-fill
659 function captureFormData() {
660 const forms = document.querySelectorAll('form');
661
662 forms.forEach(form => {
663 form.addEventListener('submit', async (e) => {
664 const formData = {};
665 const inputs = form.querySelectorAll('input, select, textarea');
666
667 inputs.forEach(input => {
668 if (input.name && input.value && input.type !== 'password') {
669 formData[input.name] = input.value;
670 }
671 });
672
673 if (Object.keys(formData).length > 0) {
674 await saveFormData(formData);
675 log('Form data captured and saved');
676 }
677 });
678 });
679 }
680
681 // Initialize Extension
682 async function init() {
683 log('BLS Auto Appointment Booker initialized');
684
685 // Wait for page to load
686 if (document.readyState === 'loading') {
687 document.addEventListener('DOMContentLoaded', init);
688 return;
689 }
690
691 // Wait a bit for dynamic content to load
692 setTimeout(() => {
693 // Create control panel
694 createControlPanel();
695
696 // Auto-fill login if on login page
697 if (window.location.href.includes('login')) {
698 setTimeout(() => autoFillLogin(), 1000);
699 }
700
701 // Capture form data for future use
702 captureFormData();
703
704 // Set up mutation observer for dynamic content
705 const observer = new MutationObserver(debounce(() => {
706 if (!document.getElementById('bls-control-panel')) {
707 createControlPanel();
708 }
709 }, 1000));
710
711 observer.observe(document.body, {
712 childList: true,
713 subtree: true
714 });
715
716 log('Extension fully loaded and ready');
717 }, 2000);
718 }
719
720 // Start the extension
721 init();
722
723})();