nakama/backend/utils/tickets.js

469 lines
17 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const User = require('../models/User');
const TicketActivity = require('../models/TicketActivity');
const Post = require('../models/Post');
const { getMoscowDate, formatMoscowTime } = require('./moscowTime');
/**
* Проверяет, можно ли начислить баллы с учетом лимитов и антифрод правил
*/
async function canAwardTickets(userId, actionType, options = {}) {
// Используем московское время для определения дня
const today = getMoscowDate();
const activity = await TicketActivity.getOrCreateToday(userId);
// Проверка лимитов в зависимости от типа действия
switch (actionType) {
case 'post_created':
return activity.postsCreated < 5;
case 'like_given':
return activity.likesGiven < 50;
case 'like_received':
return activity.likesReceived < 100;
case 'comment_written':
// Проверка длины комментария (антифрод)
if (options.commentLength && options.commentLength < 10) {
return false;
}
return activity.commentsWritten < 20;
case 'comment_received':
return true; // Нет лимита на получение комментариев
case 'art_reaction':
// Проверка лимита на реакцию на арт (100 баллов в сутки с одного арта)
const postId = options.postId;
if (!postId) return false;
const postIdStr = postId.toString();
const currentPoints = (activity.artReactionsPoints && activity.artReactionsPoints[postIdStr]) || 0;
const newPoints = currentPoints + (options.points || 0);
return newPoints <= 100;
default:
return false;
}
}
/**
* Проверяет возраст аккаунта (антифрод - аккаунты младше 24 часов не считаются)
*/
async function isAccountOldEnough(userId) {
const user = await User.findById(userId);
if (!user) return false;
const accountAge = Date.now() - new Date(user.createdAt).getTime();
const hours24 = 24 * 60 * 60 * 1000;
return accountAge >= hours24;
}
/**
* Начисляет баллы пользователю с проверкой лимитов и антифрод
*/
async function awardTickets(userId, points, actionType, options = {}) {
try {
// Антифрод: проверка возраста аккаунта для входящих реакций
if (actionType === 'like_received' || actionType === 'comment_received') {
const senderId = options.senderId;
if (senderId) {
const senderOldEnough = await isAccountOldEnough(senderId);
if (!senderOldEnough) {
console.log(`[Tickets ${formatMoscowTime()}] Пропуск начисления: аккаунт отправителя младше 24 часов (${senderId})`);
return { success: false, reason: 'account_too_new' };
}
}
}
// Проверка возможности начисления
const canAward = await canAwardTickets(userId, actionType, options);
if (!canAward) {
console.log(`[Tickets ${formatMoscowTime()}] Лимит достигнут для ${actionType} пользователя ${userId}`);
return { success: false, reason: 'limit_reached' };
}
// Получить активность за сегодня
const activity = await TicketActivity.getOrCreateToday(userId);
// Обновить счетчики активности
switch (actionType) {
case 'post_created':
activity.postsCreated += 1;
break;
case 'like_given':
activity.likesGiven += 1;
break;
case 'like_received':
activity.likesReceived += 1;
break;
case 'comment_written':
activity.commentsWritten += 1;
break;
case 'comment_received':
activity.commentsReceived += 1;
break;
case 'art_reaction':
const postId = options.postId;
if (postId) {
if (!activity.artReactionsPoints) {
activity.artReactionsPoints = {};
}
const postIdStr = postId.toString();
const currentPoints = activity.artReactionsPoints[postIdStr] || 0;
activity.artReactionsPoints[postIdStr] = currentPoints + points;
activity.markModified('artReactionsPoints');
}
break;
}
await activity.save();
// Начислить баллы пользователю
await User.findByIdAndUpdate(userId, {
$inc: { tickets: points }
});
console.log(`[Tickets ${formatMoscowTime()}] Начислено ${points} баллов пользователю ${userId} за ${actionType}`);
return { success: true, points };
} catch (error) {
console.error(`[Tickets] Ошибка начисления баллов:`, error);
return { success: false, reason: 'error', error: error.message };
}
}
/**
* Начисляет баллы за создание поста
*/
async function awardPostCreation(userId) {
return await awardTickets(userId, 15, 'post_created');
}
/**
* Начисляет баллы за лайк (тому, кто ставит)
*/
async function awardLikeGiven(userId) {
return await awardTickets(userId, 1, 'like_given');
}
/**
* Начисляет баллы за полученный лайк (автору поста)
*/
async function awardLikeReceived(authorId, likerId) {
return await awardTickets(authorId, 2, 'like_received', { senderId: likerId });
}
/**
* Начисляет баллы за написанный комментарий
*/
async function awardCommentWritten(userId, commentLength) {
return await awardTickets(userId, 4, 'comment_written', { commentLength });
}
/**
* Начисляет баллы за полученный комментарий (автору поста)
*/
async function awardCommentReceived(authorId, commenterId) {
return await awardTickets(authorId, 6, 'comment_received', { senderId: commenterId });
}
/**
* Начисляет баллы за реакцию на арт (лайк)
*/
async function awardArtLike(authorId, likerId, postId) {
return await awardTickets(authorId, 8, 'art_reaction', {
senderId: likerId,
postId,
points: 8
});
}
/**
* Начисляет баллы за комментарий под артом
*/
async function awardArtComment(authorId, commenterId, postId) {
return await awardTickets(authorId, 12, 'art_reaction', {
senderId: commenterId,
postId,
points: 12
});
}
/**
* Начисляет билеты за арт, прошедший модерацию
*/
async function awardArtModeration(userId) {
// Проверяем лимиты: 1 арт в день / 5 в неделю
const { getMoscowStartOfDay } = require('./moscowTime');
const today = getMoscowStartOfDay();
const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
const activity = await TicketActivity.getOrCreateToday(userId);
// Проверка лимита: 1 арт в день
const todayArts = activity.artsModerated || 0;
if (todayArts >= 1) {
console.log(`[Tickets ${formatMoscowTime()}] Лимит артов на сегодня достигнут для пользователя ${userId}`);
return { success: false, reason: 'daily_limit_reached' };
}
// Проверка лимита: 5 артов в неделю
const TicketActivityModel = require('../models/TicketActivity');
const weekActivities = await TicketActivityModel.find({
user: userId,
date: { $gte: weekAgo }
});
const weekArtsCount = weekActivities.reduce((sum, act) => {
return sum + (act.artsModerated || 0);
}, 0);
if (weekArtsCount >= 5) {
console.log(`[Tickets ${formatMoscowTime()}] Лимит артов на неделю достигнут для пользователя ${userId}`);
return { success: false, reason: 'weekly_limit_reached' };
}
// Начислить билеты
if (!activity.artsModerated) {
activity.artsModerated = 0;
}
activity.artsModerated += 1;
await activity.save();
await User.findByIdAndUpdate(userId, {
$inc: { tickets: 40 }
});
console.log(`[Tickets ${formatMoscowTime()}] Начислено 40 билетов пользователю ${userId} за арт, прошедший модерацию`);
return { success: true, points: 40 };
}
/**
* Списывает билеты за модерацию арта при удалении поста
*/
async function deductArtModeration(userId, postCreatedAt) {
try {
const { getMoscowStartOfDay } = require('./moscowTime');
// Проверяем, что пост был создан сегодня
const postDate = getMoscowStartOfDay(postCreatedAt);
const today = getMoscowStartOfDay();
// Если пост создан не сегодня, не списываем билеты
if (postDate.getTime() !== today.getTime()) {
return { success: false, reason: 'post_not_today' };
}
// Получить активность за сегодня
const activity = await TicketActivity.getOrCreateToday(userId);
// Уменьшить счетчик артов (но не меньше 0)
if (activity.artsModerated > 0) {
activity.artsModerated -= 1;
await activity.save();
}
// Списать билеты (но не меньше 0)
const user = await User.findById(userId);
if (user) {
const ticketsToDeduct = Math.min(40, user.tickets || 0);
if (ticketsToDeduct > 0) {
await User.findByIdAndUpdate(userId, {
$inc: { tickets: -ticketsToDeduct }
});
console.log(`[Tickets ${formatMoscowTime()}] Списано ${ticketsToDeduct} билетов пользователю ${userId} за удаление арта`);
return { success: true, points: -ticketsToDeduct };
}
}
return { success: true, points: 0 };
} catch (error) {
console.error(`[Tickets] Ошибка списания билетов за арт:`, error);
return { success: false, reason: 'error', error: error.message };
}
}
/**
* Списывает билеты за действие (лайк/комментарий) с удаленным постом
*/
async function deductAction(userId, points, actionType, postCreatedAt) {
try {
const { getMoscowStartOfDay } = require('./moscowTime');
// Проверяем, что действие было в день создания поста
const postDate = getMoscowStartOfDay(postCreatedAt);
const today = getMoscowStartOfDay();
// Если пост создан не сегодня, не списываем билеты
if (postDate.getTime() !== today.getTime()) {
return { success: false, reason: 'post_not_today' };
}
// Получить активность за сегодня
const activity = await TicketActivity.getOrCreateToday(userId);
// Уменьшить счетчики активности
switch (actionType) {
case 'post_created':
if (activity.postsCreated > 0) {
activity.postsCreated -= 1;
}
break;
case 'like_given':
if (activity.likesGiven > 0) {
activity.likesGiven -= 1;
}
break;
case 'like_received':
if (activity.likesReceived > 0) {
activity.likesReceived -= 1;
}
break;
case 'comment_written':
if (activity.commentsWritten > 0) {
activity.commentsWritten -= 1;
}
break;
case 'comment_received':
if (activity.commentsReceived > 0) {
activity.commentsReceived -= 1;
}
break;
}
await activity.save();
// Списать билеты (но не меньше 0)
const user = await User.findById(userId);
if (user) {
const ticketsToDeduct = Math.min(points, user.tickets || 0);
if (ticketsToDeduct > 0) {
await User.findByIdAndUpdate(userId, {
$inc: { tickets: -ticketsToDeduct }
});
console.log(`[Tickets ${formatMoscowTime()}] Списано ${ticketsToDeduct} билетов пользователю ${userId} за ${actionType}`);
return { success: true, points: -ticketsToDeduct };
}
}
return { success: true, points: 0 };
} catch (error) {
console.error(`[Tickets] Ошибка списания билетов за действие:`, error);
return { success: false, reason: 'error', error: error.message };
}
}
/**
* Списывает все билеты, связанные с удаленным постом
* Списывает только действия, которые были в день создания поста
*/
async function deductPostDeletion(post) {
try {
const { getMoscowStartOfDay } = require('./moscowTime');
// Проверяем, что пост был создан сегодня
const postDate = getMoscowStartOfDay(post.createdAt);
const today = getMoscowStartOfDay();
// Если пост создан не сегодня, не списываем билеты
if (postDate.getTime() !== today.getTime()) {
console.log(`[Tickets ${formatMoscowTime()}] Пост создан не сегодня, билеты не списываются`);
return { success: false, reason: 'post_not_today' };
}
const authorId = post.author;
const postCreatedAt = post.createdAt;
// 1. Списываем билеты за создание поста у автора
await deductAction(authorId, 15, 'post_created', postCreatedAt);
// 1.5. Если пост был помечен как арт, списываем билеты за модерацию арта
if (post.isArt) {
await deductArtModeration(authorId, postCreatedAt);
}
// 2. Списываем билеты за полученные лайки у автора
if (post.likes && post.likes.length > 0) {
for (const likerId of post.likes) {
// Проверяем, что лайк был поставлен в день создания поста
// (лайки не имеют даты, но если пост создан сегодня, то и лайки сегодня)
await deductAction(authorId, 2, 'like_received', postCreatedAt);
}
}
// 3. Списываем билеты за полученные комментарии у автора
if (post.comments && post.comments.length > 0) {
for (const comment of post.comments) {
// Проверяем, что комментарий был написан в день создания поста
const commentDate = getMoscowStartOfDay(comment.createdAt || comment.created_at || postCreatedAt);
if (commentDate.getTime() === postDate.getTime()) {
await deductAction(authorId, 6, 'comment_received', postCreatedAt);
}
}
}
// 4. Списываем билеты у тех, кто поставил лайк (за поставленный лайк)
if (post.likes && post.likes.length > 0) {
for (const likerId of post.likes) {
// Проверяем, что это не автор поста
if (!likerId.equals(authorId)) {
await deductAction(likerId, 1, 'like_given', postCreatedAt);
}
}
}
// 5. Списываем билеты у тех, кто написал комментарий (за написанный комментарий)
if (post.comments && post.comments.length > 0) {
for (const comment of post.comments) {
const commentAuthorId = comment.author;
if (commentAuthorId && !commentAuthorId.equals(authorId)) {
// Проверяем, что комментарий был написан в день создания поста
const commentDate = getMoscowStartOfDay(comment.createdAt || comment.created_at || postCreatedAt);
if (commentDate.getTime() === postDate.getTime()) {
// Проверяем длину комментария (>= 10 символов для начисления)
const commentLength = (comment.content || '').trim().length;
if (commentLength >= 10) {
await deductAction(commentAuthorId, 4, 'comment_written', postCreatedAt);
}
}
}
}
}
console.log(`[Tickets ${formatMoscowTime()}] Списаны все билеты за удаление поста ${post._id}`);
return { success: true };
} catch (error) {
console.error(`[Tickets] Ошибка списания билетов за удаление поста:`, error);
return { success: false, reason: 'error', error: error.message };
}
}
/**
* Списывает билеты при удалении поста (старая функция для обратной совместимости)
* @deprecated Используйте deductPostDeletion вместо этого
*/
async function deductPostCreation(userId, postCreatedAt) {
return await deductAction(userId, 15, 'post_created', postCreatedAt);
}
module.exports = {
awardTickets,
awardPostCreation,
awardLikeGiven,
awardLikeReceived,
awardCommentWritten,
awardCommentReceived,
awardArtLike,
awardArtComment,
awardArtModeration,
deductPostCreation,
deductPostDeletion,
deductAction,
canAwardTickets,
isAccountOldEnough
};