updates push notifications

This commit is contained in:
Rytek Digital Inc
2025-08-09 11:43:05 -03:00
parent 7180e9b096
commit 049a832575
5 changed files with 499 additions and 7 deletions

View File

@@ -0,0 +1,55 @@
/**
* Remove Push Notification Subscription
*
* This cloud function removes or deactivates a user's push notification subscription.
*/
Parse.Cloud.define("removePushSubscription", async (request) => {
const user = request.user;
console.log('removePushSubscription called for user:', user?.id);
// Validation
if (!user) {
throw new Parse.Error(401, 'User must be authenticated');
}
try {
// Find all active subscriptions for this user
const PushSubscription = Parse.Object.extend("PUSH_SUBSCRIPTIONS");
const query = new Parse.Query(PushSubscription);
query.equalTo("user", user);
query.equalTo("active", true);
const subscriptions = await query.find({ useMasterKey: true });
if (subscriptions.length === 0) {
return {
success: true,
message: 'No active subscriptions found',
removed: 0
};
}
// Deactivate all subscriptions for this user
const promises = subscriptions.map(subscription => {
subscription.set("active", false);
subscription.set("deactivatedAt", new Date());
return subscription.save(null, { useMasterKey: true });
});
await Promise.all(promises);
console.log(`Deactivated ${subscriptions.length} push subscriptions for user:`, user.id);
return {
success: true,
message: `Successfully deactivated ${subscriptions.length} push subscription(s)`,
removed: subscriptions.length
};
} catch (error) {
console.error('Error removing push subscription:', error);
throw new Parse.Error(500, `Failed to remove push subscription: ${error.message}`);
}
});

View File

@@ -0,0 +1,70 @@
/**
* Save Push Notification Subscription
*
* This cloud function saves a user's push notification subscription
* to enable sending web push notifications for game events.
*/
Parse.Cloud.define("savePushSubscription", async (request) => {
const { subscription, userAgent, timestamp } = request.params;
const user = request.user;
console.log('savePushSubscription called for user:', user?.id);
// Validation
if (!user) {
throw new Parse.Error(401, 'User must be authenticated');
}
if (!subscription || !subscription.endpoint || !subscription.keys) {
throw new Parse.Error(400, 'Invalid subscription data');
}
try {
// Check if subscription already exists for this user
const PushSubscription = Parse.Object.extend("PUSH_SUBSCRIPTIONS");
const query = new Parse.Query(PushSubscription);
query.equalTo("user", user);
query.equalTo("endpoint", subscription.endpoint);
let subscriptionObj = await query.first({ useMasterKey: true });
if (subscriptionObj) {
// Update existing subscription
subscriptionObj.set("keys", subscription.keys);
subscriptionObj.set("userAgent", userAgent);
subscriptionObj.set("lastUpdated", new Date());
subscriptionObj.set("active", true);
} else {
// Create new subscription
subscriptionObj = new PushSubscription();
subscriptionObj.set("user", user);
subscriptionObj.set("endpoint", subscription.endpoint);
subscriptionObj.set("keys", subscription.keys);
subscriptionObj.set("userAgent", userAgent);
subscriptionObj.set("createdAt", new Date());
subscriptionObj.set("lastUpdated", new Date());
subscriptionObj.set("active", true);
// Set ACL so only the user can read/write their subscription
const acl = new Parse.ACL(user);
acl.setPublicReadAccess(false);
acl.setPublicWriteAccess(false);
subscriptionObj.setACL(acl);
}
await subscriptionObj.save(null, { useMasterKey: true });
console.log('Push subscription saved successfully for user:', user.id);
return {
success: true,
message: 'Push subscription saved successfully',
subscriptionId: subscriptionObj.id
};
} catch (error) {
console.error('Error saving push subscription:', error);
throw new Parse.Error(500, `Failed to save push subscription: ${error.message}`);
}
});

View File

