Skip to content

Commit

Permalink
feat: initial widget logic
Browse files Browse the repository at this point in the history
  • Loading branch information
adit-bala committed Dec 19, 2024
1 parent 35bf539 commit baf714f
Show file tree
Hide file tree
Showing 11 changed files with 235 additions and 16 deletions.
2 changes: 2 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
### Todo

- [ ] implement CRON logic in normal extension use

### In Progress

- [ ] Widget to show unopened emails
Expand Down
2 changes: 1 addition & 1 deletion chrome-extension/js/inject.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Configuration and Logger
const isDev = false;
const isDev = true;
const DOMAIN = "stealthbyte.deno.dev";
const LOCALHOST = "localhost:8080";
const baseTrackingPixelUrl = isDev ? `http://${LOCALHOST}` : `https://${DOMAIN}`;
Expand Down
31 changes: 31 additions & 0 deletions chrome-extension/js/popup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
chrome.runtime.sendMessage({message: "fetch_user_data"}, function(response) {
if (response.error) {
console.error('Error:', response.error);
document.body.innerHTML = '<p>Error loading data. Please try again later.</p>';
} else {
displayUserInfo(response.userData);
displayEmails(response.userEmails);
}
});

function displayUserInfo(userData) {
const userInfoDiv = document.getElementById('user-info');
userInfoDiv.innerHTML = `
<p>Email: ${userData.email}</p>
<p>Emails sent this month: ${userData.emailsSentThisMonth}</p>
`;
}

