feat: implement notification system

This commit is contained in:
Face 2025-06-11 18:37:03 +03:00
parent de3f8a4929
commit e61c41e414
19 changed files with 883 additions and 3196 deletions

View file

@ -4,6 +4,7 @@ import { db } from '$lib/server/db';
import { coin, userPortfolio, user, transaction, priceHistory } from '$lib/server/db/schema';
import { eq, and, gte } from 'drizzle-orm';
import { redis } from '$lib/server/redis';
import { createNotification } from '$lib/server/notification';
async function calculate24hMetrics(coinId: number, currentPrice: number) {
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
@ -329,6 +330,36 @@ export async function POST({ params, request }) {
})
.where(eq(coin.id, coinData.id));
const isRugPull = priceImpact < -20 && totalCost > 1000;
// Send rug pull notifications to affected users
if (isRugPull) {
(async () => {
const affectedUsers = await db
.select({
userId: userPortfolio.userId,
quantity: userPortfolio.quantity
})
.from(userPortfolio)
.where(eq(userPortfolio.coinId, coinData.id));
for (const holder of affectedUsers) {
if (holder.userId === userId) continue;
const holdingValue = Number(holder.quantity) * newPrice;
if (holdingValue > 10) {
const lossAmount = Number(holder.quantity) * (currentPrice - newPrice);
await createNotification(
holder.userId.toString(),
'RUG_PULL',
'Coin rugpulled!',
`A coin you owned, ${coinData.name} (*${normalizedSymbol}), crashed ${Math.abs(priceImpact).toFixed(1)}%!`,
);
}
}
})();
}
const priceUpdateData = {
currentPrice: newPrice,
marketCap: Number(coinData.circulatingSupply) * newPrice,

View file

@ -0,0 +1,77 @@
import { auth } from '$lib/auth';
import { error, json } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { notifications } from '$lib/server/db/schema';
import { eq, desc, and, count, inArray } from 'drizzle-orm';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ url, request }) => {
const session = await auth.api.getSession({ headers: request.headers });
if (!session?.user) throw error(401, 'Not authenticated');
const userId = Number(session.user.id);
const unreadOnly = url.searchParams.get('unread_only') === 'true';
try {
const conditions = [eq(notifications.userId, userId)];
if (unreadOnly) {
conditions.push(eq(notifications.isRead, false));
}
const whereCondition = and(...conditions);
const notificationsList = await db.select({
id: notifications.id,
type: notifications.type,
title: notifications.title,
message: notifications.message,
isRead: notifications.isRead,
createdAt: notifications.createdAt,
})
.from(notifications)
.where(whereCondition)
.orderBy(desc(notifications.createdAt))
.limit(50);
const unreadCount = await db
.select({ count: count() })
.from(notifications)
.where(and(eq(notifications.userId, userId), eq(notifications.isRead, false)))
.then(result => result[0]?.count || 0);
return json({
notifications: notificationsList,
unreadCount
});
} catch (e) {
console.error('Failed to fetch notificationss:', e);
throw error(500, 'Failed to fetch notificationss');
}
};
export const PATCH: RequestHandler = async ({ request }) => {
const session = await auth.api.getSession({ headers: request.headers });
if (!session?.user) throw error(401, 'Not authenticated');
const userId = Number(session.user.id);
const { ids, markAsRead } = await request.json();
if (!Array.isArray(ids) || typeof markAsRead !== 'boolean') {
throw error(400, 'Invalid request body');
}
try {
await db
.update(notifications)
.set({ isRead: markAsRead })
.where(and(
eq(notifications.userId, userId),
inArray(notifications.id, ids)
));
return json({ success: true });
} catch (e) {
console.error('Failed to update notifications:', e);
throw error(500, 'Failed to update notifications');
}
};

View file

@ -3,6 +3,8 @@ import { error, json } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { user, userPortfolio, coin, transaction } from '$lib/server/db/schema';
import { eq, and } from 'drizzle-orm';
import { createNotification } from '$lib/server/notification';
import { formatValue } from '$lib/utils';
import type { RequestHandler } from './$types';
interface TransferRequest {
@ -19,7 +21,7 @@ export const POST: RequestHandler = async ({ request }) => {
if (!session?.user) {
throw error(401, 'Not authenticated');
} try {
} try {
const { recipientUsername, type, amount, coinSymbol }: TransferRequest = await request.json();
if (!recipientUsername || !type || !amount || typeof amount !== 'number' || !Number.isFinite(amount) || amount <= 0) {
@ -123,6 +125,15 @@ export const POST: RequestHandler = async ({ request }) => {
recipientUserId: recipientData.id
});
(async () => {
await createNotification(
recipientData.id.toString(),
'TRANSFER',
'Money received!',
`You received ${formatValue(amount)} from @${senderData.username}`,
);
})();
return json({
success: true,
type: 'CASH',
@ -247,6 +258,15 @@ export const POST: RequestHandler = async ({ request }) => {
recipientUserId: recipientData.id
});
(async () => {
await createNotification(
recipientData.id.toString(),
'TRANSFER',
'Coins received!',
`You received ${amount.toFixed(6)} *${coinData.symbol} from @${senderData.username}`,
);
})();
return json({
success: true,
type: 'COIN',