@@ -0,0 +1,319 @@
/**
* Send Push Notifications
*
* Cloud functions for sending web push notifications to users.
*/
let webpush;
try {
webpush = require('web-push');
} catch (error) {
console.error('web-push module not found. Run: npm install web-push');
webpush = null;
}
// VAPID keys configuration (in production, use environment variables)
const vapidKeys = {
publicKey: 'BAQnBrOv7KWtyZwEIm8kWElTidAYRTEIvuAc0XHvOPbEn31aD34wh3iXx4M87C9lsgMofFc1KDoBFOZZXRfdm-c',
privateKey: process.env.VAPID_PRIVATE_KEY || 's2O0GNbimWmWzEMTSEKl0kKr_FYh8mmejrF8-Hr1Qxo'
};
// Configure web-push
if (webpush) {
webpush.setVapidDetails(
'mailto:admin@dracattus.com',
vapidKeys.publicKey,
vapidKeys.privateKey
);
}
/**
* Send Test Push Notification
*/
Parse.Cloud.define("sendTestPushNotification", async (request) => {
const user = request.user;
console.log('sendTestPushNotification called for user:', user?.id);
if (!user) {
throw new Parse.Error(401, 'User must be authenticated');
}
if (!webpush) {
throw new Parse.Error(500, 'Push notification service not available. Missing web-push dependency.');
}
try {
const result = await sendPushToUser(user.id, {
title: '🐉 Dracattus Test Notification',
body: 'Web push notifications are now working! Your dragons await your return.',
icon: '/assets/icon/dracattus-icon-256.png',
badge: '/assets/icon/dracattus-icon-72.png',
data: {
type: 'test',
timestamp: new Date().toISOString(),
url: '/'
}
});
return {
success: true,
message: 'Test notification sent successfully',
details: result
};
} catch (error) {
console.error('Error sending test notification:', error);
throw new Parse.Error(500, `Failed to send test notification: ${error.message}`);
}
});
/**
* Send Trade Offer Notification
*/
Parse.Cloud.define("sendTradeOfferNotification", async (request) => {
const { userId, assetName, assetId, offerCreator } = request.params;
console.log('sendTradeOfferNotification called:', { userId, assetName, assetId });
if (!userId || !assetName || !assetId) {
throw new Parse.Error(400, 'Missing required parameters');
}
try {
const result = await sendPushToUser(userId, {
title: '💰 New Trade Offer!',
body: `Someone wants to trade for your ${assetName}!`,
icon: '/assets/icon/dracattus-icon-256.png',
badge: '/assets/icon/dracattus-icon-72.png',
data: {
type: 'trade_offer',
assetId: assetId,
assetName: assetName,
offerCreator: offerCreator,
timestamp: new Date().toISOString()
}
});
return {
success: true,
message: 'Trade offer notification sent',
details: result
};
} catch (error) {
console.error('Error sending trade offer notification:', error);
throw new Parse.Error(500, `Failed to send trade offer notification: ${error.message}`);
}
});
/**
* Send Battle Result Notification
*/
Parse.Cloud.define("sendBattleResultNotification", async (request) => {
const { userId, battleResult, opponentName } = request.params;
console.log('sendBattleResultNotification called:', { userId, battleResult, opponentName });
if (!userId || !battleResult) {
throw new Parse.Error(400, 'Missing required parameters');
}
const isWin = battleResult === 'win';
const title = isWin ? '🎉 Victory!' : '⚔️ Battle Result';
const body = isWin
? `Congratulations! You defeated ${opponentName || 'your opponent'}!`
: `Your battle against ${opponentName || 'your opponent'} has concluded.`;
try {
const result = await sendPushToUser(userId, {
title,
body,
icon: '/assets/icon/dracattus-icon-256.png',
badge: '/assets/icon/dracattus-icon-72.png',
data: {
type: 'battle_result',
result: battleResult,
opponentName: opponentName,
timestamp: new Date().toISOString()
}
});
return {
success: true,
message: 'Battle result notification sent',
details: result
};
} catch (error) {
console.error('Error sending battle result notification:', error);
throw new Parse.Error(500, `Failed to send battle result notification: ${error.message}`);
}
});
/**
* Send Breeding Complete Notification
*/
Parse.Cloud.define("sendBreedingCompleteNotification", async (request) => {
const { userId, newDracattusName, newDracattusId } = request.params;
console.log('sendBreedingCompleteNotification called:', { userId, newDracattusName, newDracattusId });
if (!userId || !newDracattusName || !newDracattusId) {
throw new Parse.Error(400, 'Missing required parameters');
}
try {
const result = await sendPushToUser(userId, {
title: '🥚 New Dragon Born!',
body: `Your breeding is complete! Welcome ${newDracattusName} to your collection.`,
icon: '/assets/icon/dracattus-icon-256.png',
badge: '/assets/icon/dracattus-icon-72.png',
data: {
type: 'breeding_complete',
newAssetId: newDracattusId,
newAssetName: newDracattusName,
timestamp: new Date().toISOString()
}
});
return {
success: true,
message: 'Breeding complete notification sent',
details: result
};
} catch (error) {
console.error('Error sending breeding complete notification:', error);
throw new Parse.Error(500, `Failed to send breeding complete notification: ${error.message}`);
}
});
/**
* Send Quest Available Notification
*/
Parse.Cloud.define("sendQuestNotification", async (request) => {
const { userId, questTitle, questDescription, questId } = request.params;
console.log('sendQuestNotification called:', { userId, questTitle, questId });
if (!userId || !questTitle) {
throw new Parse.Error(400, 'Missing required parameters');
}
try {
const result = await sendPushToUser(userId, {
title: '⚡ New Quest Available!',
body: questTitle,
icon: '/assets/icon/dracattus-icon-256.png',
badge: '/assets/icon/dracattus-icon-72.png',
data: {
type: 'new_quest',
questId: questId,
questTitle: questTitle,
questDescription: questDescription,
timestamp: new Date().toISOString()
}
});
return {
success: true,
message: 'Quest notification sent',
details: result
};
} catch (error) {
console.error('Error sending quest notification:', error);
throw new Parse.Error(500, `Failed to send quest notification: ${error.message}`);
}
});
/**
* Core function to send push notification to a specific user
*/
async function sendPushToUser(userId, notificationData) {
console.log('Sending push notification to user:', userId);
try {
// Find active push subscriptions for the user
const User = Parse.Object.extend("User");
const userQuery = new Parse.Query(User);
const user = await userQuery.get(userId, { useMasterKey: true });
if (!user) {
throw new Error('User not found');
}
const PushSubscription = Parse.Object.extend("PUSH_SUBSCRIPTIONS");
const subscriptionQuery = new Parse.Query(PushSubscription);
subscriptionQuery.equalTo("user", user);
subscriptionQuery.equalTo("active", true);
const subscriptions = await subscriptionQuery.find({ useMasterKey: true });
if (subscriptions.length === 0) {
console.log('No active push subscriptions found for user:', userId);
return {
sent: 0,
message: 'No active subscriptions'
};
}
console.log(`Found ${subscriptions.length} active subscriptions for user:`, userId);
// Send notification to all active subscriptions
const results = [];
const errors = [];
for (const subscription of subscriptions) {
try {
const pushSubscription = {
endpoint: subscription.get('endpoint'),
keys: subscription.get('keys')
};
const payload = JSON.stringify(notificationData);
await webpush.sendNotification(pushSubscription, payload);
results.push({
subscriptionId: subscription.id,
status: 'sent'
});
console.log('Push notification sent successfully to subscription:', subscription.id);
} catch (error) {
console.error('Error sending to subscription:', subscription.id, error);
errors.push({
subscriptionId: subscription.id,
error: error.message
});
// If subscription is invalid, deactivate it
if (error.statusCode === 410 || error.statusCode === 404) {
subscription.set('active', false);
subscription.set('deactivatedAt', new Date());
subscription.set('deactivationReason', 'Invalid subscription');
await subscription.save(null, { useMasterKey: true });
console.log('Deactivated invalid subscription:', subscription.id);
}
}
}
return {
sent: results.length,
failed: errors.length,
results: results,
errors: errors
};
} catch (error) {
console.error('Error in sendPushToUser:', error);
throw error;
}
}
// Export the function for use in other cloud functions
module.exports = {
sendPushToUser
};

