diff --git a/src/api/middlewares/ErrorHandler.ts b/src/api/middlewares/ErrorHandler.ts index 25434ad..eb56e7a 100644 --- a/src/api/middlewares/ErrorHandler.ts +++ b/src/api/middlewares/ErrorHandler.ts @@ -31,7 +31,7 @@ function handleError( error: message, httpCode: httpCode, }; - console.log(message); + console.error(`[${httpCode}] ${message}`); response.status(httpCode).json(errorResponse); next(); } diff --git a/src/app.ts b/src/app.ts index da89d9f..716ce7f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -61,7 +61,6 @@ async function main() { controllers: controllers, middlewares: middlewares, currentUserChecker: async (action: any) => { - console.log("AUTH MIDDLEWARE CALLED for path:", action.request.path); const authHeader = action.request.headers["authorization"]; if (!authHeader) { throw new ForbiddenError("No authorization token provided"); @@ -77,9 +76,7 @@ async function main() { const email = decodedToken.email; const userId = decodedToken.uid; action.request.email = email; - console.log("uid"); action.request.firebaseUid = userId; - console.log("here"); if (!email || !email.endsWith("@cornell.edu")) { throw new ForbiddenError("Only Cornell email addresses are allowed"); } diff --git a/src/services/NotifService.ts b/src/services/NotifService.ts index efd8b28..df80ad4 100644 --- a/src/services/NotifService.ts +++ b/src/services/NotifService.ts @@ -1,17 +1,21 @@ -import { NotFoundError } from "routing-controllers"; -import { Service } from "typedi"; -import { - NotificationData, - FindTokensRequest, - DiscountNotificationRequest, - RequestMatchNotificationRequest, - TokenWrapper, -} from "../types"; -import Repositories, { TransactionsManager } from "../repositories"; -import { EntityManager } from "typeorm"; -import { InjectManager } from "typeorm-typedi-extensions"; -import { getMessaging, Message } from "firebase-admin/messaging"; -import { admin } from "../app"; +import { NotFoundError } from 'routing-controllers'; +import { Service } from 'typedi'; +import { NotificationData, FindTokensRequest, DiscountNotificationRequest, RequestMatchNotificationRequest } from '../types'; +import Repositories, { TransactionsManager } from '../repositories'; +import { EntityManager } from 'typeorm'; +import { InjectManager } from 'typeorm-typedi-extensions'; +import { getMessaging, Message } from 'firebase-admin/messaging'; + +interface NotifPayload { + title: string; + body: string; + data: Record; +} + +interface NotifResult { + message: string; + httpCode: number; +} @Service() export class NotifService { @@ -21,149 +25,120 @@ export class NotifService { this.transactions = new TransactionsManager(entityManager); } - public sendFCMNotifs = async (notifs: NotificationData[], userId: string) => { - return this.transactions.readWrite(async (transactionalEntityManager) => { - const notifRepository = Repositories.notification( - transactionalEntityManager, - ); - console.log(notifs); - for (const notif of notifs) { - const msg: Message = { - notification: { - title: notif.title, - body: notif.body, - }, - data: notif.data as unknown as { [key: string]: string }, - token: notif.to[0], - }; - try { - // Send FCM notification - const response = await getMessaging().send(msg); - console.log(`Notification sent successfully: ${response}`); - - // Save notification to database - await notifRepository.save({ - userId: userId, - title: notif.title, - body: notif.body, - data: notif.data, - read: false, - }); - } catch (error) { - console.warn( - `Skipping invalid token for notification "${notif.title}": ${error.message}`, - ); - // Do NOT throw, just skip - continue; + /** + * fetch all FCM tokens for a user + */ + private async getTokensForUser( + fcmTokenRepository: ReturnType, + userId: string + ): Promise { + const tokenRecords = await fcmTokenRepository.getTokensByUserId(userId); + return tokenRecords.map(t => t.token); + } + + /** + * send notification to a user and persist it, skips invalid tokens + */ + private async sendToUser( + userId: string, + tokens: string[], + payload: NotifPayload, + notifRepository: ReturnType + ): Promise { + for (const token of tokens) { + const msg: Message = { + notification: { + title: payload.title, + body: payload.body, + }, + data: payload.data as { [key: string]: string }, + token: token, + }; + try { + await getMessaging().send(msg); + await notifRepository.save({ + userId: userId, + title: payload.title, + body: payload.body, + data: payload.data as unknown as JSON, + read: false + }); + } catch (error) { + console.warn(`Skipping invalid token for notification "${payload.title}": ${error.message}`); + continue; + } + } + } + + /** + * send a notification to a user by userId + * gets tokens, sends, persists, and returns result + */ + private async sendNotificationToUser( + transactionalEntityManager: EntityManager, + userId: string, + payload: NotifPayload + ): Promise { + const fcmTokenRepository = Repositories.fcmToken(transactionalEntityManager); + const notifRepository = Repositories.notification(transactionalEntityManager); + + const tokens = await this.getTokensForUser(fcmTokenRepository, userId); + if (tokens.length === 0) { + return { message: "No device tokens found", httpCode: 200 }; } - } - }); - }; - - public async sendNotifs(request: FindTokensRequest) { - return this.transactions.readWrite(async (transactionalEntityManager) => { - const userRepository = Repositories.user(transactionalEntityManager); - const fcmTokenRepository = Repositories.fcmToken( - transactionalEntityManager, - ); - const user = await userRepository.getUserByEmail(request.email); - if (!user) { - throw new NotFoundError("User not found!"); - } - const allDeviceTokens = []; - const alltokens = await fcmTokenRepository.getTokensByUserId( - user.firebaseUid, - ); - for (const token of alltokens) { - allDeviceTokens.push(token); - } - console.log(allDeviceTokens); - const notif: NotificationData = { - to: allDeviceTokens.map((tokenObj) => tokenObj.token), - sound: "default", - title: request.title, - body: request.body, - data: request.data as unknown as JSON, - }; - try { - const notifs: NotificationData[] = []; - notif.to.forEach((token) => { - notifs.push({ - to: [token], - sound: notif.sound, - title: notif.title, - body: notif.body, - data: notif.data, - }); + try { + await this.sendToUser(userId, tokens, payload, notifRepository); + return { message: "Notification sent successfully", httpCode: 200 }; + } catch (err) { + console.error('Failed to send notification:', err); + return { message: "Notification not sent", httpCode: 500 }; + } + } + + public async sendNotifs(request: FindTokensRequest): Promise { + return this.transactions.readWrite(async (transactionalEntityManager) => { + const userRepository = Repositories.user(transactionalEntityManager); + const user = await userRepository.getUserByEmail(request.email); + if (!user) { + throw new NotFoundError("User not found!"); + } + + return this.sendNotificationToUser(transactionalEntityManager, user.firebaseUid, { + title: request.title, + body: request.body, + data: request.data as Record + }); }); - await this.sendFCMNotifs(notifs, user.firebaseUid); - return { - message: "Notification sent successfully", - httpCode: 200, - }; - } catch (err) { - console.log(err); - return { - message: "Notification not sent", - httpCode: 500, - }; - } - }); - } - - public async sendDiscountNotification(request: DiscountNotificationRequest) { - return this.transactions.readWrite(async (transactionalEntityManager) => { - const userRepository = Repositories.user(transactionalEntityManager); - const fcmTokenRepository = Repositories.fcmToken( - transactionalEntityManager, - ); - const postRepository = Repositories.post(transactionalEntityManager); - - const user = await userRepository.getUserById(request.sellerId); - if (!user) { - throw new NotFoundError("User not found!"); - } - - const post = await postRepository.getPostById(request.listingId); - if (!post) { - throw new NotFoundError("Post not found!"); - } - - const allDeviceTokens = []; - const allTokens = await fcmTokenRepository.getTokensByUserId( - user.firebaseUid, - ); - for (const token of allTokens) { - allDeviceTokens.push(token); - } - console.log(allDeviceTokens); - - const notif: NotificationData = { - to: allDeviceTokens.map((tokenObj) => tokenObj.token), - sound: "default", - title: "Price Drop Alert!", - body: `${post.title} is now available at $${request.newPrice}!`, - data: { - postId: post.id, - postTitle: post.title, - originalPrice: request.oldPrice, - newPrice: request.newPrice, - sellerId: post.user.firebaseUid, - sellerUsername: post.user.username, - } as unknown as JSON, - }; - - try { - const notifs: NotificationData[] = []; - notif.to.forEach((token) => { - notifs.push({ - to: [token], - sound: notif.sound, - title: notif.title, - body: notif.body, - data: notif.data, - }); + } + + public async sendDiscountNotification(request: DiscountNotificationRequest): Promise { + return this.transactions.readWrite(async (transactionalEntityManager) => { + const userRepository = Repositories.user(transactionalEntityManager); + const postRepository = Repositories.post(transactionalEntityManager); + + const user = await userRepository.getUserById(request.sellerId); + if (!user) { + throw new NotFoundError("User not found!"); + } + + const post = await postRepository.getPostById(request.listingId); + if (!post) { + throw new NotFoundError("Post not found!"); + } + + return this.sendNotificationToUser(transactionalEntityManager, user.firebaseUid, { + title: "Price Drop Alert!", + body: `${post.title} is now available at $${request.newPrice}!`, + data: { + postId: post.id, + postTitle: post.title, + originalPrice: String(request.oldPrice), + newPrice: String(request.newPrice), + sellerId: post.user.firebaseUid, + sellerUsername: post.user.username + } + }); }); await this.sendFCMNotifs(notifs, user.firebaseUid); return { @@ -180,71 +155,42 @@ export class NotifService { }); } - public async sendRequestMatchNotification( - request: RequestMatchNotificationRequest, - ) { - return this.transactions.readWrite(async (transactionalEntityManager) => { - const userRepository = Repositories.user(transactionalEntityManager); - const fcmTokenRepository = Repositories.fcmToken( - transactionalEntityManager, - ); - const postRepository = Repositories.post(transactionalEntityManager); - const requestRepository = Repositories.request( - transactionalEntityManager, - ); - - const user = await userRepository.getUserById(request.userId); - if (!user) { - throw new NotFoundError("User not found!"); - } - - const post = await postRepository.getPostById(request.listingId); - if (!post) { - throw new NotFoundError("Post not found!"); - } - - const userRequest = await requestRepository.getRequestById( - request.requestId, - ); - if (!userRequest) { - throw new NotFoundError("Request not found!"); - } - - const allDeviceTokens = []; - const allTokens = await fcmTokenRepository.getTokensByUserId( - user.firebaseUid, - ); - for (const token of allTokens) { - allDeviceTokens.push(token); - } - - const notif: NotificationData = { - to: allDeviceTokens.map((tokenObj) => tokenObj.token), - sound: "default", - title: "Request Match Found!", - body: `We found a match for your request: ${post.title}`, - data: { - postId: post.id, - postTitle: post.title, - price: - post.altered_price > 0 ? post.altered_price : post.original_price, - requestId: userRequest.id, - requestTitle: userRequest.title, - sellerId: post.user.firebaseUid, - sellerUsername: post.user.username, - } as unknown as JSON, - }; - - try { - const notifs: NotificationData[] = []; - notif.to.forEach((token) => { - notifs.push({ - to: [token], - sound: notif.sound, - title: notif.title, - body: notif.body, - data: notif.data, - }); + public async sendRequestMatchNotification(request: RequestMatchNotificationRequest): Promise { + return this.transactions.readWrite(async (transactionalEntityManager) => { + const userRepository = Repositories.user(transactionalEntityManager); + const postRepository = Repositories.post(transactionalEntityManager); + const requestRepository = Repositories.request(transactionalEntityManager); + + const user = await userRepository.getUserById(request.userId); + if (!user) { + throw new NotFoundError("User not found!"); + } + + const post = await postRepository.getPostById(request.listingId); + if (!post) { + throw new NotFoundError("Post not found!"); + } + + const userRequest = await requestRepository.getRequestById(request.requestId); + if (!userRequest) { + throw new NotFoundError("Request not found!"); + } + + const price = post.altered_price > 0 ? post.altered_price : post.original_price; + + return this.sendNotificationToUser(transactionalEntityManager, request.userId, { + title: "Request Match Found!", + body: `We found a match for your request: ${post.title}`, + data: { + postId: post.id, + postTitle: post.title, + price: String(price), + requestId: userRequest.id, + requestTitle: userRequest.title, + sellerId: post.user.firebaseUid, + sellerUsername: post.user.username + } + }); }); await this.sendFCMNotifs(notifs, request.userId); return { diff --git a/src/services/PostService.ts b/src/services/PostService.ts index c4f89c8..4ad21bd 100644 --- a/src/services/PostService.ts +++ b/src/services/PostService.ts @@ -77,24 +77,7 @@ export class PostService { }); } - public async createPost( - post: CreatePostRequest, - authenticatedUser: UserModel, - ): Promise { - console.log("=== CREATE POST DEBUG ==="); - console.log("authenticatedUser:", authenticatedUser); - console.log("authenticatedUser type:", typeof authenticatedUser); - console.log("authenticatedUser === null:", authenticatedUser === null); - console.log( - "authenticatedUser === undefined:", - authenticatedUser === undefined, - ); - if (authenticatedUser) { - console.log("user.firebaseUid:", authenticatedUser.firebaseUid); - console.log("user.isActive:", authenticatedUser.isActive); - } - console.log("========================"); - + public async createPost(post: CreatePostRequest, authenticatedUser: UserModel): Promise { return this.transactions.readWrite(async (transactionalEntityManager) => { const user = authenticatedUser; if (!user) throw new NotFoundError("User is null or undefined!"); @@ -573,17 +556,13 @@ export class PostService { return this.transactions.readOnly(async (transactionalEntityManager) => { const postRepository = Repositories.post(transactionalEntityManager); const post = await postRepository.getPostById(params.id); - if (!post) throw new NotFoundError("Post not found!"); - const embedding = post.embedding; - if (embedding == null) { - // TODO: after writing migration, throw new NotFoundError('Post does not have embedding!'); + if (!post) throw new NotFoundError('Post not found!'); + + // Posts with no embeddings cant have similar posts, so return empty array + if (post.embedding == null) { return []; } - const similarPosts = await postRepository.getSimilarPosts( - embedding, - post.id, - user.firebaseUid, - ); + const similarPosts = await postRepository.getSimilarPosts(post.embedding, post.id, user.firebaseUid); const activePosts = this.filterInactiveUserPosts(similarPosts); return this.filterBlockedUserPosts(activePosts, user); }); @@ -664,25 +643,24 @@ export class PostService { const postRepository = Repositories.post(transactionalEntityManager); // Get the search by ID const search = await searchRepository.getSearchById(searchIndex); - if (!search) throw new NotFoundError("Search not found!"); - // Parse vector - const searchVector: number[] = JSON.parse(search.searchVector); - // Get active, unarchived posts - const allPosts = await postRepository.getAllPosts(); - const model = await getLoadedModel(); - // For each post, generate embedding from the title - const postEmbeddings = await model.embed(allPosts.map((p) => p.title)); - const embeddingsArray = postEmbeddings.arraySync(); - // Find similarity - const scoredPosts = allPosts.map((post, idx) => ({ - id: post.id, - similarity: this.similarity(searchVector, embeddingsArray[idx]), - })); - // Sort by similarity (descending order) and choose top N - const topPosts = scoredPosts - .sort((a, b) => b.similarity - a.similarity) - .slice(0, postCount); - return topPosts.map((p) => p.id); + if (!search) throw new NotFoundError('Search not found!'); + // Parse search vector + const searchVector: number[] = JSON.parse(search.searchVector); + // Get active, unarchived posts + const allPosts = await postRepository.getAllPosts(); + const postsWithEmbeddings = allPosts.filter(p => p.embedding != null); + + if (postsWithEmbeddings.length === 0) { + return []; + } + // Use precomputed embeddings from db + const scoredPosts = postsWithEmbeddings.map(post => ({ + id: post.id, + similarity: this.similarity(searchVector, post.embedding as number[]) + })); + // Sort by similarity (descending order) and choose top N + const topPosts = scoredPosts.sort((a, b) => b.similarity - a.similarity).slice(0, postCount); + return topPosts.map(p => p.id); }); } diff --git a/src/utils/SentenceEncoder.ts b/src/utils/SentenceEncoder.ts index 7f0301d..bc97f95 100644 --- a/src/utils/SentenceEncoder.ts +++ b/src/utils/SentenceEncoder.ts @@ -1,5 +1,5 @@ -import "@tensorflow/tfjs-node"; -import use = require("@tensorflow-models/universal-sentence-encoder"); +import * as tf from "@tensorflow/tfjs-node"; +import * as use from "@tensorflow-models/universal-sentence-encoder"; let loadedModel: use.UniversalSentenceEncoder | null = null;