function displayEmails(emails) {
const emailListDiv = document.getElementById('email-list');
emails.forEach(email => {
const emailDiv = document.createElement('div');
emailDiv.className = 'email-item';
emailDiv.innerHTML = `
<div class="email-subject">${email.subject}</div>
<div class="email-opens">Opened: ${email.numberOfOpens} times</div>
<div>Sent: ${new Date(Number(email.dateAtTimeOfSend)).toLocaleString()}</div>
`;
emailListDiv.appendChild(emailDiv);
});
}
95 changes: 92 additions & 3 deletions chrome-extension/js/service-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ const logger = {
let authToken = null;
const DOMAIN = "stealthbyte.deno.dev";
const LOCALHOST = "localhost:8080";
const isDev = false;
const serverUrl = isDev ? `http://${LOCALHOST}` : `https://${DOMAIN}`;
const isDev = true;
const SERVER_URL = isDev ? `http://${LOCALHOST}` : `https://${DOMAIN}`;
// Function to get OAuth2 token
function getAuthToken(interactive) {
return new Promise((resolve, reject) => {
Expand Down Expand Up @@ -141,6 +141,10 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
logger.info("Processing email data:", request.data.subject);
processEmail(request.data, sender); // Pass sender to processEmail
}
if (request.message === "fetch_user_data") {
fetchUserData().then(sendResponse);
return true; // Indicates that the response is sent asynchronously
}
});

// Function to process email data and initiate retry mechanism
Expand Down Expand Up @@ -282,7 +286,7 @@ chrome.alarms.onAlarm.addListener(async (alarm) => {
// Send a notification to the server
try {
const serverResponse = await fetch(
`${serverUrl}/${uniqueId}/pixel.png`,
`${SERVER_URL}/${uniqueId}/pixel.png`,
{
method: "POST",
headers: {
Expand Down Expand Up @@ -333,3 +337,88 @@ chrome.alarms.onAlarm.addListener(async (alarm) => {
logger.error(`Error handling alarm "${sleepId}":`, error.message);
}
});

async function fetchUserData() {
logger.info("Fetching user data...");
try {
const token = await ensureAuthenticated();
logger.info("Authentication token obtained");

const cacheStatus = await fetchCacheStatus(token);
logger.info("Cache status:", cacheStatus);

if (!cacheStatus.cached) {
logger.info("Cache is invalid, fetching new data from server");
const data = await fetchAllEmailData(token);
logger.info("New data fetched from server");
await storeDataInLocalStorage(data);
logger.info("New data stored in local storage");
return data;
} else {
logger.info("Cache is valid, retrieving data from local storage");
return await getDataFromLocalStorage();
}
} catch (error) {
logger.error('Error in fetchUserData:', error);
return { error: 'Failed to fetch user data' };
}
}

async function fetchCacheStatus(token) {
logger.info("Fetching cache status from server");
try {
const response = await fetch(`${SERVER_URL}/cache-status`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
logger.info("Cache status received:", data);
return data;
} catch (error) {
logger.error('Error fetching cache status:', error);
throw error;
}
}

async function fetchAllEmailData(token) {
logger.info("Fetching all email data from server");
try {
const response = await fetch(`${SERVER_URL}/all-email-data`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
logger.info("All email data received");
return data;
} catch (error) {
logger.error('Error fetching all email data:', error);
throw error;
}
}

async function storeDataInLocalStorage(data) {
logger.info("Storing data in local storage");
return new Promise((resolve) => {
chrome.storage.local.set({ 'userData': data }, () => {
logger.info("Data stored in local storage");
resolve();
});
});
}

async function getDataFromLocalStorage() {
logger.info("Retrieving data from local storage");
return new Promise((resolve) => {
chrome.storage.local.get('userData', (result) => {
if (result.userData) {
logger.info("Data retrieved from local storage");
resolve(result.userData);
} else {
logger.warn("No data found in local storage");
resolve(null);
}
});
});
}
7 changes: 5 additions & 2 deletions chrome-extension/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,8 @@
"https://www.googleapis.com/auth/userinfo.profile"
]
},
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo+GASpGqfyteWsX4CNqnyRpsDuH/ZbIfQemxI8mJifK2NZC+VsxBTULNsJj+V8vgFeTu5N0R2Gz5mTgiYnzRIo2xUfWUeNnC2xS2XigBYkGBziY7cT/eHFTT5x7pinEKK/FwByhlYfwHm6lOlrqWQJCnliUSfHMRCeVakh7hB6V7G2tt/CohZhmNKxPx3azsqSMq1zhtZc5jzSYcomfDO5SdO1mJ3ZXMweAERXs0AAYJ8vneDc0FzkPIeQVipSQhxSqeObtAK9jaM1SPaIZSoQ997+mrkRXXbvYhNBtiQ/eHoNIovarim0eYLbi80OfLhFqX6EYJGSFLHjYSHlpVeQIDAQAB"
}
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo+GASpGqfyteWsX4CNqnyRpsDuH/ZbIfQemxI8mJifK2NZC+VsxBTULNsJj+V8vgFeTu5N0R2Gz5mTgiYnzRIo2xUfWUeNnC2xS2XigBYkGBziY7cT/eHFTT5x7pinEKK/FwByhlYfwHm6lOlrqWQJCnliUSfHMRCeVakh7hB6V7G2tt/CohZhmNKxPx3azsqSMq1zhtZc5jzSYcomfDO5SdO1mJ3ZXMweAERXs0AAYJ8vneDc0FzkPIeQVipSQhxSqeObtAK9jaM1SPaIZSoQ997+mrkRXXbvYhNBtiQ/eHoNIovarim0eYLbi80OfLhFqX6EYJGSFLHjYSHlpVeQIDAQAB",
"action": {
"default_popup": "popup.html"
}
}
19 changes: 19 additions & 0 deletions chrome-extension/popup.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<title>Email Tracker Summary</title>
<style>
body { width: 300px; font-family: Arial, sans-serif; }
.email-item { margin-bottom: 10px; border-bottom: 1px solid #eee; padding-bottom: 5px; }
.email-subject { font-weight: bold; }
.email-opens { color: #666; }
</style>
</head>
<body>
<h2>Email Tracker Summary</h2>
<div id="user-info"></div>
<h3>Tracked Emails</h3>
<div id="email-list"></div>
<script src="js/popup.js"></script>
</body>
</html>
6 changes: 4 additions & 2 deletions web-server/html/home.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,10 @@ export const homePageHtml = `
<a href="/privacy-policy" class="button">View Privacy Policy</a>
</div>
<div class="footer">
&copy; ${new Date().getFullYear()} Stealth Byte | <a href="/privacy-policy">Privacy Policy</a>
&copy; ${
new Date().getFullYear()
} Stealth Byte | <a href="/privacy-policy">Privacy Policy</a>
</div>
</body>
</html>
`;
`;
2 changes: 1 addition & 1 deletion web-server/html/privacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,4 @@ export const privacyPolicyHtml = `
</div>
</body>
</html>
`;
`;
59 changes: 52 additions & 7 deletions web-server/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
extractNamesAndEmails,
formatDate,
getUserData,
listAllKeysAndValues,
getUserEmailsSorted,
returnImage,
updateUserData,
} from "@utils";
Expand All @@ -21,7 +21,8 @@ const subjectMapping: { [key: number]: string } = {
2: "Twice the attention! Your email '{subject}' was opened for the second time.",
3: "Your email '{subject}' has caught their eye for the third time!",
10: "10th open milestone—your email '{subject}' is making waves!",
20: "Your email '{subject}' has been revisited 20 times—keep the momentum going!",
20:
"Your email '{subject}' has been revisited 20 times—keep the momentum going!",
50: "Half a century of opens! Your email '{subject}' is a hit.",
};

Expand Down Expand Up @@ -187,7 +188,9 @@ router.get("/", (ctx) => {
if (timeDifferenceInSeconds <= thresholdInSeconds) {
// The request came in too soon after sending the email
console.log(
`Request came in ${timeDifferenceInSeconds.toFixed(2)} seconds after sending email. Not counting as an open.`
`Request came in ${
timeDifferenceInSeconds.toFixed(2)
} seconds after sending email. Not counting as an open.`,
);
returnImage(ctx);
return;
Expand Down Expand Up @@ -231,12 +234,15 @@ router.get("/", (ctx) => {
const emailLink =
`https://mail.google.com/mail/u/${userIndex}/#inbox/${emailData.email_id}`;

const emailSubject = subjectMapping[numberOfOpens].replace('{subject}', emailData.subject);
const emailSubject = subjectMapping[numberOfOpens].replace(
"{subject}",
emailData.subject,
);

const recipients = extractNamesAndEmails(emailData.recipient);
const recipientList = recipients.map(recipient => {
const recipientList = recipients.map((recipient) => {
return `${recipient.name} <a href="mailto:${recipient.email}">${recipient.email}</a>`;
}).join(', ');
}).join(", ");

const emailFrom = `Email-Tracker <no-reply@${
Deno.env.get("EMAIL_TRACKER_DOMAIN")
Expand Down Expand Up @@ -281,6 +287,8 @@ router.get("/", (ctx) => {

// Update user data after successful send
userData.emailsSentThisMonth += 1;
// Mark the user data as not cached to force frontend to refresh
userData.cached = false;
await updateUserData(userData, kv);
} catch (error) {
// Log the error and proceed
Expand All @@ -297,7 +305,35 @@ router.get("/", (ctx) => {
// ctx.response.status = 500;
// ctx.response.body = { error: "Internal server error" };
}
})
// endpoint to check if the user data has been updated since last being cached
.get("/cache-status", authorizationMiddleware, async (ctx) => {
// Get the user's cache status
console.log("Checking cache status for user:", ctx.state.email);
const email = ctx.state.email;
const userData = await getUserData(email, kv);
console.log("Cache status:", userData.cached);
ctx.response.body = { cached: userData.cached };
ctx.response.status = 200;
ctx.response.headers.set("Content-Type", "application/json");
})
// endpoint to fetch all email data for a given user
.get("/all-email-data", authorizationMiddleware, async (ctx) => {
// Get all email data for the user
console.log("Fetching all email data for user:", ctx.state.email);
const email = ctx.state.email;
const userData = await getUserData(email, kv);
const userEmails = await getUserEmailsSorted(email, kv);
console.log("All email data:", userEmails);
console.log("User data:", userData);
// Mark the user data as cached
userData.cached = true;
await updateUserData(userData, kv);
ctx.response.body = { userEmails, userData };
ctx.response.status = 200;
ctx.response.headers.set("Content-Type", "application/json");
});

router.post("/:uuid/pixel.png", authorizationMiddleware, async (ctx) => {
try {
const body = await ctx.request.body.json();
Expand All @@ -308,9 +344,18 @@ router.post("/:uuid/pixel.png", authorizationMiddleware, async (ctx) => {
numberOfOpens: 0,
storedAt: timestamp,
};
console.log("email_key: ", email_path_key);
console.log("email_path_key: ", email_path_key);
console.log("body: ", body);

// Store the email data
await kv.set(["emailData", email_path_key], emailData);

// Update the user's cache status
const senderEmail = ctx.state.email;
const userData = await getUserData(senderEmail, kv);
userData.cached = false;
await updateUserData(userData, kv);

ctx.response.status = 200;
ctx.response.body = { message: "Email data stored successfully" };
} catch (error) {
Expand Down
1 change: 1 addition & 0 deletions web-server/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ export interface UserData {
email: string;
emailsSentThisMonth: number;
lastReset: number;
cached: boolean;
}
27 changes: 27 additions & 0 deletions web-server/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export async function getUserData(
email: email.toLowerCase(),
emailsSentThisMonth: 0,
lastReset: now,
cached: false,
};
}

Expand All @@ -41,6 +42,32 @@ export async function updateUserData(userData: UserData, kv: Deno.Kv) {
await kv.set(userKey, userData);
}

export async function getUserEmailsSorted(
email: string,
kv: Deno.Kv,
): Promise<EmailData[]> {
const lowercaseEmail = email.toLowerCase();
const iterator = kv.list({ prefix: ["emailData"] });
const senderCache = new Map<string, string>();

return await Array.fromAsync(iterator)
.then((entries) =>
entries
.map((entry) => entry.value as EmailData)
.filter((emailData) => {
let senderEmail: string;
if (senderCache.has(emailData.sender)) {
senderEmail = senderCache.get(emailData.sender)!;
} else {
senderEmail = extractNamesAndEmails(emailData.sender)[0].email.toLowerCase();
senderCache.set(emailData.sender, senderEmail);
}
return senderEmail === lowercaseEmail;
})
.sort((a, b) => Number(b.dateAtTimeOfSend) - Number(a.dateAtTimeOfSend))
);
}

export const extractNamesAndEmails = (
input: string,
): Array<{ name: string; email: string }> => {
Expand Down

0 comments on commit baf714f

Please sign in to comment.