61
package-lock.json generated
View File

@@ -32,6 +32,7 @@
"parse-server-api-mail-adapter": "^2.2.0",
"pdfkit": "^0.16.0",
"sharp": "^0.34.3",
"web-push": "^3.6.7",
"web3": "^4.1.1"
},
"devDependencies": {
@@ -3160,7 +3161,6 @@
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 14"
}
@@ -3330,6 +3330,18 @@
"safer-buffer": "~2.1.0"
}
},
"node_modules/asn1.js": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
"license": "MIT",
"dependencies": {
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"safer-buffer": "^2.1.0"
}
},
"node_modules/assert-options": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/assert-options/-/assert-options-0.8.1.tgz",
@@ -3618,6 +3630,12 @@
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
"license": "MIT"
},
"node_modules/bn.js": {
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
@@ -6822,6 +6840,15 @@
"license": "MIT",
"optional": true
},
"node_modules/http_ece": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz",
"integrity": "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==",
"license": "MIT",
"engines": {
"node": ">=16"
}
},
"node_modules/http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
@@ -6942,7 +6969,6 @@
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"license": "MIT",
"optional": true,
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
@@ -6956,7 +6982,6 @@
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"optional": true,
"dependencies": {
"ms": "^2.1.3"
},
@@ -6973,8 +6998,7 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/humanize-ms": {
"version": "1.2.1",
@@ -7678,7 +7702,6 @@
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
"integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
"license": "MIT",
"optional": true,
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
@@ -7730,7 +7753,6 @@
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
"license": "MIT",
"optional": true,
"dependencies": {
"jwa": "^2.0.0",
"safe-buffer": "^5.0.1"
@@ -8185,6 +8207,12 @@
"node": ">=4"
}
},
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
"license": "ISC"
},
"node_modules/minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
@@ -12830,6 +12858,25 @@
"defaults": "^1.0.3"
}
},
"node_modules/web-push": {
"version": "3.6.7",
"resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz",
"integrity": "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==",
"license": "MPL-2.0",
"dependencies": {
"asn1.js": "^5.3.0",
"http_ece": "1.2.0",
"https-proxy-agent": "^7.0.0",
"jws": "^4.0.0",
"minimist": "^1.2.5"
},
"bin": {
"web-push": "src/cli.js"
},
"engines": {
"node": ">= 16"
}
},
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",

View File

@@ -44,6 +44,7 @@
"parse-server-api-mail-adapter": "^2.2.0",
"pdfkit": "^0.16.0",
"sharp": "^0.34.3",
"web-push": "^3.6.7",
"web3": "^4.1.1"
},
"devDependencies": {