Skip to content

Commit 400fbfb

Browse files
committed
Updated Firebase packages and fixed notifications functionality
1 parent 2f14128 commit 400fbfb

4 files changed

Lines changed: 4381 additions & 411 deletions

File tree

cloud-functions/functions/index.js

Lines changed: 199 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -14,119 +14,218 @@
1414
* limitations under the License.
1515
*/
1616

17-
// Import the Firebase SDK for Google Cloud Functions.
18-
const functions = require('firebase-functions');
19-
// Import and initialize the Firebase Admin SDK.
20-
const admin = require('firebase-admin');
21-
admin.initializeApp();
22-
const Vision = require('@google-cloud/vision');
23-
const vision = new Vision.ImageAnnotatorClient();
24-
const {promisify} = require('util');
25-
const exec = promisify(require('child_process').exec);
26-
const path = require('path');
27-
const os = require('os');
28-
const fs = require('fs');
29-
30-
// Adds a message that welcomes new users into the chat.
31-
exports.addWelcomeMessages = functions.auth.user().onCreate(async (user) => {
32-
functions.logger.log('A new user signed in for the first time.');
33-
const fullName = user.displayName || 'Anonymous';
34-
35-
// Saves the new welcome message into the database
36-
// which then displays it in the FriendlyChat clients.
37-
await admin.firestore().collection('messages').add({
38-
name: 'Firebase Bot',
39-
profilePicUrl: '/images/firebase-logo.png', // Firebase logo
40-
text: `${fullName} signed in for the first time! Welcome!`,
41-
timestamp: admin.firestore.FieldValue.serverTimestamp(),
42-
});
43-
functions.logger.log('Welcome message written to database.');
17+
import { auth, logger, storage, firestore } from "firebase-functions/v1"; // Firebase Functions
18+
import { initializeApp } from "firebase-admin/app"; // App Initialization
19+
import { getFirestore, FieldValue } from "firebase-admin/firestore"; // Firestore
20+
import { getStorage } from "firebase-admin/storage"; // Firebase Cloud Storage
21+
import { getMessaging } from "firebase-admin/messaging";
22+
import {
23+
ImageAnnotatorClient,
24+
protos as visionProtos,
25+
} from "@google-cloud/vision"; // Google Vision API
26+
import { promisify } from "util"; // Node.js Utility for promisifying functions
27+
import { exec as childExec } from "child_process"; // For running shell commands
28+
import path from "path"; // For handling file paths
29+
import os from "os"; // For working with temporary OS files
30+
import fs from "fs"; // File system module
31+
32+
// Initialize Firebase Admin SDK
33+
initializeApp();
34+
35+
const db = getFirestore();
36+
const messaging = getMessaging();
37+
38+
// Initialize Google Vision API Client
39+
const vision = new ImageAnnotatorClient();
40+
const { Likelihood } = visionProtos.google.cloud.vision.v1; // Helper for likelihood comparison
41+
42+
// Promisify exec for async use
43+
const exec = promisify(childExec);
44+
45+
// ----------------------------
46+
// 1. Welcome Message Function
47+
// ----------------------------
48+
export const addWelcomeMessages = auth.user().onCreate(async (user) => {
49+
logger.log("A new user signed in for the first time.");
50+
const fullName = user.displayName || "Anonymous";
51+
52+
try {
53+
// Add a welcome message to the "messages" collection
54+
await db.collection("messages").add({
55+
name: "Firebase Bot",
56+
profilePicUrl: "/images/firebase-logo.png",
57+
text: `${fullName} signed in for the first time! Welcome!`,
58+
timestamp: FieldValue.serverTimestamp(),
59+
});
60+
logger.log("Welcome message written to Firestore successfully.");
61+
} catch (error) {
62+
logger.error("Error writing welcome message to Firestore:", error);
63+
}
4464
});
4565

46-
// Checks if uploaded images are flagged as Adult or Violence and if so blurs them.
47-
exports.blurOffensiveImages = functions.runWith({memory: '2GB'}).storage.object().onFinalize(
48-
async (object) => {
49-
const imageUri = `gs://${object.bucket}/${object.name}`;
50-
51-
// Check the image content using the Cloud Vision API.
52-
const batchAnnotateImagesResponse = await vision.safeSearchDetection(imageUri);
53-
const safeSearchResult = batchAnnotateImagesResponse[0].safeSearchAnnotation;
54-
const Likelihood = Vision.protos.google.cloud.vision.v1.Likelihood;
55-
if (Likelihood[safeSearchResult.adult] >= Likelihood.LIKELY ||
56-
Likelihood[safeSearchResult.violence] >= Likelihood.LIKELY) {
57-
functions.logger.log('The image', object.name, 'has been detected as inappropriate.');
58-
return blurImage(object.name);
66+
// ------------------------------------------------
67+
// 2. Blur Offensive Images Function (Storage API)
68+
// ------------------------------------------------
69+
export const blurOffensiveImages = storage
70+
.object()
71+
.onFinalize(async (object) => {
72+
const fileURI = `gs://${object.bucket}/${object.name}`; // Google Cloud Storage URI
73+
74+
logger.log(`Analyzing image: ${fileURI}`);
75+
76+
try {
77+
// Run Vision API's SafeSearch Detection to check for inappropriate content
78+
const [result] = await vision.safeSearchDetection(fileURI);
79+
const safeSearchAnnotation = result.safeSearchAnnotation;
80+
81+
// Check likelihood of adult or violent content
82+
if (
83+
Likelihood[safeSearchAnnotation?.adult] >= Likelihood.LIKELY ||
84+
Likelihood[safeSearchAnnotation?.violence] >= Likelihood.LIKELY
85+
) {
86+
logger.log(
87+
`The image "${object.name}" has been marked as inappropriate.`
88+
);
89+
return blurImage(object.name, object.bucket);
5990
}
60-
functions.logger.log('The image', object.name, 'has been detected as OK.');
61-
});
6291

63-
// Blurs the given image located in the given bucket using ImageMagick.
64-
async function blurImage(filePath) {
65-
const tempLocalFile = path.join(os.tmpdir(), path.basename(filePath));
66-
const messageId = filePath.split(path.sep)[1];
67-
const bucket = admin.storage().bucket();
68-
69-
// Download file from bucket.
70-
await bucket.file(filePath).download({destination: tempLocalFile});
71-
functions.logger.log('Image has been downloaded to', tempLocalFile);
72-
// Blur the image using ImageMagick.
73-
await exec(`convert "${tempLocalFile}" -channel RGBA -blur 0x24 "${tempLocalFile}"`);
74-
functions.logger.log('Image has been blurred');
75-
// Uploading the Blurred image back into the bucket.
76-
await bucket.upload(tempLocalFile, {destination: filePath});
77-
functions.logger.log('Blurred image has been uploaded to', filePath);
78-
// Deleting the local file to free up disk space.
79-
fs.unlinkSync(tempLocalFile);
80-
functions.logger.log('Deleted local file.');
81-
// Indicate that the message has been moderated.
82-
await admin.firestore().collection('messages').doc(messageId).update({moderated: true});
83-
functions.logger.log('Marked the image as moderated in the database.');
92+
logger.log(`The image "${object.name}" is safe.`);
93+
return null;
94+
} catch (error) {
95+
logger.error(
96+
`Error analyzing the image "${object.name}": ${error.message}`
97+
);
98+
return null;
99+
}
100+
});
101+
102+
// ------------------
103+
// Helper: Blur Image
104+
// ------------------
105+
async function blurImage(filePath, bucketName) {
106+
const tempLocalFile = path.join(os.tmpdir(), path.basename(filePath)); // Create temp file path
107+
const bucket = getStorage().bucket(bucketName); // Get bucket reference
108+
const messageId = filePath.split("/")[1]; // Derive message ID (assuming structure like "messages/{messageId}/{fileName}")
109+
110+
try {
111+
// Step 1: Download the file from Firebase Storage
112+
await bucket.file(filePath).download({ destination: tempLocalFile });
113+
logger.log(`Image downloaded locally to: "${tempLocalFile}".`);
114+
115+
// Step 2: Blur the image using ImageMagick
116+
await exec(
117+
`convert "${tempLocalFile}" -channel RGBA -blur 0x24 "${tempLocalFile}"`
118+
);
119+
logger.log(`Image blurred locally: "${tempLocalFile}".`);
120+
121+
// Step 3: Upload the blurred image back to Firebase Storage
122+
await bucket.upload(tempLocalFile, { destination: filePath });
123+
logger.log(`Blurred image re-uploaded to bucket at path: "${filePath}".`);
124+
125+
// Step 4: Mark the image as moderated in Firestore
126+
if (messageId) {
127+
await db
128+
.collection("messages")
129+
.doc(messageId)
130+
.update({ moderated: true });
131+
logger.log(`Marked the image "${filePath}" as moderated in Firestore.`);
132+
} else {
133+
logger.warn(
134+
`Could not derive a valid message ID from filePath: "${filePath}". Skipping Firestore update.`
135+
);
136+
}
137+
} catch (error) {
138+
logger.error(`Error in blurring image "${filePath}":`, error);
139+
} finally {
140+
// Step 5: Delete the local temporary file
141+
if (fs.existsSync(tempLocalFile)) {
142+
fs.unlinkSync(tempLocalFile);
143+
logger.log("Temporary local file deleted.");
144+
}
145+
}
84146
}
85147

86-
// Sends a notifications to all users when a new message is posted.
87-
exports.sendNotifications = functions.firestore.document('messages/{messageId}').onCreate(
88-
async (snapshot) => {
89-
// Notification details.
90-
const text = snapshot.data().text;
91-
const payload = {
92-
notification: {
93-
title: `${snapshot.data().name} posted ${text ? 'a message' : 'an image'}`,
94-
body: text ? (text.length <= 100 ? text : text.substring(0, 97) + '...') : '',
95-
icon: snapshot.data().profilePicUrl || '/images/profile_placeholder.png',
96-
click_action: `https://${process.env.GCLOUD_PROJECT}.firebaseapp.com`,
97-
}
98-
};
148+
// ---------------------------------------------------
149+
// 3. Send Notifications when New Firestore Data Added
150+
// ---------------------------------------------------
151+
export const sendNotifications = firestore
152+
.document("messages/{messageId}")
153+
.onCreate(async (snapshot) => {
154+
const messageData = snapshot.data();
155+
const text = messageData.text;
99156

100-
// Get the list of device tokens.
101-
const allTokens = await admin.firestore().collection('fcmTokens').get();
102-
const tokens = [];
103-
allTokens.forEach((tokenDoc) => {
104-
tokens.push(tokenDoc.id);
105-
});
157+
logger.log("New message detected:", messageData);
158+
159+
try {
160+
// Fetch all available FCM tokens from the "fcmTokens" collection
161+
const allTokensSnapshot = await db.collection("fcmTokens").get();
162+
163+
const tokens = [];
164+
allTokensSnapshot.forEach((tokenDoc) => {
165+
const token = tokenDoc.data().token;
166+
if (token) {
167+
tokens.push(token);
168+
}
169+
});
106170

107-
if (tokens.length > 0) {
108-
// Send notifications to all tokens.
109-
const response = await admin.messaging().sendToDevice(tokens, payload);
110-
await cleanupTokens(response, tokens);
111-
functions.logger.log('Notifications have been sent and tokens cleaned up.');
171+
logger.log("Fetched FCM tokens:", tokens);
172+
173+
if (tokens.length > 0) {
174+
const responses = await Promise.all(
175+
tokens.map((token) =>
176+
messaging.send({
177+
token: token,
178+
notification: {
179+
title: `${messageData.name} posted ${
180+
text ? "a message" : "an image"
181+
}`,
182+
body: text
183+
? text.length <= 100
184+
? text
185+
: text.substring(0, 97) + "..."
186+
: "",
187+
imageUrl:
188+
messageData.profilePicUrl ||
189+
"/images/profile_placeholder.png",
190+
},
191+
})
192+
)
193+
);
194+
195+
logger.log("Send Responses:", responses);
196+
197+
await cleanupTokens(responses, tokens);
198+
}
199+
} catch (error) {
200+
logger.error("Error sending notifications:", error);
112201
}
113202
});
114203

115-
// Cleans up the tokens that are no longer valid.
116-
function cleanupTokens(response, tokens) {
117-
// For each notification we check if there was an error.
204+
// -------------------
205+
// Helper: Cleanup Tokens
206+
// -------------------
207+
const cleanupTokens = async (responses, tokens) => {
118208
const tokensDelete = [];
119-
response.results.forEach((result, index) => {
120-
const error = result.error;
209+
210+
responses.forEach((res, index) => {
211+
const error = res.error;
121212
if (error) {
122-
functions.logger.error('Failure sending notification to', tokens[index], error);
123-
// Cleanup the tokens who are not registered anymore.
124-
if (error.code === 'messaging/invalid-registration-token' ||
125-
error.code === 'messaging/registration-token-not-registered') {
126-
const deleteTask = admin.firestore().collection('fcmTokens').doc(tokens[index]).delete();
127-
tokensDelete.push(deleteTask);
213+
logger.error(
214+
"Failure sending notification to token:",
215+
tokens[index],
216+
error
217+
);
218+
219+
// Remove invalid or unregistered tokens
220+
if (
221+
error.code === "messaging/invalid-registration-token" ||
222+
error.code === "messaging/registration-token-not-registered"
223+
) {
224+
const tokenDoc = db.collection("fcmTokens").doc(tokens[index]);
225+
tokensDelete.push(tokenDoc.delete());
128226
}
129227
}
130228
});
131-
return Promise.all(tokensDelete);
132-
}
229+
230+
return Promise.all(tokensDelete);
231+
};

0 commit comments

Comments
 (0)