197 lines
6.9 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.reactionsRoutes = reactionsRoutes;
const database_1 = require("../../../config/database");
const auth_1 = require("../middleware/auth");
const chat_stream_routes_js_1 = require("./chat-stream.routes.js");
// Rebranded reaction emojis (6 standard social reactions)
const REACTION_EMOJIS = {
like: '👍',
love: '❤️',
laugh: '😂',
wow: '😮',
sad: '😢',
angry: '😠',
};
// Cooldown tracking: userId+type -> lastTimestamp
const reactionCooldowns = new Map();
const COOLDOWN_SECONDS = 30;
// Format video timestamp as MM:SS or H:MM:SS
function formatVideoTime(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
if (h > 0) {
return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
}
return `${m}:${s.toString().padStart(2, '0')}`;
}
async function reactionsRoutes(fastify) {
// Add reaction (authenticated users only)
fastify.post('/', {
preHandler: auth_1.authenticate,
}, async (request, reply) => {
const { mediaId, reactionType, videoTimestamp } = request.body;
const userId = request.user.id;
// Validate reaction type
if (!REACTION_EMOJIS[reactionType]) {
return reply.code(400).send({ message: 'Invalid reaction type' });
}
// Check cooldown
const cooldownKey = `${userId}:${reactionType}`;
const now = Date.now();
const lastReactionTime = reactionCooldowns.get(cooldownKey);
if (lastReactionTime) {
const timeSinceLastReaction = (now - lastReactionTime) / 1000;
if (timeSinceLastReaction < COOLDOWN_SECONDS) {
const cooldownRemaining = Math.ceil(COOLDOWN_SECONDS - timeSinceLastReaction);
return reply.code(429).send({
message: `Please wait ${cooldownRemaining}s before reacting again`,
cooldownEndsAt: new Date(lastReactionTime + COOLDOWN_SECONDS * 1000).toISOString(),
});
}
}
// Check if video exists
const video = await database_1.prisma.video.findUnique({
where: { id: mediaId },
});
if (!video) {
return reply.code(404).send({ message: 'Video not found' });
}
// Update cooldown
reactionCooldowns.set(cooldownKey, now);
// Create reaction
const reaction = await database_1.prisma.videoReaction.create({
data: {
mediaId,
userId,
reactionType: reactionType, // Cast string to ReactionType enum
videoTimestamp,
createdAt: new Date(),
},
include: {
user: {
select: {
id: true,
name: true,
email: true,
},
},
},
});
// Broadcast to SSE subscribers
const broadcastData = {
id: reaction.id,
reactionType: reaction.reactionType,
emoji: REACTION_EMOJIS[reactionType],
videoTimestamp: reaction.videoTimestamp,
formattedTime: formatVideoTime(videoTimestamp),
createdAt: reaction.createdAt.toISOString(),
user: reaction.user
? {
id: reaction.user.id,
name: reaction.user.name || reaction.user.email,
}
: null,
};
(0, chat_stream_routes_js_1.broadcastReactionToVideo)(mediaId, broadcastData);
return {
success: true,
reaction: broadcastData,
};
});
// Get reactions
fastify.get('/', async (request, reply) => {
const { mediaId, userId, limit = '50' } = request.query;
const where = {};
// Filter by mediaId if provided
if (mediaId) {
where.mediaId = parseInt(mediaId);
}
// Filter by userId if provided
if (userId) {
where.userId = userId;
}
const reactions = await database_1.prisma.videoReaction.findMany({
where,
orderBy: {
createdAt: 'desc',
},
take: parseInt(limit),
});
// Enhance with emojis and formatted times
const enhancedReactions = reactions.map(r => ({
...r,
emoji: REACTION_EMOJIS[r.reactionType] || '❓',
formattedTime: formatVideoTime(r.videoTimestamp),
}));
return {
reactions: enhancedReactions,
};
});
// Get reaction config (returns available reactions)
fastify.get('/config', async (request, reply) => {
return {
reactions: Object.entries(REACTION_EMOJIS).map(([type, emoji]) => ({
type,
emoji,
label: type.charAt(0).toUpperCase() + type.slice(1),
})),
};
});
// Get reactions for chat timeline (non-aggregated, for display in chat)
fastify.get('/:mediaId/chat', async (request, reply) => {
const mediaId = parseInt(request.params.mediaId, 10);
const limit = parseInt(request.query.limit || '500', 10);
if (isNaN(mediaId)) {
return reply.code(400).send({ message: 'Invalid media ID' });
}
const reactions = await database_1.prisma.videoReaction.findMany({
where: { mediaId },
include: {
user: {
select: {
id: true,
name: true,
email: true,
},
},
},
orderBy: {
createdAt: 'asc',
},
take: limit,
});
// Transform for chat timeline
const transformedReactions = reactions.map((r) => ({
id: r.id,
type: 'reaction',
reactionType: r.reactionType,
emoji: REACTION_EMOJIS[r.reactionType] || '❓',
videoTimestamp: r.videoTimestamp,
formattedTime: formatVideoTime(r.videoTimestamp),
createdAt: r.createdAt.toISOString(),
user: r.user
? {
id: r.user.id,
name: r.user.name || r.user.email,
}
: null,
}));
return {
reactions: transformedReactions,
};
});
/**
* Cleanup cooldown map periodically (every 5 minutes)
*/
setInterval(() => {
const now = Date.now();
for (const [key, timestamp] of reactionCooldowns.entries()) {
if (now - timestamp > COOLDOWN_SECONDS * 1000) {
reactionCooldowns.delete(key);
}
}
}, 5 * 60 * 1000);
}
//# sourceMappingURL=reactions.routes.js.map