diff --git a/client/Images/Aria.jpg b/client/Images/Aria.jpg new file mode 100644 index 00000000..3ecf98e5 Binary files /dev/null and b/client/Images/Aria.jpg differ diff --git a/client/service-worker.js b/client/service-worker.js new file mode 100644 index 00000000..73d96009 --- /dev/null +++ b/client/service-worker.js @@ -0,0 +1,32 @@ +self.addEventListener('push', event => { + try { + const data = event.data.json(); // Tries to parse JSON data + self.registration.showNotification(data.title, { + body: data.body, + icon: './Images/Aria.jpg' + }); +} catch (error) { + console.error('Error parsing push notification data:', error); + self.registration.showNotification('Notification', { + body: event.data.text(), + icon: './Images/Aria.jpg' + }); +} +}); + + self.addEventListener('notificationclick', event => { + event.notification.close(); + event.waitUntil( + self.clients.matchAll({type: 'window'}).then(clientList => { + for (let client of clientList) { + if (client.url === '/' && 'focus' in client) { + return client.focus(); + } + } + if (self.clients.openWindow) { + return self.clients.openWindow('/'); + } + }) + ); + }); + \ No newline at end of file diff --git a/client/src/main.jsx b/client/src/main.jsx index 87d1310e..d6342139 100644 --- a/client/src/main.jsx +++ b/client/src/main.jsx @@ -4,6 +4,99 @@ import App from './App.jsx' import { BrowserRouter } from 'react-router-dom'; import { UserProvider } from './Components/userContext'; +function urlBase64ToUint8Array(base64String) { + const padding = '='.repeat((4 - base64String.length % 4) % 4); + const base64 = (base64String + padding) + .replace(/-/g, '+') + .replace(/_/g, '/'); + + const rawData = window.atob(base64); + const outputArray = new Uint8Array(rawData.length); + + for (let i = 0; i < rawData.length; ++i) { + outputArray[i] = rawData.charCodeAt(i); + } + + return outputArray; +} + +function getToken() { + // Implement a function to retrieve the JWT token + return localStorage.getItem('token'); +} + +const VAPID_PUBLIC_KEY = "BJO2lvL7cXXdg0MqKdCtQyWOz3Nb1Ny-X8x_67MKdRtQOLLl3FRpAPJOUJEzjaQGNBcIOqwjeS165Rb3Pl0x2ZI"; + +if ('serviceWorker' in navigator) { + window.addEventListener('load', function() { + navigator.serviceWorker.register('../service-worker.js') + .then(function(registration) { + console.log('Service Worker registered with scope:', registration.scope); + + // Request notification permission + return Notification.requestPermission().then(permission => { + if (permission !== 'granted') { + throw new Error('Permission not granted for Notification'); + } + + // Check for permission and subscribe for push notifications + return registration.pushManager.getSubscription(); + }).then(function(subscription) { + if (!subscription) { + return registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY) + }); + } + return subscription; + + }) + .then(function(subscription) { + console.log('Subscription:', subscription); + + // Ensure the keys are properly encoded + const keys = { + p256dh: btoa(String.fromCharCode.apply(null, new Uint8Array(subscription.getKey('p256dh')))), + auth: btoa(String.fromCharCode.apply(null, new Uint8Array(subscription.getKey('auth')))) + }; + console.log('Subscription keys:', keys); + + if (!keys.p256dh || !keys.auth) { + console.error('Subscription object:', subscription); + throw new Error('Subscription keys are missing'); + } + const subscriptionData = { + endpoint: subscription.endpoint, + keys: keys + }; + + const token = getToken(); + + if (!token) { + throw new Error('No token found'); + } + + return fetch('/api/subscribe', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify(subscriptionData), + }); + }) + .then(response => response.json()) + .then(data => console.log('Subscription response:', data)) + .catch(err => console.error('Subscription failed:', err)); + + + }) + .catch(function(err) { + console.error('Service Worker registration failed:', err); + }); + }); +} + ReactDOM.createRoot(document.getElementById('root')).render( diff --git a/server/app.py b/server/app.py index 34053b6a..3f669d64 100644 --- a/server/app.py +++ b/server/app.py @@ -2,32 +2,85 @@ API entrypoint for backend API. """ import os -from flask import Flask +from flask import Flask, request, jsonify from flask_cors import CORS -from flask_jwt_extended import JWTManager +from flask_jwt_extended import JWTManager, jwt_required , get_jwt_identity +from models.subscription import Subscription,db -from routes.scheduler import init_scheduler from routes.user import user_routes from routes.ai import ai_routes from routes.checkIn import checkIn_routes from services.azure_mongodb import MongoDBClient - +from routes.scheduler import send_push_notification from agents.mental_health_agent import MentalHealthAIAgent # Set up the app app = Flask(__name__) app.config['JWT_SECRET_KEY'] = os.environ.get("JWT_SECRET_KEY") +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///mydatabase.db' jwt = JWTManager(app) CORS(app) +db.init_app(app) + + +# Create the tables +with app.app_context(): + db.create_all() + +@app.route('/subscribe', methods=['POST']) +@jwt_required() +def subscribe(): + data = request.json + print(f"Received subscription data: {data}") + + if not data or 'endpoint' not in data or 'keys' not in data or 'p256dh' not in data['keys'] or 'auth' not in data['keys']: + return jsonify({'error': 'Missing required fields'}), 400 + + + subscription_info = { + 'endpoint': data['endpoint'], + 'keys': { + 'p256dh': data['keys']['p256dh'], + 'auth': data['keys']['auth'] + } + } + + user_id = get_jwt_identity() + + # Check if the subscription already exists + existing_subscription = Subscription.query.filter_by(user_id=user_id).first() + if existing_subscription: + # Update existing subscription + existing_subscription.subscription_info = subscription_info + else: + # Create new subscription + new_subscription = Subscription(user_id=user_id, subscription_info=subscription_info) + db.session.add(new_subscription) + + db.session.commit() + + return jsonify({'message': 'Subscription saved successfully'}), 200 + +@app.route('/send_push', methods=['POST']) +@jwt_required() +def send_push(): + data = request.json + user_id = data['user_id'] + message = data['message'] + success = send_push_notification(user_id, message) + if success: + return jsonify({'message': 'Push notification sent successfully'}), 200 + else: + return jsonify({'error': 'Failed to send push notification'}), 500 + + # Register routes app.register_blueprint(user_routes) app.register_blueprint(ai_routes) app.register_blueprint(checkIn_routes) -# Initialize the scheduler -init_scheduler(app) # DB pre-load MentalHealthAIAgent.load_agent_facts_to_db() diff --git a/server/instance/mydatabase.db b/server/instance/mydatabase.db new file mode 100644 index 00000000..d8f11f23 Binary files /dev/null and b/server/instance/mydatabase.db differ diff --git a/server/models/subscription.py b/server/models/subscription.py new file mode 100644 index 00000000..59985731 --- /dev/null +++ b/server/models/subscription.py @@ -0,0 +1,8 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() + +class Subscription(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.String(120), unique=True, nullable=False) + subscription_info = db.Column(db.String, nullable=False) diff --git a/server/routes/scheduler.py b/server/routes/scheduler.py index 6d1e1652..ea1a2bba 100644 --- a/server/routes/scheduler.py +++ b/server/routes/scheduler.py @@ -1,39 +1,30 @@ -from apscheduler.schedulers.background import BackgroundScheduler from flask import current_app as app -from datetime import datetime, timedelta -from services.azure_mongodb import MongoDBClient -import pymongo +from models.subscription import db, Subscription +from pywebpush import webpush, WebPushException +import json +import os +from dotenv import load_dotenv +load_dotenv() -def notify_check_ins(): - db_client = MongoDBClient.get_client() - db = db_client[MongoDBClient.get_db_name()] - now = datetime.now() - upcoming_check_ins = db.check_ins.find({ - 'check_in_time': {'$gte': now, '$lt': now + timedelta(days=7)}, - 'notify': True, - 'status': 'upcoming' - }) - - for check_in in upcoming_check_ins: - delta = check_in['check_in_time'] - now - if delta.days == 7 or delta.days == 1 or delta.total_seconds() / 3600 <= 1: - send_notification(check_in['user_id'], check_in['check_in_time'], delta) - -def send_notification(user_id, check_in_time, delta): - message = "" - if delta.days == 7: - message = "Your check-in is scheduled in 1 week." - elif delta.days == 1: - message = "Your check-in is scheduled tomorrow." - elif delta.total_seconds() / 3600 <= 1: - message = "Your check-in is in less than 1 hour." - - # This is where you'd integrate your actual notification logic - print(f"Notify {user_id}: {message}") - -scheduler = BackgroundScheduler() -scheduler.add_job(func=notify_check_ins, trigger='interval', hours=1) -scheduler.start() - -def init_scheduler(app): - app.config['scheduler'] = scheduler +def send_push_notification(user_id, message): + subscription = Subscription.query.filter_by(user_id=user_id).first() + if not subscription: + print(f"No subscription found for user {user_id}") + return False + + try: + webpush( + subscription_info=json.loads(subscription.subscription_info), + data=json.dumps({ + "title": "Notification Title", + "body": message + }), + vapid_private_key=os.environ.get("VAPID_PRIVATE_KEY"), + vapid_claims={"sub": "mailto:dpatel24@radar.gsw.edu"} + ) + print("Notification sent successfully") + return True + except WebPushException as e: + print(f"Failed to send notification: {e}") + if e.response and e.response.json(): + print(e.response.json())