diff --git a/job-notifier/andela-appscript/Code.gs b/job-notifier/andela-appscript/Code.gs new file mode 100644 index 0000000..7066dd4 --- /dev/null +++ b/job-notifier/andela-appscript/Code.gs @@ -0,0 +1,173 @@ +// Code.gs — Google Apps Script Web App +// Deploy as: Web app, Execute as "Me", Who has access: "Anyone" + +// ============================================ +// CONFIGURATION — change these two, then create a NEW deployment version +// ============================================ +var SECRET_KEY = '5f5bd4c1751f7582f870954da60cafe2'; // e.g. output of: openssl rand -hex 16 +var ALLOWED_EMAIL = 'justiceawudi@outlook.com'; // must match "Login Email" in extension Settings + +function doPost(e) { + try { + var data = JSON.parse(e.postData.contents); + + if (!data.key || data.key !== SECRET_KEY) { + Logger.log('Unauthorized request - invalid key'); + return ContentService.createTextOutput( + JSON.stringify({ error: 'Unauthorized' }), + ).setMimeType(ContentService.MimeType.JSON); + } + + if (data.email !== ALLOWED_EMAIL) { + Logger.log('Unauthorized request - invalid email: ' + data.email); + return ContentService.createTextOutput( + JSON.stringify({ error: 'Unauthorized email' }), + ).setMimeType(ContentService.MimeType.JSON); + } + + if (data.action === 'pollForLoginLink') { + var result = pollForMagicLink(data.requestId); + return ContentService.createTextOutput( + JSON.stringify(result), + ).setMimeType(ContentService.MimeType.JSON); + } + + return ContentService.createTextOutput( + JSON.stringify({ error: 'Unknown action' }), + ).setMimeType(ContentService.MimeType.JSON); + } catch (err) { + Logger.log('Error in doPost: ' + err.message); + return ContentService.createTextOutput( + JSON.stringify({ error: err.message }), + ).setMimeType(ContentService.MimeType.JSON); + } +} + +// ============================================ +// Poll for magic login link. +// +// No "to:" filter — the email is sent to your Outlook address and +// lands in Gmail via a forwarding rule, so its To/Delivered-To headers +// may still reference Outlook rather than Gmail. from + subject + +// newer_than + is:unread is specific enough since this only ever +// searches your own mailbox. +// ============================================ +function pollForMagicLink(requestId) { + var maxAttempts = 4; + var pollInterval = 15000; + + Logger.log('[' + requestId + '] Starting poll for login link...'); + + for (var attempt = 1; attempt <= maxAttempts; attempt++) { + Logger.log('[' + requestId + '] Attempt ' + attempt + '/' + maxAttempts); + + var searchQuery = + 'from:talent.andela.com subject:"Andela Login Link" newer_than:5m is:unread'; + var threads = GmailApp.search(searchQuery, 0, 5); + + Logger.log( + '[' + requestId + '] Found ' + threads.length + ' matching threads', + ); + + if (threads.length > 0) { + var allMessages = []; + for (var i = 0; i < threads.length; i++) { + var messages = threads[i].getMessages(); + for (var j = 0; j < messages.length; j++) allMessages.push(messages[j]); + } + + allMessages.sort(function (a, b) { + return b.getDate().getTime() - a.getDate().getTime(); + }); + + var latestMessage = allMessages[0]; + var body = latestMessage.getBody(); + + Logger.log( + '[' + requestId + '] Processing email from: ' + latestMessage.getDate(), + ); + + var accessAccountPattern = + /]*href=["']([^"']+)["'][^>]*>\s*Access account\s*<\/a>/i; + var match = body.match(accessAccountPattern); + + if (match && match[1]) { + var link = match[1] + .replace(/&/g, '&') + .replace(/=/g, '=') + .replace(/=/g, '=') + .replace(/&/g, '&'); + + Logger.log('[' + requestId + '] Found Access account link: ' + link); + latestMessage.markRead(); + + return { + success: true, + loginLink: link, + emailDate: latestMessage.getDate().toISOString(), + attempt: attempt, + }; + } + + Logger.log( + '[' + requestId + '] Email found but no Access account link detected', + ); + } + + if (attempt < maxAttempts) { + Logger.log('[' + requestId + '] Waiting ' + pollInterval / 1000 + 's...'); + Utilities.sleep(pollInterval); + } + } + + Logger.log( + '[' + + requestId + + '] No login link found after ' + + maxAttempts + + ' attempts', + ); + return { + success: false, + error: 'No login link found within timeout', + attempts: maxAttempts, + }; +} + +// ============================================ +// Debug helpers — run manually from the Apps Script editor's function +// dropdown, then check the Executions log +// ============================================ +function debugLoginEmails() { + var searchQuery = + 'from:talent.andela.com subject:"Andela Login Link" newer_than:7d'; + var threads = GmailApp.search(searchQuery, 0, 5); + Logger.log('Found ' + threads.length + ' emails'); + + for (var i = 0; i < threads.length; i++) { + var messages = threads[i].getMessages(); + for (var j = 0; j < messages.length; j++) { + var msg = messages[j]; + Logger.log('---'); + Logger.log('Subject: ' + msg.getSubject()); + Logger.log('From: ' + msg.getFrom()); + Logger.log('To: ' + msg.getTo()); + Logger.log('Date: ' + msg.getDate()); + + var body = msg.getBody(); + var pattern = + /]*href=["']([^"']+)["'][^>]*>\s*Access account\s*<\/a>/i; + var match = body.match(pattern); + if (match) { + Logger.log('Link found: ' + match[1].substring(0, 100)); + } else { + Logger.log('Access account link not found'); + } + } + } +} + +function testExtraction() { + var result = pollForMagicLink('test-' + Date.now()); + Logger.log(JSON.stringify(result, null, 2)); +} diff --git a/job-notifier/andela-appscript/README.md b/job-notifier/andela-appscript/README.md new file mode 100644 index 0000000..25707a3 --- /dev/null +++ b/job-notifier/andela-appscript/README.md @@ -0,0 +1,19 @@ +# Andela Apps Script (Gmail magic-link fetcher) + +Deploy this as a Google Apps Script Web App under the Google account whose +Gmail receives the forwarded Andela login emails. + +## Setup +1. https://script.google.com → New project → paste `Code.gs` in as `Code.gs`. +2. Edit `SECRET_KEY` (generate with `openssl rand -hex 16`) and `ALLOWED_EMAIL` + (the email typed into the Andela login form — your Outlook address, not Gmail). +3. Deploy → New deployment → Web app → Execute as **Me** → Who has access **Anyone**. +4. Authorize Gmail access when prompted, then copy the `/exec` URL. +5. Put that URL + the same SECRET_KEY + the same email into the Chrome + extension's Settings page. +6. Whenever you edit this file: Deploy → Manage deployments → Edit → + Version: New version → Deploy (saving alone does not update the live web app). + +## Debugging +Run `debugLoginEmails` from the function dropdown in the editor, then check +Executions in the left sidebar for the Logger output. diff --git a/job-notifier/background.js b/job-notifier/background.js new file mode 100644 index 0000000..aa25740 --- /dev/null +++ b/job-notifier/background.js @@ -0,0 +1,438 @@ +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, + }); + } +}); diff --git a/job-notifier/content.js b/job-notifier/content.js new file mode 100644 index 0000000..122ba15 --- /dev/null +++ b/job-notifier/content.js @@ -0,0 +1,202 @@ +// content.js - Handles job extraction and login automation for app.andela.com + +console.log('[Andela Content Script] Loaded on', window.location.href); + +function isLoginPage() { + var emailInput = document.querySelector('input[type="email"]#email'); + var emailLabel = document.querySelector('label[for="email"]'); + var hasLoginForm = !!(emailInput && emailLabel); + var isLoginUrl = window.location.pathname.indexOf('/login') === 0; + + console.log('[Andela Content Script] Login check - hasForm:', hasLoginForm, 'isLoginUrl:', isLoginUrl); + return hasLoginForm || (isLoginUrl && !!emailInput); +} + +function enterEmailAndClickLogin(email) { + try { + console.log('[Andela Content Script] Attempting to enter email:', email); + + var emailInput = document.querySelector('input[type="email"]#email'); + if (!emailInput) { + console.log('[Andela Content Script] Email input not found'); + return false; + } + + emailInput.focus(); + emailInput.value = email; + emailInput.dispatchEvent(new Event('input', { bubbles: true })); + emailInput.dispatchEvent(new Event('change', { bubbles: true })); + emailInput.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true })); + + console.log('[Andela Content Script] Email entered, looking for submit button...'); + + setTimeout(function() { + var submitButton = document.querySelector('button[type="submit"]'); + + if (!submitButton) { + var buttons = document.querySelectorAll('button'); + for (var i = 0; i < buttons.length; i++) { + var btnText = buttons[i].textContent.toLowerCase(); + if (btnText.indexOf('login') !== -1 || + btnText.indexOf('sign in') !== -1 || + btnText.indexOf('continue') !== -1 || + btnText.indexOf('submit') !== -1) { + submitButton = buttons[i]; + break; + } + } + } + + if (submitButton) { + console.log('[Andela Content Script] Clicking submit button:', submitButton.textContent); + submitButton.click(); + } else { + console.log('[Andela Content Script] Submit button not found, trying form submit'); + var form = emailInput.closest('form'); + if (form) { + if (form.requestSubmit) form.requestSubmit(); + else form.submit(); + } + } + }, 500); + + return true; + } catch (err) { + console.error('[Andela Content Script] Error entering email:', err); + return false; + } +} + +function expandAllSeeMoreButtons(maxRounds) { + maxRounds = maxRounds || 25; + return new Promise(function(resolve) { + var rounds = 0; + + function findSeeMoreButton() { + var buttons = document.querySelectorAll('button'); + for (var i = 0; i < buttons.length; i++) { + var text = (buttons[i].textContent || '').trim().toLowerCase(); + if (text.indexOf('see') === 0 && text.indexOf('more') !== -1) { + return buttons[i]; + } + } + return null; + } + + function clickNext() { + rounds++; + var btn = findSeeMoreButton(); + if (!btn || rounds > maxRounds) { + console.log('[Andela Content Script] Done expanding "See more" buttons after', rounds - 1, 'click(s)'); + resolve(); + return; + } + console.log('[Andela Content Script] Clicking:', btn.textContent.trim()); + btn.click(); + setTimeout(clickNext, 700); + } + + clickNext(); + }); +} + +function extractJobFromLink(link) { + var href = link.getAttribute('href') || ''; + var idMatch = href.match(/\/job-postings\/(\d+)/); + var id = idMatch ? idMatch[1] : href; + + var card = link.closest('div.rounded-lg.border.border-neutral-200.bg-white') || link.parentElement; + + var title = ''; + var company = ''; + var titleEl = card ? card.querySelector('.text-primary-900.font-semibold') : null; + + if (titleEl) { + var clone = titleEl.cloneNode(true); + var companySpan = clone.querySelector('span.font-normal'); + if (companySpan) { + company = companySpan.textContent.replace(/^[\s—-]+/, '').trim(); + companySpan.remove(); + } + title = clone.textContent.trim(); + } + + var paragraphs = card ? card.querySelectorAll('p') : []; + var startInfo = paragraphs[0] ? paragraphs[0].textContent.trim() : ''; + var locationInfo = paragraphs[1] ? paragraphs[1].textContent.trim() : ''; + + var skillEls = card ? card.querySelectorAll('.flex.flex-wrap.gap-2 span') : []; + var skills = Array.prototype.map.call(skillEls, function(s) { return s.textContent.trim(); }); + + var url = href; + if (url && url.indexOf('http') !== 0) { + url = window.location.origin + url; + } + + return { + id: id, + title: title, + company: company || 'Company', + posted_date: startInfo || '', + location: locationInfo || '', + skills: skills, + url: url + }; +} + +function extractJobsFromPage() { + console.log('[Andela Content Script] Extracting jobs...'); + var jobs = []; + var seen = {}; + var links = document.querySelectorAll('a[href*="/job-postings/"]'); + console.log('[Andela Content Script] Found ' + links.length + ' job-posting links'); + + links.forEach(function(link) { + try { + var job = extractJobFromLink(link); + if (!job.id || seen[job.id]) return; + seen[job.id] = true; + if (job.title) { + jobs.push(job); + console.log('[Andela Content Script] Extracted job:', job.title, '(' + job.id + ')'); + } + } catch (err) { + console.error('[Andela Content Script] Error extracting a job card:', err); + } + }); + + console.log('[Andela Content Script] Extracted ' + jobs.length + ' jobs total'); + return jobs; +} + +chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { + console.log('[Andela Content Script] Received message:', request); + + if (request.action === 'extractJobs') { + expandAllSeeMoreButtons().then(function() { + setTimeout(function() { + try { + var jobs = extractJobsFromPage(); + sendResponse({ jobs: jobs }); + } catch (error) { + console.error('[Andela Content Script] Error:', error); + sendResponse({ jobs: [] }); + } + }, 500); + }); + return true; + } + + if (request.action === 'checkLoginPage') { + var result = isLoginPage(); + console.log('[Andela Content Script] Is login page:', result); + sendResponse({ isLoginPage: result }); + } + + if (request.action === 'enterEmailAndLogin') { + var success = enterEmailAndClickLogin(request.email); + sendResponse({ success: success }); + } + + return true; +}); diff --git a/job-notifier/icon.svg b/job-notifier/icon.svg new file mode 100644 index 0000000..895faee --- /dev/null +++ b/job-notifier/icon.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/job-notifier/icon128.png b/job-notifier/icon128.png new file mode 100644 index 0000000..3b1d689 Binary files /dev/null and b/job-notifier/icon128.png differ diff --git a/job-notifier/icon16.png b/job-notifier/icon16.png new file mode 100644 index 0000000..c58b3a7 Binary files /dev/null and b/job-notifier/icon16.png differ diff --git a/job-notifier/icon48.png b/job-notifier/icon48.png new file mode 100644 index 0000000..ad44d5f Binary files /dev/null and b/job-notifier/icon48.png differ diff --git a/job-notifier/manifest.json b/job-notifier/manifest.json new file mode 100644 index 0000000..d30d926 --- /dev/null +++ b/job-notifier/manifest.json @@ -0,0 +1,47 @@ +{ + "manifest_version": 3, + "name": "Andela Job Notifier", + "version": "2.0", + "description": "Checks for new job opportunities on Andela with auto-login", + "permissions": [ + "notifications", + "alarms", + "storage", + "offscreen", + "tabs", + "scripting" + ], + "host_permissions": [ + "https://app.andela.com/*", + "https://notify.tstitagency.com/*", + "https://script.google.com/*", + "https://script.googleusercontent.com/*" + ], + "background": { + "service_worker": "background.js" + }, + "content_scripts": [ + { + "matches": ["https://app.andela.com/*"], + "js": ["content.js"], + "run_at": "document_idle" + } + ], + "action": { + "default_popup": "popup.html", + "default_icon": { + "16": "icon16.png", + "48": "icon48.png", + "128": "icon128.png" + } + }, + "icons": { + "16": "icon16.png", + "48": "icon48.png", + "128": "icon128.png" + }, + "web_accessible_resources": [{ + "resources": ["notify.wav"], + "matches": [""] + }] +} diff --git a/job-notifier/notify.wav b/job-notifier/notify.wav new file mode 100644 index 0000000..22d8da9 Binary files /dev/null and b/job-notifier/notify.wav differ diff --git a/job-notifier/offscreen.html b/job-notifier/offscreen.html new file mode 100644 index 0000000..720ac6b --- /dev/null +++ b/job-notifier/offscreen.html @@ -0,0 +1,9 @@ + + + + Sound Player + + + + + diff --git a/job-notifier/offscreen.js b/job-notifier/offscreen.js new file mode 100644 index 0000000..78308c1 --- /dev/null +++ b/job-notifier/offscreen.js @@ -0,0 +1,21 @@ +let audioContext; +let audioBuffer; + +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.action === 'playSound') { + playNotificationSound(); + } +}); + +async function playNotificationSound() { + if (!audioContext) { + audioContext = new AudioContext(); + const response = await fetch(chrome.runtime.getURL('notify.wav')); + const arrayBuffer = await response.arrayBuffer(); + audioBuffer = await audioContext.decodeAudioData(arrayBuffer); + } + const source = audioContext.createBufferSource(); + source.buffer = audioBuffer; + source.connect(audioContext.destination); + source.start(0); +} diff --git a/job-notifier/popup.html b/job-notifier/popup.html new file mode 100644 index 0000000..f3556b5 --- /dev/null +++ b/job-notifier/popup.html @@ -0,0 +1,104 @@ + + + + Andela Job Notifier + + + +

