hMarket Trae ilk versiyon
This commit is contained in:
523
backend/src/services/notificationService.js
Normal file
523
backend/src/services/notificationService.js
Normal file
@@ -0,0 +1,523 @@
|
||||
const admin = require('firebase-admin');
|
||||
|
||||
class NotificationService {
|
||||
constructor(prisma, io) {
|
||||
this.prisma = prisma;
|
||||
this.io = io;
|
||||
this.initializeFirebase();
|
||||
}
|
||||
|
||||
initializeFirebase() {
|
||||
try {
|
||||
if (!admin.apps.length) {
|
||||
const serviceAccount = {
|
||||
type: process.env.FIREBASE_TYPE,
|
||||
project_id: process.env.FIREBASE_PROJECT_ID,
|
||||
private_key_id: process.env.FIREBASE_PRIVATE_KEY_ID,
|
||||
private_key: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
|
||||
client_email: process.env.FIREBASE_CLIENT_EMAIL,
|
||||
client_id: process.env.FIREBASE_CLIENT_ID,
|
||||
auth_uri: process.env.FIREBASE_AUTH_URI,
|
||||
token_uri: process.env.FIREBASE_TOKEN_URI,
|
||||
auth_provider_x509_cert_url: process.env.FIREBASE_AUTH_PROVIDER_X509_CERT_URL,
|
||||
client_x509_cert_url: process.env.FIREBASE_CLIENT_X509_CERT_URL
|
||||
};
|
||||
|
||||
admin.initializeApp({
|
||||
credential: admin.credential.cert(serviceAccount)
|
||||
});
|
||||
|
||||
console.log('Firebase Admin SDK initialized successfully');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Firebase initialization error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Veritabanına bildirim kaydet
|
||||
*/
|
||||
async createNotification(data) {
|
||||
try {
|
||||
const notification = await this.prisma.notification.create({
|
||||
data: {
|
||||
userId: data.userId,
|
||||
type: data.type,
|
||||
title: data.title,
|
||||
message: data.message,
|
||||
data: data.data ? JSON.stringify(data.data) : null,
|
||||
relatedListId: data.relatedListId || null,
|
||||
relatedUserId: data.relatedUserId || null
|
||||
},
|
||||
include: {
|
||||
relatedList: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true
|
||||
}
|
||||
},
|
||||
relatedUser: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
avatar: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return notification;
|
||||
} catch (error) {
|
||||
console.error('Error creating notification:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Socket.IO ile gerçek zamanlı bildirim gönder
|
||||
*/
|
||||
async sendRealtimeNotification(userId, notification) {
|
||||
try {
|
||||
if (this.io) {
|
||||
this.io.to(`user_${userId}`).emit('notification', notification);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending realtime notification:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Push notification gönder
|
||||
*/
|
||||
async sendPushNotification(userId, notification) {
|
||||
try {
|
||||
// Kullanıcının push notification ayarını kontrol et
|
||||
const setting = await this.prisma.setting.findUnique({
|
||||
where: {
|
||||
userId_key: {
|
||||
userId,
|
||||
key: 'notification_push_enabled'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (setting && setting.value === 'false') {
|
||||
return; // Push notification kapalı
|
||||
}
|
||||
|
||||
// Kullanıcının device token'larını al
|
||||
const deviceTokens = await this.prisma.deviceToken.findMany({
|
||||
where: {
|
||||
userId,
|
||||
isActive: true
|
||||
}
|
||||
});
|
||||
|
||||
if (deviceTokens.length === 0) {
|
||||
return; // Device token yok
|
||||
}
|
||||
|
||||
const tokens = deviceTokens.map(dt => dt.token);
|
||||
|
||||
const message = {
|
||||
notification: {
|
||||
title: notification.title,
|
||||
body: notification.message
|
||||
},
|
||||
data: {
|
||||
type: notification.type,
|
||||
notificationId: notification.id,
|
||||
...(notification.data ? JSON.parse(notification.data) : {})
|
||||
},
|
||||
tokens
|
||||
};
|
||||
|
||||
const response = await admin.messaging().sendMulticast(message);
|
||||
|
||||
// Başarısız token'ları temizle
|
||||
if (response.failureCount > 0) {
|
||||
const failedTokens = [];
|
||||
response.responses.forEach((resp, idx) => {
|
||||
if (!resp.success) {
|
||||
failedTokens.push(tokens[idx]);
|
||||
}
|
||||
});
|
||||
|
||||
if (failedTokens.length > 0) {
|
||||
await this.prisma.deviceToken.updateMany({
|
||||
where: {
|
||||
token: { in: failedTokens }
|
||||
},
|
||||
data: {
|
||||
isActive: false
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Push notification sent to ${response.successCount} devices`);
|
||||
} catch (error) {
|
||||
console.error('Error sending push notification:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tam bildirim gönderme (DB + Realtime + Push)
|
||||
*/
|
||||
async sendNotification(data) {
|
||||
try {
|
||||
// Kullanıcının bildirim ayarını kontrol et
|
||||
const notificationKey = `notification_${data.type}`;
|
||||
const setting = await this.prisma.setting.findUnique({
|
||||
where: {
|
||||
userId_key: {
|
||||
userId: data.userId,
|
||||
key: notificationKey
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (setting && setting.value === 'false') {
|
||||
return; // Bu tür bildirim kapalı
|
||||
}
|
||||
|
||||
// Veritabanına kaydet
|
||||
const notification = await this.createNotification(data);
|
||||
|
||||
// Gerçek zamanlı gönder
|
||||
await this.sendRealtimeNotification(data.userId, notification);
|
||||
|
||||
// Push notification gönder
|
||||
await this.sendPushNotification(data.userId, notification);
|
||||
|
||||
return notification;
|
||||
} catch (error) {
|
||||
console.error('Error in sendNotification:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Birden fazla kullanıcıya bildirim gönder
|
||||
*/
|
||||
async sendNotificationToMultipleUsers(userIds, data) {
|
||||
try {
|
||||
const promises = userIds.map(userId =>
|
||||
this.sendNotification({ ...data, userId })
|
||||
);
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
|
||||
const successful = results.filter(r => r.status === 'fulfilled').length;
|
||||
const failed = results.filter(r => r.status === 'rejected').length;
|
||||
|
||||
console.log(`Notifications sent: ${successful} successful, ${failed} failed`);
|
||||
|
||||
return { successful, failed };
|
||||
} catch (error) {
|
||||
console.error('Error sending notifications to multiple users:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste üyelerine bildirim gönder
|
||||
*/
|
||||
async sendNotificationToListMembers(listId, data, excludeUserId = null) {
|
||||
try {
|
||||
const members = await this.prisma.listMember.findMany({
|
||||
where: {
|
||||
listId,
|
||||
userId: excludeUserId ? { not: excludeUserId } : undefined
|
||||
},
|
||||
select: {
|
||||
userId: true
|
||||
}
|
||||
});
|
||||
|
||||
const userIds = members.map(m => m.userId);
|
||||
|
||||
if (userIds.length === 0) {
|
||||
return { successful: 0, failed: 0 };
|
||||
}
|
||||
|
||||
return await this.sendNotificationToMultipleUsers(userIds, {
|
||||
...data,
|
||||
relatedListId: listId
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error sending notifications to list members:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste daveti bildirimi
|
||||
*/
|
||||
async sendListInviteNotification(invitedUserId, inviterUser, list) {
|
||||
return await this.sendNotification({
|
||||
userId: invitedUserId,
|
||||
type: 'list_invite',
|
||||
title: 'Liste Daveti',
|
||||
message: `${inviterUser.firstName} ${inviterUser.lastName} sizi "${list.name}" listesine davet etti`,
|
||||
data: {
|
||||
inviterId: inviterUser.id,
|
||||
listId: list.id
|
||||
},
|
||||
relatedListId: list.id,
|
||||
relatedUserId: inviterUser.id
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ürün eklendi bildirimi
|
||||
*/
|
||||
async sendItemAddedNotification(listId, adderUser, itemName, excludeUserId = null) {
|
||||
return await this.sendNotificationToListMembers(listId, {
|
||||
type: 'item_added',
|
||||
title: 'Ürün Eklendi',
|
||||
message: `${adderUser.firstName} "${itemName}" ürününü listeye ekledi`,
|
||||
data: {
|
||||
adderId: adderUser.id,
|
||||
itemName
|
||||
},
|
||||
relatedUserId: adderUser.id
|
||||
}, excludeUserId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ürün silindi bildirimi
|
||||
*/
|
||||
async sendItemRemovedNotification(listId, removerUser, itemName, excludeUserId = null) {
|
||||
return await this.sendNotificationToListMembers(listId, {
|
||||
type: 'item_removed',
|
||||
title: 'Ürün Silindi',
|
||||
message: `${removerUser.firstName} "${itemName}" ürününü listeden sildi`,
|
||||
data: {
|
||||
removerId: removerUser.id,
|
||||
itemName
|
||||
},
|
||||
relatedUserId: removerUser.id
|
||||
}, excludeUserId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ürün satın alındı bildirimi
|
||||
*/
|
||||
async sendItemPurchasedNotification(listId, purchaserUser, itemName, excludeUserId = null) {
|
||||
return await this.sendNotificationToListMembers(listId, {
|
||||
type: 'item_purchased',
|
||||
title: 'Ürün Satın Alındı',
|
||||
message: `${purchaserUser.firstName} "${itemName}" ürününü satın aldı`,
|
||||
data: {
|
||||
purchaserId: purchaserUser.id,
|
||||
itemName
|
||||
},
|
||||
relatedUserId: purchaserUser.id
|
||||
}, excludeUserId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste paylaşıldı bildirimi
|
||||
*/
|
||||
async sendListSharedNotification(listId, sharerUser, sharedWithUser, listName) {
|
||||
return await this.sendNotification({
|
||||
userId: sharedWithUser.id,
|
||||
type: 'list_shared',
|
||||
title: 'Liste Paylaşıldı',
|
||||
message: `${sharerUser.firstName} "${listName}" listesini sizinle paylaştı`,
|
||||
data: {
|
||||
sharerId: sharerUser.id,
|
||||
listName
|
||||
},
|
||||
relatedListId: listId,
|
||||
relatedUserId: sharerUser.id
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Device token kaydet
|
||||
*/
|
||||
async registerDeviceToken(userId, token, deviceInfo = {}) {
|
||||
try {
|
||||
// Mevcut token'ı kontrol et
|
||||
const existingToken = await this.prisma.deviceToken.findFirst({
|
||||
where: {
|
||||
token,
|
||||
userId
|
||||
}
|
||||
});
|
||||
|
||||
if (existingToken) {
|
||||
// Token'ı güncelle
|
||||
return await this.prisma.deviceToken.update({
|
||||
where: { id: existingToken.id },
|
||||
data: {
|
||||
isActive: true,
|
||||
deviceInfo: deviceInfo ? JSON.stringify(deviceInfo) : null,
|
||||
updatedAt: new Date()
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Yeni token oluştur
|
||||
return await this.prisma.deviceToken.create({
|
||||
data: {
|
||||
userId,
|
||||
token,
|
||||
deviceInfo: deviceInfo ? JSON.stringify(deviceInfo) : null
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error registering device token:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Device token'ı deaktive et
|
||||
*/
|
||||
async unregisterDeviceToken(userId, token) {
|
||||
try {
|
||||
return await this.prisma.deviceToken.updateMany({
|
||||
where: {
|
||||
userId,
|
||||
token
|
||||
},
|
||||
data: {
|
||||
isActive: false
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error unregistering device token:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste üyelerine bildirim gönder
|
||||
*/
|
||||
async notifyListMembers(listId, excludeUserId, notificationData) {
|
||||
try {
|
||||
// Liste üyelerini getir
|
||||
const listMembers = await this.prisma.listMember.findMany({
|
||||
where: {
|
||||
listId,
|
||||
userId: {
|
||||
not: excludeUserId // Aksiyonu yapan kullanıcıyı hariç tut
|
||||
}
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Liste sahibini de dahil et (eğer üye değilse)
|
||||
const list = await this.prisma.shoppingList.findUnique({
|
||||
where: { id: listId },
|
||||
include: {
|
||||
owner: {
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const memberUserIds = listMembers.map(member => member.userId);
|
||||
if (list.owner.id !== excludeUserId && !memberUserIds.includes(list.owner.id)) {
|
||||
listMembers.push({
|
||||
user: list.owner,
|
||||
userId: list.owner.id
|
||||
});
|
||||
}
|
||||
|
||||
// Her üyeye bildirim gönder
|
||||
const notifications = [];
|
||||
for (const member of listMembers) {
|
||||
|
||||
// Socket.IO ile gerçek zamanlı bildirim
|
||||
if (this.io) {
|
||||
this.io.to(`user_${member.userId}`).emit('notification', {
|
||||
type: notificationData.type,
|
||||
title: notificationData.title,
|
||||
message: notificationData.message,
|
||||
data: notificationData.data,
|
||||
timestamp: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
// Veritabanına bildirim kaydet
|
||||
const notification = await this.createNotification({
|
||||
userId: member.userId,
|
||||
type: notificationData.type,
|
||||
title: notificationData.title,
|
||||
message: notificationData.message,
|
||||
data: notificationData.data,
|
||||
relatedListId: listId,
|
||||
relatedUserId: excludeUserId
|
||||
});
|
||||
|
||||
notifications.push(notification);
|
||||
|
||||
// Push notification gönder
|
||||
await this.sendPushNotification(member.userId, {
|
||||
title: notificationData.title,
|
||||
body: notificationData.message,
|
||||
data: notificationData.data
|
||||
});
|
||||
}
|
||||
|
||||
return notifications;
|
||||
} catch (error) {
|
||||
console.error('Error notifying list members:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
let notificationServiceInstance = null;
|
||||
|
||||
// Factory function
|
||||
const createNotificationService = (prisma, io) => {
|
||||
if (!notificationServiceInstance) {
|
||||
notificationServiceInstance = new NotificationService(prisma, io);
|
||||
}
|
||||
return notificationServiceInstance;
|
||||
};
|
||||
|
||||
// Export both class and factory
|
||||
module.exports = {
|
||||
NotificationService,
|
||||
createNotificationService,
|
||||
// Default export for backward compatibility
|
||||
notifyListMembers: async (...args) => {
|
||||
if (!notificationServiceInstance) {
|
||||
throw new Error('NotificationService not initialized. Call createNotificationService first.');
|
||||
}
|
||||
return notificationServiceInstance.notifyListMembers(...args);
|
||||
},
|
||||
createNotification: async (...args) => {
|
||||
if (!notificationServiceInstance) {
|
||||
throw new Error('NotificationService not initialized. Call createNotificationService first.');
|
||||
}
|
||||
return notificationServiceInstance.createNotification(...args);
|
||||
},
|
||||
sendPushNotification: async (...args) => {
|
||||
if (!notificationServiceInstance) {
|
||||
throw new Error('NotificationService not initialized. Call createNotificationService first.');
|
||||
}
|
||||
return notificationServiceInstance.sendPushNotification(...args);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user