const CHECK_INTERVAL = 5; // minutes const DEBUG = true; const JOBS_URL = 'https://app.andela.com/'; const NOTIFICATION_ICON = 'icon128.png'; let isLoggingIn = false; function log(...args) { if (DEBUG) console.log('[Andela Notifier]', ...args); } async function getConfig() { return new Promise((resolve) => { chrome.storage.local.get( [ 'appsScriptUrl', 'appsScriptKey', 'loginEmail', 'ntfyServer', 'ntfyTopic', 'ntfyToken', ], (result) => { resolve({ url: result.appsScriptUrl || '', key: result.appsScriptKey || '', email: result.loginEmail || '', ntfyServer: result.ntfyServer || 'https://notify.tstitagency.com', ntfyTopic: result.ntfyTopic || '', ntfyToken: result.ntfyToken || '', }); }, ); }); } async function createOffscreen() { if (await chrome.offscreen.hasDocument()) return; await chrome.offscreen.createDocument({ url: 'offscreen.html', reasons: ['AUDIO_PLAYBACK'], justification: 'Playing notification sound', }); } async function getStoredData(keys) { return new Promise((resolve) => chrome.storage.local.get(keys, resolve)); } async function setStoredData(data) { return new Promise((resolve) => chrome.storage.local.set(data, resolve)); } async function checkIfLoginPage(tabId) { return new Promise((resolve) => { chrome.tabs.sendMessage(tabId, { action: 'checkLoginPage' }, (response) => { if (chrome.runtime.lastError) { log('Error checking login page:', chrome.runtime.lastError.message); resolve(false); } else { resolve(response?.isLoginPage || false); } }); }); } async function handleAutoLogin(tab) { isLoggingIn = true; try { const config = await getConfig(); if (!config.url || !config.key || !config.email) { log('Missing configuration! Please go to extension settings.'); chrome.notifications.create('config-missing', { type: 'basic', iconUrl: NOTIFICATION_ICON, title: 'Andela Notifier - Setup Required', message: 'Please configure your settings (Apps Script URL, Key, Email)', priority: 2, }); isLoggingIn = false; return false; } log('Step 1: Entering email and clicking login...'); const emailEntered = await new Promise((resolve) => { chrome.tabs.sendMessage( tab.id, { action: 'enterEmailAndLogin', email: config.email }, (response) => { if (chrome.runtime.lastError) { log('Error entering email:', chrome.runtime.lastError.message); resolve(false); } else { resolve(response?.success || false); } }, ); }); if (!emailEntered) { log('Failed to enter email'); isLoggingIn = false; return false; } log('Step 2: Requesting Apps Script to poll for magic link...'); const requestId = 'login-' + Date.now(); const response = await fetch(config.url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'pollForLoginLink', email: config.email, key: config.key, requestId: requestId, }), }); const result = await response.json(); log('Apps Script response:', result); if ( result.error === 'Unauthorized' || result.error === 'Unauthorized email' ) { log('Authentication failed - check your secret key / login email'); chrome.notifications.create('auth-failed', { type: 'basic', iconUrl: NOTIFICATION_ICON, title: 'Andela Notifier - Auth Failed', message: 'Secret key or email mismatch. Check your settings.', priority: 2, }); isLoggingIn = false; return false; } if (!result.success || !result.loginLink) { log('No magic link received from Apps Script'); isLoggingIn = false; return false; } log('Step 3: Visiting magic link...'); await chrome.tabs.update(tab.id, { url: result.loginLink }); await new Promise((resolve) => setTimeout(resolve, 5000)); const stillOnLogin = await checkIfLoginPage(tab.id); if (stillOnLogin) { log('Still on login page after magic link, login may have failed'); isLoggingIn = false; return false; } log('Login successful!'); isLoggingIn = false; return true; } catch (error) { log('Error in handleAutoLogin:', error.message); isLoggingIn = false; return false; } } async function fetchJobsViaContentScript() { log('fetchJobsViaContentScript: Starting...'); try { const tabs = await chrome.tabs.query({ url: 'https://app.andela.com/*' }); log('Found ' + tabs.length + ' existing Andela tabs'); let tab; let createdNewTab = false; if (tabs.length === 0) { log('Opening new tab for Andela dashboard...'); tab = await chrome.tabs.create({ url: JOBS_URL, active: false }); createdNewTab = true; await new Promise((resolve) => setTimeout(resolve, 5000)); } else { tab = tabs[0]; log('Using existing tab with ID: ' + tab.id); await chrome.tabs.update(tab.id, { url: JOBS_URL }); await new Promise((resolve) => setTimeout(resolve, 4000)); } const isLoginPage = await checkIfLoginPage(tab.id); if (isLoginPage) { log('Session expired! Starting login flow...'); if (isLoggingIn) { log('Already logging in, skipping...'); if (createdNewTab) chrome.tabs.remove(tab.id).catch(() => {}); return []; } const loginSuccess = await handleAutoLogin(tab); if (!loginSuccess) { log('Login failed, aborting job check'); if (createdNewTab) chrome.tabs.remove(tab.id).catch(() => {}); return []; } log('Login successful, waiting 2 minutes before resuming...'); await new Promise((resolve) => setTimeout(resolve, 120000)); await chrome.tabs.update(tab.id, { url: JOBS_URL }); await new Promise((resolve) => setTimeout(resolve, 4000)); } let jobs = []; try { log('Extracting jobs from dashboard (expanding "See more" first)...'); jobs = await new Promise((resolve) => { chrome.tabs.sendMessage( tab.id, { action: 'extractJobs' }, (response) => { if (chrome.runtime.lastError) { log('Content script error:', chrome.runtime.lastError.message); resolve([]); } else { resolve((response && response.jobs) || []); } }, ); }); log('Found ' + jobs.length + ' jobs'); } catch (err) { log('Error during job extraction:', err.message, err.stack); } if (createdNewTab && tab) { log('Closing created tab...'); chrome.tabs.remove(tab.id).catch(() => {}); } log( 'fetchJobsViaContentScript: Completed, found ' + jobs.length + ' unique jobs', ); return jobs; } catch (error) { log('Error in fetchJobsViaContentScript:', error.message, error.stack); return []; } } function sortJobsByRecency(jobs) { return jobs .slice() .sort((a, b) => (parseInt(b.id, 10) || 0) - (parseInt(a.id, 10) || 0)); } function isFrontendJob(title) { return /front[\s-]?end/i.test(title || ''); } async function sendNtfyNotification(job, companyName) { const config = await getConfig(); if (!config.ntfyTopic) { log('ntfy topic not configured, skipping push notification'); return; } const server = config.ntfyServer.replace(/\/+$/, ''); const headers = { 'Content-Type': 'application/json' }; if (config.ntfyToken) headers.Authorization = 'Bearer ' + config.ntfyToken; const frontend = isFrontendJob(job.title); const payload = { topic: config.ntfyTopic, title: frontend ? 'Your Perfect Fit - Frontend' : 'New Job on Andela!', message: job.title + ' - ' + companyName, tags: frontend ? ['star', 'computer'] : ['briefcase'], priority: frontend ? 5 : 4, }; if (job.url) payload.click = job.url; try { const response = await fetch(server, { method: 'POST', headers: headers, body: JSON.stringify(payload), }); if (!response.ok) { log('ntfy push failed: ' + response.status + ' ' + response.statusText); } else { log('ntfy push sent for job: ' + job.title); } } catch (error) { log('ntfy push error:', error.message); } } async function notifyNewJob(job) { log('Notifying for job: ' + job.title); await createOffscreen(); chrome.runtime.sendMessage({ action: 'playSound' }); const companyName = job.company || 'Company'; chrome.notifications.create('andela-job-' + job.id, { type: 'basic', iconUrl: NOTIFICATION_ICON, title: 'New Job on Andela!', message: job.title + ' - ' + companyName, priority: 2, requireInteraction: true, }); await sendNtfyNotification(job, companyName); } let keepAliveInterval = null; function startKeepAlive() { if (keepAliveInterval !== null) return; keepAliveInterval = setInterval(() => { chrome.runtime.getPlatformInfo(() => { if (chrome.runtime.lastError) { /* no-op, purpose is just to reset idle timer */ } }); }, 20000); } function stopKeepAlive() { if (keepAliveInterval !== null) { clearInterval(keepAliveInterval); keepAliveInterval = null; } } async function checkForNewJobs() { log('=== Starting job check ==='); log('Current time:', new Date().toLocaleString()); startKeepAlive(); try { const allJobs = await fetchJobsViaContentScript(); const sortedJobs = sortJobsByRecency(allJobs); const result = await getStoredData(['notifiedJobIds']); const notifiedJobIds = new Set(result.notifiedJobIds || []); log('Previously notified jobs: ' + notifiedJobIds.size); const newJobs = sortedJobs.filter((job) => !notifiedJobIds.has(job.id)); log('Found ' + newJobs.length + ' new jobs'); for (const job of newJobs) { await notifyNewJob(job); notifiedJobIds.add(job.id); } await setStoredData({ notifiedJobIds: Array.from(notifiedJobIds), lastAndelaJobs: sortedJobs, lastAndelaUpdated: new Date().toISOString(), totalAndelaMatching: sortedJobs.length, }); log('Notifications stored successfully'); } catch (error) { console.error('Error in checkForNewJobs:', error); log('Error details:', error.message, error.stack); } finally { stopKeepAlive(); } log('=== Job check completed ==='); } chrome.alarms.create('checkAndelaJobs', { periodInMinutes: CHECK_INTERVAL }); chrome.alarms.onAlarm.addListener((alarm) => { if (alarm.name === 'checkAndelaJobs') checkForNewJobs(); }); chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { log('Received message:', request); if (request.action === 'checkNow') { log('Manual check requested from popup'); checkForNewJobs(); sendResponse({ status: 'Check initiated' }); } return true; }); createOffscreen().catch(console.error); chrome.runtime.onInstalled.addListener(() => { log('Extension installed/updated, checking for jobs...'); checkForNewJobs(); testNotification(); }); async function testNotification() { log('Testing notification system...'); chrome.notifications.create('test-notification', { type: 'basic', iconUrl: NOTIFICATION_ICON, title: 'Andela Job Notifier Test', message: 'Extension is installed and working!', priority: 2, }); } log('Andela Job Notifier started at:', new Date().toLocaleString()); chrome.alarms.get('checkAndelaJobs', (alarm) => { if (alarm) { log( 'Alarm is set, next check at:', new Date(alarm.scheduledTime).toLocaleString(), ); } else { log('No alarm found, creating one...'); chrome.alarms.create('checkAndelaJobs', { periodInMinutes: CHECK_INTERVAL, delayInMinutes: 0.1, }); } });