Andela Job Notifier

+
+
+ + +
+
+
+ + + diff --git a/job-notifier/popup.js b/job-notifier/popup.js new file mode 100644 index 0000000..12fffbe --- /dev/null +++ b/job-notifier/popup.js @@ -0,0 +1,76 @@ +// popup.js + +function sortJobsById(jobs) { + return jobs.slice().sort(function(a, b) { + return (parseInt(b.id, 10) || 0) - (parseInt(a.id, 10) || 0); + }); +} + +function updateUI(data) { + var lastAndelaJobs = data.lastAndelaJobs || []; + var lastAndelaUpdated = data.lastAndelaUpdated; + var totalAndelaMatching = data.totalAndelaMatching || 0; + + document.getElementById('jobCount').textContent = totalAndelaMatching + ' jobs found'; + + var jobList = document.getElementById('jobList'); + jobList.innerHTML = ''; + + if (lastAndelaJobs.length === 0) { + jobList.innerHTML = '
No jobs found yet. Click "Check Now" to scan for jobs.
'; + } else { + var sortedJobs = sortJobsById(lastAndelaJobs); + + sortedJobs.slice(0, 10).forEach(function(job) { + var jobElement = document.createElement('div'); + jobElement.className = 'job-item'; + + var postedDate = job.posted_date || 'Recently posted'; + var companyName = typeof job.company === 'object' ? (job.company && job.company.name || 'Company') : (job.company || 'Company'); + + jobElement.innerHTML = + '
' + job.title + '
' + + '
' + companyName + '
' + + '
' + (job.location || 'Remote') + '
' + + '
' + postedDate + '
'; + + jobElement.addEventListener('click', function() { + var url = job.url || ('https://app.andela.com/job-postings/' + job.id); + chrome.tabs.create({ url: url }); + }); + + jobList.appendChild(jobElement); + }); + } + + var status = document.getElementById('status'); + status.textContent = lastAndelaUpdated ? + 'Last checked: ' + new Date(lastAndelaUpdated).toLocaleString() : + 'Not checked yet'; +} + +document.addEventListener('DOMContentLoaded', function() { + document.getElementById('checkNow').addEventListener('click', function() { + console.log('Manual check triggered'); + chrome.runtime.sendMessage({ action: 'checkNow' }, function(response) { + console.log('Check initiated'); + }); + }); + + document.getElementById('openSettings').addEventListener('click', function() { + chrome.tabs.create({ url: 'settings.html' }); + }); + + chrome.storage.local.get(['lastAndelaJobs', 'lastAndelaUpdated', 'totalAndelaMatching'], function(result) { + updateUI(result); + }); + + chrome.storage.onChanged.addListener(function(changes, areaName) { + if (areaName === 'local' && (changes.lastAndelaJobs || changes.lastAndelaUpdated || changes.totalAndelaMatching)) { + console.log('Storage updated, refreshing UI...'); + chrome.storage.local.get(['lastAndelaJobs', 'lastAndelaUpdated', 'totalAndelaMatching'], function(result) { + updateUI(result); + }); + } + }); +}); diff --git a/job-notifier/settings.html b/job-notifier/settings.html new file mode 100644 index 0000000..bca0101 --- /dev/null +++ b/job-notifier/settings.html @@ -0,0 +1,83 @@ + + + +Andela Notifier - Settings + + + +

