Skip to content

Commit 2f95486

Browse files
committed
Integrated Gmail and Gcal
1 parent 6d08edd commit 2f95486

3 files changed

Lines changed: 622 additions & 91 deletions

File tree

src/index.ts

Lines changed: 154 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as ejs from 'ejs';
44
import * as path from 'path';
55
import Anthropic from '@anthropic-ai/sdk';
66
import { googleCalendarService, CreateEventData } from './services/googleCalendarService';
7+
import { gmailService } from './services/gmailService';
78

89
/**
910
* Interface representing a stored photo with metadata
@@ -382,15 +383,28 @@ class ExampleMentraOSApp extends AppServer {
382383
*/
383384
private async getRecentEmailsForContext(userId: string): Promise<any[]> {
384385
try {
385-
// This would integrate with the Gmail service from the dashboard
386-
// For now, return empty array as placeholder
387386
this.logger.debug(`Getting recent emails for context for user ${userId}`);
388387

389-
// TODO: Implement integration with gmailService from dashboard-sep
390-
// const gmailService = require('./dashboard-sep/src/services/gmailService');
391-
// return await gmailService.getUnrepliedEmails();
388+
if (!gmailService.isUserAuthorized(userId)) {
389+
this.logger.debug(`User ${userId} not authorized for Gmail access`);
390+
return [];
391+
}
392392

393-
return [];
393+
// Get recent emails from Gmail API
394+
const recentEmails = await gmailService.getRecentEmails(userId, 5);
395+
396+
// Convert to context format for email processing
397+
const emailContext = recentEmails.map(email => ({
398+
id: email.id,
399+
subject: email.subject,
400+
from: email.from,
401+
snippet: email.snippet,
402+
date: email.date,
403+
isRead: email.isRead
404+
}));
405+
406+
this.logger.debug(`Retrieved ${emailContext.length} emails for context`);
407+
return emailContext;
394408
} catch (error) {
395409
this.logger.warn('Failed to get recent emails for context:', error);
396410
return [];
@@ -805,71 +819,79 @@ class ExampleMentraOSApp extends AppServer {
805819
try {
806820
session.logger.info(`Processing email request: ${spokenText}`);
807821

808-
const requestBody = {
809-
messages: [
810-
{
811-
role: 'user',
812-
content: spokenText
813-
}
814-
],
815-
context_emails: await this.getRecentEmailsForContext(userId) // Populate with recent emails for context
816-
};
822+
// Check if user is authorized for Gmail access
823+
if (!gmailService.isUserAuthorized(userId)) {
824+
const authUrl = googleCalendarService.generateAuthUrl(userId);
825+
if (authUrl) {
826+
session.layouts.showTextWall("🔐 Please authorize Gmail access", { durationMs: 5000 });
827+
session.audio.speak("Please authorize Gmail access to use email features. Check your dashboard for the authorization link.", {
828+
voice_settings: { stability: 0.8, speed: 0.95 }
829+
});
830+
return;
831+
} else {
832+
throw new Error('Gmail not configured properly');
833+
}
834+
}
817835

818-
// Add timeout to prevent hanging
819-
const controller = new AbortController();
820-
const timeoutId = setTimeout(() => controller.abort(), 20000); // 20 second timeout for email processing
836+
// Get recent emails for context
837+
const contextEmails = await this.getRecentEmailsForContext(userId);
838+
session.logger.info(`Got ${contextEmails.length} emails for context`);
821839

822-
const result = await this.retryApiCall(async () => {
823-
const response = await fetch('https://email.globalstarxyz.com/chat', {
824-
method: 'POST',
825-
headers: {
826-
'Content-Type': 'application/json'
827-
},
828-
body: JSON.stringify(requestBody),
829-
signal: controller.signal
830-
});
840+
// Process the email request locally based on common email actions
841+
const lowerText = spokenText.toLowerCase();
842+
let responseMessage = "";
843+
let actionPerformed = false;
831844

832-
clearTimeout(timeoutId);
845+
if (lowerText.includes('check') || lowerText.includes('inbox') || lowerText.includes('unread')) {
846+
// Check email summary
847+
const unreadCount = await gmailService.getUnreadCount(userId);
848+
const recentEmails = await gmailService.getRecentEmails(userId, 3);
833849

834-
if (!response.ok) {
835-
const errorText = await response.text().catch(() => 'Unknown error');
836-
throw new Error(`Email agent API error: ${response.status} ${response.statusText} - ${errorText}`);
837-
}
850+
if (unreadCount === 0) {
851+
responseMessage = "You have no unread emails. Your inbox is all caught up!";
852+
} else {
853+
const emailSummary = recentEmails.slice(0, 2).map(email =>
854+
`${email.from}: ${email.subject}`
855+
).join('. ');
838856

839-
return await response.json();
840-
});
841-
session.logger.info(`Email agent response: ${JSON.stringify(result)}`);
857+
responseMessage = `You have ${unreadCount} unread email${unreadCount > 1 ? 's' : ''}. Recent messages: ${emailSummary}`;
858+
}
859+
actionPerformed = true;
842860

843-
// Validate response structure
844-
if (!result || typeof result !== 'object') {
845-
throw new Error('Invalid response from email agent');
846-
}
861+
} else if (lowerText.includes('reply') || lowerText.includes('respond')) {
862+
// Reply functionality
863+
responseMessage = "To reply to emails, please use the dashboard interface or specify which email you'd like to reply to.";
864+
actionPerformed = true;
847865

848-
// Show response to user with fallback message
849-
const responseMessage = result.assistant_reply || result.response || 'Email request processed';
850-
session.layouts.showTextWall(`📧 ${responseMessage}`, { durationMs: 8000 });
866+
} else if (lowerText.includes('send') || lowerText.includes('compose') || lowerText.includes('write')) {
867+
// Send/compose functionality
868+
responseMessage = "To compose and send emails, please use the dashboard interface where you can specify recipients and content.";
869+
actionPerformed = true;
851870

852-
// Play audio response through glasses speakers using ElevenLabs TTS
853-
try {
854-
await session.audio.speak(responseMessage, {
855-
voice_settings: {
856-
stability: 0.6, // Slightly more variation for email responses
857-
similarity_boost: 0.8, // Natural sounding
858-
style: 0.3, // More expressive for email interactions
859-
speed: 0.85 // Slightly faster for email responses
860-
}
861-
});
862-
session.logger.info('Email response played via TTS');
863-
} catch (ttsError) {
864-
session.logger.warn('Failed to play email response via TTS:', ttsError);
865-
// TTS failure shouldn't break the main functionality
871+
} else {
872+
// Generic email summary
873+
const unreadCount = await gmailService.getUnreadCount(userId);
874+
if (contextEmails.length > 0) {
875+
const latestEmail = contextEmails[0];
876+
responseMessage = `Latest email from ${latestEmail.from}: ${latestEmail.subject}. You have ${unreadCount} unread emails total.`;
877+
} else {
878+
responseMessage = `You have ${unreadCount} unread emails. Use the dashboard to manage your email.`;
879+
}
880+
actionPerformed = true;
866881
}
867882

868-
// Process actions if any (email sending, replies, etc.)
869-
if (result.actions && Array.isArray(result.actions) && result.actions.length > 0) {
870-
session.logger.info(`Processing ${result.actions.length} email actions`);
871-
for (const action of result.actions) {
872-
await this.processEmailAction(action, session, userId);
883+
if (actionPerformed) {
884+
// Show response to user
885+
session.layouts.showTextWall(`📧 ${responseMessage}`, { durationMs: 8000 });
886+
887+
// Play audio response through glasses speakers
888+
try {
889+
await session.audio.speak(responseMessage, {
890+
voice_settings: { stability: 0.6, speed: 0.85 }
891+
});
892+
session.logger.info('Email response played via TTS');
893+
} catch (audioError) {
894+
session.logger.warn('Failed to play email response TTS:', audioError);
873895
}
874896
}
875897

@@ -879,32 +901,24 @@ class ExampleMentraOSApp extends AppServer {
879901
// Provide specific error feedback to user
880902
let errorMessage = "❌ Email request failed";
881903
if (error instanceof Error) {
882-
if (error.name === 'AbortError') {
883-
errorMessage = "⏱️ Email request timed out. Please try again.";
884-
} else if (error.message.includes('fetch')) {
885-
errorMessage = "🌐 Unable to connect to email service. Check your internet connection.";
886-
} else if (error.message.includes('404')) {
887-
errorMessage = "🔍 Email service not found. Please contact support.";
888-
} else if (error.message.includes('500')) {
889-
errorMessage = "⚙️ Email service temporarily unavailable. Try again in a moment.";
890-
} else if (error.message.includes('authorization') || error.message.includes('401')) {
891-
errorMessage = "🔒 Email authentication required. Please set up Gmail access.";
904+
if (error.message.includes('not authorized') || error.message.includes('Gmail not configured')) {
905+
errorMessage = "🔐 Gmail authorization required";
906+
} else if (error.message.includes('quota') || error.message.includes('rate limit')) {
907+
errorMessage = "⏱️ Gmail rate limit reached";
892908
}
893909
}
894910

911+
// Display error on glasses
895912
session.layouts.showTextWall(errorMessage, { durationMs: 5000 });
896913

897914
// Play error message via TTS
898915
try {
899-
// Simplify error message for audio
900916
let audioErrorMessage = "Sorry, there was an issue with your email request.";
901917
if (error instanceof Error) {
902-
if (error.name === 'AbortError') {
903-
audioErrorMessage = "Email request timed out. Please try again.";
904-
} else if (error.message.includes('fetch')) {
905-
audioErrorMessage = "Unable to connect to email service.";
906-
} else if (error.message.includes('401')) {
907-
audioErrorMessage = "Email authentication required. Please set up Gmail access.";
918+
if (error.message.includes('not authorized') || error.message.includes('Gmail not configured')) {
919+
audioErrorMessage = "Please authorize Gmail access first to use email features.";
920+
} else if (error.message.includes('quota') || error.message.includes('rate limit')) {
921+
audioErrorMessage = "Gmail rate limit reached. Please try again later.";
908922
}
909923
}
910924

@@ -1224,13 +1238,17 @@ class ExampleMentraOSApp extends AppServer {
12241238
*/
12251239
private async createGoogleCalendarEvent(userId: string, eventData: any): Promise<void> {
12261240
try {
1241+
this.logger.info(`[Calendar Auth Debug] Attempting to create event for user: ${userId}`);
1242+
12271243
if (!googleCalendarService.isConfigured()) {
12281244
this.logger.warn('Google Calendar not configured, skipping event creation');
12291245
return;
12301246
}
12311247

12321248
if (!googleCalendarService.isUserAuthorized(userId)) {
12331249
this.logger.warn(`User ${userId} not authorized with Google Calendar`);
1250+
this.logger.info(`[Calendar Auth Debug] Available authorized users: ${JSON.stringify(Array.from(googleCalendarService.getAuthorizedUsers()))}`);
1251+
this.logger.info(`[Calendar Auth Debug] Google Calendar service configured: ${googleCalendarService.isConfigured()}`);
12341252
return;
12351253
}
12361254

@@ -1262,13 +1280,17 @@ class ExampleMentraOSApp extends AppServer {
12621280
*/
12631281
private async fetchGoogleCalendarEvents(userId: string, dateRange?: any): Promise<void> {
12641282
try {
1283+
this.logger.info(`[Calendar Auth Debug] Attempting to fetch events for user: ${userId}`);
1284+
12651285
if (!googleCalendarService.isConfigured()) {
12661286
this.logger.warn('Google Calendar not configured, skipping event fetch');
12671287
return;
12681288
}
12691289

12701290
if (!googleCalendarService.isUserAuthorized(userId)) {
12711291
this.logger.warn(`User ${userId} not authorized with Google Calendar`);
1292+
this.logger.info(`[Calendar Auth Debug] Available authorized users: ${JSON.stringify(Array.from(googleCalendarService.getAuthorizedUsers()))}`);
1293+
this.logger.info(`[Calendar Auth Debug] Google Calendar service configured: ${googleCalendarService.isConfigured()}`);
12721294
return;
12731295
}
12741296

@@ -2251,14 +2273,33 @@ class ExampleMentraOSApp extends AppServer {
22512273
}
22522274

22532275
try {
2254-
// Placeholder for email integration
2255-
// In a real implementation, this would integrate with Gmail API
2256-
const emails = await this.getRecentEmailsForContext(userId);
2276+
if (!gmailService.isUserAuthorized(userId)) {
2277+
res.json({
2278+
emails: [],
2279+
unreadCount: 0,
2280+
lastSync: new Date().toISOString(),
2281+
authRequired: true,
2282+
authUrl: googleCalendarService.generateAuthUrl(userId)
2283+
});
2284+
return;
2285+
}
2286+
2287+
// Get recent emails from Gmail API
2288+
const emails = await gmailService.getRecentEmails(userId, 10);
2289+
const unreadCount = await gmailService.getUnreadCount(userId);
22572290

22582291
res.json({
2259-
emails: emails,
2260-
unreadCount: 0,
2261-
lastSync: new Date().toISOString()
2292+
emails: emails.map(email => ({
2293+
id: email.id,
2294+
subject: email.subject,
2295+
from: email.from,
2296+
snippet: email.snippet,
2297+
date: email.date,
2298+
isRead: email.isRead
2299+
})),
2300+
unreadCount: unreadCount,
2301+
lastSync: new Date().toISOString(),
2302+
authRequired: false
22622303
});
22632304
} catch (error) {
22642305
this.logger.error('Error getting emails:', error);
@@ -2347,15 +2388,44 @@ class ExampleMentraOSApp extends AppServer {
23472388
}
23482389
});
23492390

2391+
// Public Google OAuth authentication route (doesn't require prior auth)
2392+
app.get('/auth/google/start', async (req: any, res: any) => {
2393+
// Get userId from query parameter or use email as default
2394+
const userId = req.query.userId || 'ryanrong24@gmail.com';
2395+
2396+
this.logger.info(`[OAuth Start Debug] Starting Google auth for user: ${userId}`);
2397+
2398+
if (!googleCalendarService.isConfigured()) {
2399+
this.logger.error('[OAuth Start Debug] Google Calendar not configured');
2400+
res.status(500).json({ error: 'Google Calendar not configured' });
2401+
return;
2402+
}
2403+
2404+
const authUrl = googleCalendarService.generateAuthUrl(userId);
2405+
if (authUrl) {
2406+
this.logger.info(`[OAuth Start Debug] Generated auth URL for user ${userId}`);
2407+
this.logger.info(`[OAuth Start Debug] Redirecting to: ${authUrl.substring(0, 100)}...`);
2408+
res.redirect(authUrl);
2409+
} else {
2410+
this.logger.error('[OAuth Start Debug] Failed to generate auth URL');
2411+
res.status(500).json({ error: 'Failed to generate authentication URL' });
2412+
}
2413+
});
2414+
23502415
app.get('/auth/google/callback', async (req: any, res: any) => {
23512416
const { code, state: userId } = req.query;
2417+
2418+
this.logger.info(`[OAuth Callback Debug] Received callback - code: ${code ? 'present' : 'missing'}, userId: ${userId || 'missing'}`);
2419+
this.logger.info(`[OAuth Callback Debug] Full query params:`, JSON.stringify(req.query, null, 2));
23522420

23532421
if (!code || !userId) {
2422+
this.logger.error(`[OAuth Callback Debug] Missing required parameters - code: ${!!code}, userId: ${!!userId}`);
23542423
res.status(400).send('Missing authorization code or user ID');
23552424
return;
23562425
}
23572426

23582427
try {
2428+
this.logger.info(`[OAuth Callback Debug] Attempting to handle auth callback for user: ${userId}`);
23592429
const success = await googleCalendarService.handleAuthCallback(code, userId);
23602430

23612431
if (success) {

0 commit comments

Comments
 (0)