Andela Notifications Updated
This commit is contained in:
438
job-notifier/background.js
Normal file
438
job-notifier/background.js
Normal file
@@ -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,
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user