Settings

+ +
+ Your credentials are stored locally in your browser and never sent anywhere except to your own Apps Script. +
+ +
+ + +
Your deployed Apps Script web app URL
+
+ +
+ + +
Must match SECRET_KEY in your Apps Script
+
+ +
+ + +
The email Andela actually has on file — this is typed into the Andela login form (not your Gmail forwarding address)
+
+ +
+ + +
Your self-hosted ntfy server (leave default if unsure)
+
+ +
+ + +
Topic to publish job alerts to. Leave empty to disable phone push.
+
+ +
+ + +
Only needed if your ntfy server requires authentication
+
+ + + + + +← Back to Jobs + + + + diff --git a/job-notifier/settings.js b/job-notifier/settings.js new file mode 100644 index 0000000..d785112 --- /dev/null +++ b/job-notifier/settings.js @@ -0,0 +1,69 @@ +// settings.js - Handle secure configuration storage + +document.addEventListener('DOMContentLoaded', function () { + chrome.storage.local.get( + [ + 'appsScriptUrl', + 'appsScriptKey', + 'loginEmail', + 'ntfyServer', + 'ntfyTopic', + 'ntfyToken', + ], + function (result) { + if (result.appsScriptUrl) + document.getElementById('appsScriptUrl').value = result.appsScriptUrl; + if (result.appsScriptKey) + document.getElementById('appsScriptKey').value = result.appsScriptKey; + if (result.loginEmail) + document.getElementById('loginEmail').value = result.loginEmail; + document.getElementById('ntfyServer').value = + result.ntfyServer || 'https://notify.tstitagency.com'; + if (result.ntfyTopic) + document.getElementById('ntfyTopic').value = result.ntfyTopic; + if (result.ntfyToken) + document.getElementById('ntfyToken').value = result.ntfyToken; + }, + ); + + document.getElementById('saveBtn').addEventListener('click', function () { + var appsScriptUrl = document.getElementById('appsScriptUrl').value.trim(); + var appsScriptKey = document.getElementById('appsScriptKey').value.trim(); + var loginEmail = document.getElementById('loginEmail').value.trim(); + var ntfyServer = document.getElementById('ntfyServer').value.trim(); + var ntfyTopic = document.getElementById('ntfyTopic').value.trim(); + var ntfyToken = document.getElementById('ntfyToken').value.trim(); + + if (!appsScriptUrl) + return showStatus('Please enter Apps Script URL', 'error'); + if (!appsScriptUrl.startsWith('https://script.google.com/')) + return showStatus('Invalid Apps Script URL', 'error'); + if (!appsScriptKey) return showStatus('Please enter Secret Key', 'error'); + if (!loginEmail || !loginEmail.includes('@')) + return showStatus('Please enter a valid email', 'error'); + + chrome.storage.local.set( + { + appsScriptUrl: appsScriptUrl, + appsScriptKey: appsScriptKey, + loginEmail: loginEmail, + ntfyServer: ntfyServer, + ntfyTopic: ntfyTopic, + ntfyToken: ntfyToken, + }, + function () { + showStatus('Settings saved successfully!', 'success'); + }, + ); + }); +}); + +function showStatus(message, type) { + var statusEl = document.getElementById('status'); + statusEl.textContent = message; + statusEl.className = 'status ' + type; + statusEl.style.display = 'block'; + setTimeout(function () { + statusEl.style.display = 'none'; + }, 3000); +}