1206 lines
45 KiB
JavaScript
1206 lines
45 KiB
JavaScript
import BaseService from './BaseService.js';
|
|
import { Op } from 'sequelize';
|
|
import User from '../models/community/user.js';
|
|
import UserParam from '../models/community/user_param.js';
|
|
import UserParamType from '../models/type/user_param.js';
|
|
import UserParamValue from '../models/type/user_param_value.js';
|
|
import UserParamVisibility from '../models/community/user_param_visibility.js';
|
|
import UserParamVisibilityType from '../models/type/user_param_visibility.js';
|
|
import Folder from '../models/community/folder.js';
|
|
import Image from '../models/community/image.js';
|
|
import EroticVideo from '../models/community/erotic_video.js';
|
|
import EroticContentReport from '../models/community/erotic_content_report.js';
|
|
import ImageVisibilityType from '../models/type/image_visibility.js';
|
|
import FolderImageVisibility from '../models/community/folder_image_visibility.js';
|
|
import ImageImageVisibility from '../models/community/image_image_visibility.js';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
import fs from 'fs';
|
|
import fsPromises from 'fs/promises';
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import FolderVisibilityUser from '../models/community/folder_visibility_user.js';
|
|
import ImageVisibilityUser from '../models/community/image_visibility_user.js';
|
|
import GuestbookEntry from '../models/community/guestbook.js';
|
|
import { JSDOM } from 'jsdom';
|
|
import DOMPurify from 'dompurify';
|
|
import sharp from 'sharp';
|
|
import Diary from '../models/community/diary.js';
|
|
import Friendship from '../models/community/friendship.js';
|
|
import { getUserSession } from '../utils/redis.js';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
class SocialNetworkService extends BaseService {
|
|
normalizeAdultVerificationStatus(value) {
|
|
if (!value) return 'none';
|
|
const normalized = String(value).trim().toLowerCase();
|
|
return ['pending', 'approved', 'rejected'].includes(normalized) ? normalized : 'none';
|
|
}
|
|
|
|
async getAdultAccessState(userId) {
|
|
const params = await this.getUserParams(userId, ['birthdate', 'adult_verification_status', 'adult_upload_blocked']);
|
|
const birthdateParam = params.find(param => param.paramType.description === 'birthdate');
|
|
const statusParam = params.find(param => param.paramType.description === 'adult_verification_status');
|
|
const uploadBlockedParam = params.find(param => param.paramType.description === 'adult_upload_blocked');
|
|
const age = birthdateParam ? this.calculateAge(birthdateParam.value) : 0;
|
|
const adultVerificationStatus = this.normalizeAdultVerificationStatus(statusParam?.value);
|
|
return {
|
|
age,
|
|
isAdult: age >= 18,
|
|
adultVerificationStatus,
|
|
adultAccessEnabled: age >= 18 && adultVerificationStatus === 'approved',
|
|
adultUploadBlocked: String(uploadBlockedParam?.value).toLowerCase() === 'true'
|
|
};
|
|
}
|
|
|
|
async requireAdultAreaAccessByHash(hashedId) {
|
|
const userId = await this.checkUserAccess(hashedId);
|
|
const adultAccess = await this.getAdultAccessState(userId);
|
|
if (!adultAccess.adultAccessEnabled) {
|
|
const error = new Error('Adult area access denied');
|
|
error.status = 403;
|
|
throw error;
|
|
}
|
|
return userId;
|
|
}
|
|
|
|
async ensureAdultUploadsAllowed(userId) {
|
|
const adultAccess = await this.getAdultAccessState(userId);
|
|
if (adultAccess.adultUploadBlocked) {
|
|
const error = new Error('Adult uploads are blocked');
|
|
error.status = 403;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async resolveEroticTarget(targetType, targetId) {
|
|
if (targetType === 'image') {
|
|
const image = await Image.findOne({
|
|
where: {
|
|
id: targetId,
|
|
isAdultContent: true
|
|
}
|
|
});
|
|
if (!image) {
|
|
throw new Error('Target not found');
|
|
}
|
|
return { targetType, target: image, ownerId: image.userId };
|
|
}
|
|
if (targetType === 'video') {
|
|
const video = await EroticVideo.findByPk(targetId);
|
|
if (!video) {
|
|
throw new Error('Target not found');
|
|
}
|
|
return { targetType, target: video, ownerId: video.userId };
|
|
}
|
|
throw new Error('Unsupported target type');
|
|
}
|
|
|
|
async ensureRootFolder(userId) {
|
|
let rootFolder = await Folder.findOne({
|
|
where: { parentId: null, userId, isAdultArea: false },
|
|
include: [{
|
|
model: ImageVisibilityType,
|
|
through: { model: FolderImageVisibility },
|
|
attributes: ['id'],
|
|
}]
|
|
});
|
|
if (rootFolder) {
|
|
return rootFolder;
|
|
}
|
|
|
|
const user = await User.findOne({ where: { id: userId } });
|
|
const visibility = await ImageVisibilityType.findOne({ where: { description: 'everyone' } });
|
|
rootFolder = await Folder.create({
|
|
name: user.username,
|
|
parentId: null,
|
|
userId,
|
|
isAdultArea: false
|
|
});
|
|
if (visibility) {
|
|
await FolderImageVisibility.create({
|
|
folderId: rootFolder.id,
|
|
visibilityTypeId: visibility.id
|
|
});
|
|
}
|
|
return await Folder.findOne({
|
|
where: { id: rootFolder.id },
|
|
include: [{
|
|
model: ImageVisibilityType,
|
|
through: { model: FolderImageVisibility },
|
|
attributes: ['id'],
|
|
}]
|
|
});
|
|
}
|
|
|
|
async ensureAdultRootFolder(userId) {
|
|
const rootFolder = await this.ensureRootFolder(userId);
|
|
let adultRoot = await Folder.findOne({
|
|
where: {
|
|
parentId: rootFolder.id,
|
|
userId,
|
|
isAdultArea: true,
|
|
name: 'Erotik'
|
|
},
|
|
include: [{
|
|
model: ImageVisibilityType,
|
|
through: { model: FolderImageVisibility },
|
|
attributes: ['id'],
|
|
}]
|
|
});
|
|
if (adultRoot) {
|
|
return adultRoot;
|
|
}
|
|
|
|
const adultsVisibility = await ImageVisibilityType.findOne({ where: { description: 'adults' } });
|
|
adultRoot = await Folder.create({
|
|
name: 'Erotik',
|
|
parentId: rootFolder.id,
|
|
userId,
|
|
isAdultArea: true
|
|
});
|
|
if (adultsVisibility) {
|
|
await FolderImageVisibility.create({
|
|
folderId: adultRoot.id,
|
|
visibilityTypeId: adultsVisibility.id
|
|
});
|
|
}
|
|
return await Folder.findOne({
|
|
where: { id: adultRoot.id },
|
|
include: [{
|
|
model: ImageVisibilityType,
|
|
through: { model: FolderImageVisibility },
|
|
attributes: ['id'],
|
|
}]
|
|
});
|
|
}
|
|
|
|
async searchUsers({ hashedUserId, username, ageFrom, ageTo, genders }) {
|
|
const whereClause = this.buildSearchWhereClause(username);
|
|
const user = await this.loadUserByHash(hashedUserId);
|
|
console.log(hashedUserId, user);
|
|
if (!user) {
|
|
throw new Error('User not found');
|
|
}
|
|
whereClause.id = { [Op.ne]: user.id };
|
|
const users = await User.findAll({ where: whereClause, include: this.getUserParamsInclude() });
|
|
return await this.filterUsersByCriteria(users, ageFrom, ageTo, genders);
|
|
}
|
|
|
|
async getProfile(hashedUserId, requestingUserId) {
|
|
await this.checkUserAccess(requestingUserId);
|
|
const user = await this.fetchUserProfile(hashedUserId);
|
|
if (!user) return null;
|
|
return this.constructUserProfile(user, requestingUserId);
|
|
}
|
|
|
|
async createFolder(hashedUserId, data, folderId, options = {}) {
|
|
await this.checkUserAccess(hashedUserId);
|
|
const user = await this.loadUserByHash(hashedUserId);
|
|
if (!user) {
|
|
throw new Error('User not found');
|
|
}
|
|
const isAdultArea = Boolean(options.isAdultArea);
|
|
if (isAdultArea) {
|
|
await this.requireAdultAreaAccessByHash(hashedUserId);
|
|
}
|
|
console.log('given data', data, folderId);
|
|
const parentFolder = data.parentId ? await Folder.findOne({
|
|
where: { id: data.parentId, userId: user.id, isAdultArea }
|
|
}) : null;
|
|
if (data.parentId && !parentFolder) {
|
|
throw new Error('Parent folder not found');
|
|
}
|
|
console.log('parentFolder', parentFolder);
|
|
let newFolder;
|
|
if (parentFolder) {
|
|
newFolder = await Folder.create({
|
|
parentId: parentFolder.id || null,
|
|
userId: user.id,
|
|
name: data.name,
|
|
isAdultArea
|
|
});
|
|
} else {
|
|
newFolder = await Folder.findOne({
|
|
where: { id: folderId, userId: user.id, isAdultArea }
|
|
});
|
|
if (!newFolder) {
|
|
throw new Error('Folder not found or user does not own the folder');
|
|
}
|
|
newFolder.name = data.name;
|
|
await newFolder.save();
|
|
}
|
|
await FolderImageVisibility.destroy({
|
|
where: { folderId: newFolder.id }
|
|
});
|
|
for (const visibilityId of data.visibilities) {
|
|
await FolderImageVisibility.create({
|
|
folderId: newFolder.id,
|
|
visibilityTypeId: visibilityId
|
|
});
|
|
}
|
|
return newFolder;
|
|
}
|
|
|
|
async getFolders(hashedId) {
|
|
const userId = await this.checkUserAccess(hashedId);
|
|
const rootFolder = await this.ensureRootFolder(userId);
|
|
const children = await this.getSubFolders(rootFolder.id, userId, false);
|
|
rootFolder = rootFolder.get();
|
|
rootFolder.visibilityTypeIds = rootFolder.image_visibility_types.map(v => v.id);
|
|
delete rootFolder.image_visibility_types;
|
|
rootFolder.children = children;
|
|
return rootFolder;
|
|
}
|
|
|
|
async getSubFolders(parentId, userId, isAdultArea = false) {
|
|
const folders = await Folder.findAll({
|
|
where: { parentId, userId, isAdultArea },
|
|
include: [{
|
|
model: ImageVisibilityType,
|
|
through: { model: FolderImageVisibility },
|
|
attributes: ['id'],
|
|
}],
|
|
order: [
|
|
['name', 'asc']
|
|
]
|
|
});
|
|
for (const folder of folders) {
|
|
const children = await this.getSubFolders(folder.id, userId, isAdultArea);
|
|
const visibilityTypeIds = folder.image_visibility_types.map(v => v.id);
|
|
folder.setDataValue('visibilityTypeIds', visibilityTypeIds);
|
|
folder.setDataValue('children', children);
|
|
folder.setDataValue('image_visibility_types', undefined);
|
|
}
|
|
return folders.map(folder => folder.get());
|
|
}
|
|
|
|
async getFolderImageList(hashedId, folderId) {
|
|
const userId = await this.checkUserAccess(hashedId);
|
|
const folder = await Folder.findOne({
|
|
where: {
|
|
id: folderId,
|
|
userId,
|
|
isAdultArea: false
|
|
}
|
|
});
|
|
if (!folder) throw new Error('Folder not found');
|
|
return await Image.findAll({
|
|
where: {
|
|
folderId: folder.id,
|
|
isAdultContent: false
|
|
},
|
|
order: [
|
|
['title', 'asc']
|
|
]
|
|
});
|
|
}
|
|
|
|
async uploadImage(hashedId, file, formData) {
|
|
const userId = await this.getUserId(hashedId);
|
|
const processedImageName = await this.processAndUploadUserImage(file, 'user');
|
|
const newImage = await this.createImageRecord(formData, userId, file, processedImageName, { isAdultContent: false });
|
|
await this.saveImageVisibilities(newImage.id, formData.visibility);
|
|
return newImage;
|
|
}
|
|
|
|
async processAndUploadUserImage(file, storageType = 'user') {
|
|
try {
|
|
const img = sharp(file.buffer);
|
|
const metadata = await img.metadata();
|
|
if (!metadata || !['jpeg', 'png', 'webp', 'gif'].includes(metadata.format)) {
|
|
throw new Error('File is not a valid image');
|
|
}
|
|
if (metadata.width < 75 || metadata.height < 75) {
|
|
throw new Error('Image dimensions are too small. Minimum size is 75x75 pixels.');
|
|
}
|
|
const resizedImg = img.resize({
|
|
width: Math.min(metadata.width, 500),
|
|
height: Math.min(metadata.height, 500),
|
|
fit: sharp.fit.inside,
|
|
withoutEnlargement: true
|
|
});
|
|
const newFileName = this.generateUniqueFileName(file.originalname);
|
|
const filePath = this.buildFilePath(newFileName, storageType);
|
|
await resizedImg.toFile(filePath);
|
|
return newFileName;
|
|
} catch (error) {
|
|
throw new Error(`Failed to process image: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
|
|
async getUserId(hashedId) {
|
|
return await this.checkUserAccess(hashedId);
|
|
}
|
|
|
|
generateUniqueFileName(originalFileName) {
|
|
const uniqueHash = uuidv4();
|
|
return uniqueHash;
|
|
}
|
|
|
|
buildFilePath(fileName, type) {
|
|
const basePath = path.join(__dirname, '..', 'images', type);
|
|
return path.join(basePath, fileName);
|
|
}
|
|
|
|
async saveFile(buffer, filePath) {
|
|
try {
|
|
await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
|
|
await fsPromises.writeFile(filePath, buffer);
|
|
} catch (error) {
|
|
throw new Error(`Failed to save file: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
async createImageRecord(formData, userId, file, fileName, options = {}) {
|
|
try {
|
|
return await Image.create({
|
|
title: formData.title,
|
|
description: formData.description || null,
|
|
originalFileName: file.originalname,
|
|
hash: fileName,
|
|
folderId: formData.folderId,
|
|
userId: userId,
|
|
isAdultContent: Boolean(options.isAdultContent),
|
|
});
|
|
} catch (error) {
|
|
throw new Error(`Failed to create image record: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
async saveImageVisibilities(imageId, visibilities) {
|
|
if (typeof visibilities === 'string') {
|
|
visibilities = JSON.parse(visibilities);
|
|
}
|
|
if (!visibilities || !Array.isArray(visibilities)) {
|
|
throw new Error('Invalid visibilities provided');
|
|
}
|
|
|
|
try {
|
|
const visibilityPromises = visibilities.map(visibilityId => {
|
|
return ImageImageVisibility.create({
|
|
imageId: imageId,
|
|
visibilityTypeId: visibilityId
|
|
});
|
|
});
|
|
|
|
await Promise.all(visibilityPromises);
|
|
} catch (error) {
|
|
throw new Error(`Failed to save image visibilities: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
async getImage(imageId) {
|
|
const image = await Image.findByPk(imageId);
|
|
if (!image) throw new Error('Image not found');
|
|
if (image.isAdultContent) throw new Error('Access denied');
|
|
await this.checkUserAccess(image.userId);
|
|
return image;
|
|
}
|
|
|
|
async loadUserByHash(hashedId) {
|
|
console.log('Loading user by hashedId:', hashedId);
|
|
return await User.findOne({ where: { hashedId: hashedId } });
|
|
}
|
|
|
|
async loadUserByName(userName) {
|
|
return await User.findOne({ username: userName});
|
|
}
|
|
|
|
validateFolderData(data) {
|
|
if (!data.name || typeof data.name !== 'string') throw new Error('Invalid folder data: Name is required');
|
|
}
|
|
|
|
validateImageData(imageData) {
|
|
if (!imageData.url || typeof imageData.url !== 'string') throw new Error('Invalid image data: URL is required');
|
|
}
|
|
|
|
buildSearchWhereClause(username) {
|
|
const whereClause = { active: true, searchable: true };
|
|
if (username) {
|
|
whereClause.username = { [Op.iLike]: `%${username}%` };
|
|
}
|
|
return whereClause;
|
|
}
|
|
|
|
getUserParamsInclude() {
|
|
return [
|
|
{
|
|
model: UserParam,
|
|
as: 'user_params',
|
|
include: [{ model: UserParamType, as: 'paramType', required: true }]
|
|
}
|
|
];
|
|
}
|
|
|
|
async filterUsersByCriteria(users, ageFrom, ageTo, genders) {
|
|
const results = [];
|
|
for (const user of users) {
|
|
const userDetails = await this.extractUserDetails(user);
|
|
if (this.isUserValid(userDetails, ageFrom, ageTo, genders)) {
|
|
results.push(userDetails);
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
async extractUserDetails(user) {
|
|
const birthdateParam = user.user_params.find(param => param.paramType.description === 'birthdate');
|
|
const genderParam = user.user_params.find(param => param.paramType.description === 'gender');
|
|
const age = birthdateParam ? this.calculateAge(birthdateParam.value) : null;
|
|
const gender = genderParam ? await this.getGenderValue(genderParam.value) : null;
|
|
return {
|
|
id: user.hashedId,
|
|
username: user.username,
|
|
email: user.email,
|
|
gender,
|
|
age
|
|
};
|
|
}
|
|
|
|
async getGenderValue(genderId) {
|
|
const genderValue = await UserParamValue.findOne({ where: { id: genderId } });
|
|
return genderValue ? genderValue.value : null;
|
|
}
|
|
|
|
isUserValid(userDetails, ageFrom, ageTo, genders) {
|
|
const { age, gender } = userDetails;
|
|
const isWithinAgeRange = (!ageFrom || age >= ageFrom) && (!ageTo || age <= ageTo);
|
|
const isGenderValid = !genders || !genders.length || (gender && genders.includes(gender));
|
|
return isWithinAgeRange && isGenderValid && age >= 14;
|
|
}
|
|
|
|
async fetchUserProfile(hashedUserId) {
|
|
return await User.findOne({
|
|
where: { hashedId: hashedUserId, active: true, searchable: true },
|
|
include: [
|
|
{
|
|
model: UserParam,
|
|
as: 'user_params',
|
|
include: [
|
|
{ model: UserParamType, as: 'paramType' },
|
|
{ model: UserParamVisibility, as: 'param_visibilities', include: [{ model: UserParamVisibilityType, as: 'visibility_type' }] }
|
|
],
|
|
order: [['order_id', 'asc']],
|
|
},
|
|
{
|
|
model: Friendship,
|
|
as: 'friendSender',
|
|
},
|
|
{
|
|
model: Friendship,
|
|
as: 'friendReceiver',
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
async constructUserProfile(user, hashedUserId) {
|
|
const userParams = {};
|
|
const requestingUser = await this.loadUserByHash(hashedUserId);
|
|
if (!requestingUser) {
|
|
throw new Error('User not found');
|
|
}
|
|
const requestingUserAge = await this.getUserAge(requestingUser.id);
|
|
for (const param of user.user_params) {
|
|
const visibility = param.param_visibilities?.[0]?.visibility_type?.description || 'Invisible';
|
|
if (visibility === 'Invisible') continue;
|
|
if (this.isVisibleToUser(visibility, requestingUserAge)) {
|
|
userParams[param.paramType.description] = {
|
|
type: param.paramType.datatype,
|
|
value: await this.getParamValue(param)
|
|
};
|
|
}
|
|
}
|
|
let friendship = null;
|
|
if (user.friendSender && user.friendSender.length > 0) {
|
|
friendship = {
|
|
isSender: true,
|
|
accepted: user.friendSender[0].dataValues.accepted,
|
|
denied: user.friendSender[0].dataValues.denied,
|
|
withdrawn: user.friendSender[0].dataValues.withdrawn,
|
|
}
|
|
} else if (user.friendReceiver && user.friendReceiver.length > 0) {
|
|
friendship = {
|
|
isSender: false,
|
|
accepted: user.friendReceiver[0].dataValues.accepted,
|
|
denied: user.friendReceiver[0].dataValues.denied,
|
|
withdrawn: user.friendReceiver[0].dataValues.withdrawn,
|
|
}
|
|
}
|
|
return {
|
|
username: user.username,
|
|
registrationDate: user.registrationDate,
|
|
friendship: friendship,
|
|
params: userParams
|
|
};
|
|
}
|
|
|
|
async getUserAge(userId) {
|
|
const params = await this.getUserParams(userId, ['birthdate']);
|
|
const birthdateParam = params.find(param => param.paramType.description === 'birthdate');
|
|
return birthdateParam ? this.calculateAge(birthdateParam.value) : 0;
|
|
}
|
|
|
|
isVisibleToUser(visibility, requestingUserAge) {
|
|
return visibility === 'All' ||
|
|
(visibility === 'FriendsAndAdults' && requestingUserAge >= 18) ||
|
|
(visibility === 'AdultsOnly' && requestingUserAge >= 18);
|
|
}
|
|
|
|
async getParamValue(param) {
|
|
let paramValue = param.value;
|
|
try {
|
|
const parsedValue = JSON.parse(paramValue);
|
|
if (Array.isArray(parsedValue)) {
|
|
paramValue = await Promise.all(parsedValue.map(value => this.getValueFromDatabase(value, param.paramTypeId)));
|
|
} else if (/^\d+$/.test(paramValue)) {
|
|
paramValue = await this.getValueFromDatabase(paramValue, param.paramTypeId);
|
|
}
|
|
} catch (e) {
|
|
}
|
|
return paramValue;
|
|
}
|
|
|
|
async getValueFromDatabase(value, paramTypeId) {
|
|
const userParamValue = await UserParamValue.findOne({
|
|
where: { id: parseInt(value, 10), userParamTypeId: paramTypeId }
|
|
});
|
|
return userParamValue ? userParamValue.value : value;
|
|
}
|
|
|
|
async getPossibleImageVisibilities() {
|
|
return await ImageVisibilityType.findAll();
|
|
}
|
|
|
|
async getImageFilePath(hashedUserId, hash) {
|
|
const image = await Image.findOne({ where: { hash } });
|
|
if (!image) {
|
|
throw new Error('Image not found');
|
|
}
|
|
if (image.isAdultContent) {
|
|
throw new Error('Access denied');
|
|
}
|
|
const userId = await this.checkUserAccess(hashedUserId);
|
|
const hasAccess = await this.checkUserImageAccess(userId, image.id);
|
|
if (!hasAccess) {
|
|
throw new Error('Access denied');
|
|
}
|
|
const imagePath = this.buildFilePath(image.hash, 'user');
|
|
if (!fs.existsSync(imagePath)) {
|
|
throw new Error(`File "${imagePath}" not found`);
|
|
}
|
|
return imagePath;
|
|
}
|
|
|
|
async getAdultFolders(hashedId) {
|
|
const userId = await this.requireAdultAreaAccessByHash(hashedId);
|
|
const rootFolder = await this.ensureAdultRootFolder(userId);
|
|
const children = await this.getSubFolders(rootFolder.id, userId, true);
|
|
const data = rootFolder.get();
|
|
data.visibilityTypeIds = data.image_visibility_types.map(v => v.id);
|
|
delete data.image_visibility_types;
|
|
data.children = children;
|
|
return data;
|
|
}
|
|
|
|
async getAdultFolderImageList(hashedId, folderId) {
|
|
const userId = await this.requireAdultAreaAccessByHash(hashedId);
|
|
const folder = await Folder.findOne({
|
|
where: { id: folderId, userId, isAdultArea: true }
|
|
});
|
|
if (!folder) {
|
|
throw new Error('Folder not found');
|
|
}
|
|
return await Image.findAll({
|
|
where: {
|
|
folderId: folder.id,
|
|
isAdultContent: true,
|
|
userId
|
|
},
|
|
order: [['title', 'asc']]
|
|
});
|
|
}
|
|
|
|
async createAdultFolder(hashedId, data, folderId) {
|
|
await this.requireAdultAreaAccessByHash(hashedId);
|
|
if (!data.parentId) {
|
|
const userId = await this.checkUserAccess(hashedId);
|
|
const adultRoot = await this.ensureAdultRootFolder(userId);
|
|
data.parentId = adultRoot.id;
|
|
}
|
|
return this.createFolder(hashedId, data, folderId, { isAdultArea: true });
|
|
}
|
|
|
|
async uploadAdultImage(hashedId, file, formData) {
|
|
const userId = await this.requireAdultAreaAccessByHash(hashedId);
|
|
await this.ensureAdultUploadsAllowed(userId);
|
|
const folder = await Folder.findOne({
|
|
where: {
|
|
id: formData.folderId,
|
|
userId,
|
|
isAdultArea: true
|
|
}
|
|
});
|
|
if (!folder) {
|
|
throw new Error('Folder not found');
|
|
}
|
|
const processedImageName = await this.processAndUploadUserImage(file, 'erotic');
|
|
const newImage = await this.createImageRecord(formData, userId, file, processedImageName, { isAdultContent: true });
|
|
await this.saveImageVisibilities(newImage.id, formData.visibility);
|
|
return newImage;
|
|
}
|
|
|
|
async getAdultImageFilePath(hashedId, hash) {
|
|
const userId = await this.requireAdultAreaAccessByHash(hashedId);
|
|
const image = await Image.findOne({
|
|
where: {
|
|
hash,
|
|
userId,
|
|
isAdultContent: true
|
|
}
|
|
});
|
|
if (!image) {
|
|
throw new Error('Image not found');
|
|
}
|
|
if (image.isModeratedHidden) {
|
|
throw new Error('Image hidden by moderation');
|
|
}
|
|
const imagePath = this.buildFilePath(image.hash, 'erotic');
|
|
if (!fs.existsSync(imagePath)) {
|
|
throw new Error(`File "${imagePath}" not found`);
|
|
}
|
|
return imagePath;
|
|
}
|
|
|
|
async listEroticVideos(hashedId) {
|
|
const userId = await this.requireAdultAreaAccessByHash(hashedId);
|
|
return await EroticVideo.findAll({
|
|
where: { userId },
|
|
order: [['createdAt', 'DESC']]
|
|
});
|
|
}
|
|
|
|
async uploadEroticVideo(hashedId, file, formData) {
|
|
const userId = await this.requireAdultAreaAccessByHash(hashedId);
|
|
await this.ensureAdultUploadsAllowed(userId);
|
|
if (!file) {
|
|
throw new Error('Video file is required');
|
|
}
|
|
const allowedMimeTypes = ['video/mp4', 'video/webm', 'video/ogg', 'video/quicktime'];
|
|
if (!allowedMimeTypes.includes(file.mimetype)) {
|
|
throw new Error('Unsupported video format');
|
|
}
|
|
|
|
const fileName = this.generateUniqueFileName(file.originalname);
|
|
const filePath = this.buildFilePath(fileName, 'erotic-video');
|
|
await this.saveFile(file.buffer, filePath);
|
|
|
|
return await EroticVideo.create({
|
|
title: formData.title || file.originalname,
|
|
description: formData.description || null,
|
|
originalFileName: file.originalname,
|
|
hash: fileName,
|
|
mimeType: file.mimetype,
|
|
userId
|
|
});
|
|
}
|
|
|
|
async getEroticVideoFilePath(hashedId, hash) {
|
|
const userId = await this.requireAdultAreaAccessByHash(hashedId);
|
|
const video = await EroticVideo.findOne({
|
|
where: { hash, userId }
|
|
});
|
|
if (!video) {
|
|
throw new Error('Video not found');
|
|
}
|
|
if (video.isModeratedHidden) {
|
|
throw new Error('Video hidden by moderation');
|
|
}
|
|
const videoPath = this.buildFilePath(video.hash, 'erotic-video');
|
|
if (!fs.existsSync(videoPath)) {
|
|
throw new Error(`File "${videoPath}" not found`);
|
|
}
|
|
return { filePath: videoPath, mimeType: video.mimeType };
|
|
}
|
|
|
|
async createEroticContentReport(hashedId, payload) {
|
|
const reporterId = await this.requireAdultAreaAccessByHash(hashedId);
|
|
const targetType = String(payload.targetType || '').trim().toLowerCase();
|
|
const targetId = Number(payload.targetId);
|
|
const reason = String(payload.reason || '').trim().toLowerCase();
|
|
const note = payload.note ? String(payload.note).trim() : null;
|
|
|
|
if (!['image', 'video'].includes(targetType) || !Number.isInteger(targetId) || targetId <= 0) {
|
|
throw new Error('Invalid report target');
|
|
}
|
|
if (!['suspected_minor', 'non_consensual', 'violence', 'harassment', 'spam', 'other'].includes(reason)) {
|
|
throw new Error('Invalid report reason');
|
|
}
|
|
|
|
const { ownerId } = await this.resolveEroticTarget(targetType, targetId);
|
|
if (ownerId === reporterId) {
|
|
throw new Error('Own content cannot be reported');
|
|
}
|
|
|
|
const existingOpen = await EroticContentReport.findOne({
|
|
where: {
|
|
reporterId,
|
|
targetType,
|
|
targetId,
|
|
status: 'open'
|
|
}
|
|
});
|
|
if (existingOpen) {
|
|
return existingOpen;
|
|
}
|
|
|
|
return await EroticContentReport.create({
|
|
reporterId,
|
|
targetType,
|
|
targetId,
|
|
reason,
|
|
note,
|
|
status: 'open'
|
|
});
|
|
}
|
|
|
|
// Public variant used by blog: allow access if the image's folder is visible to 'everyone'.
|
|
async getImageFilePathPublicByHash(hash) {
|
|
const image = await Image.findOne({ where: { hash } });
|
|
if (!image) {
|
|
throw new Error('Image not found');
|
|
}
|
|
const folder = await Folder.findOne({ where: { id: image.folderId } });
|
|
if (!folder) {
|
|
throw new Error('Folder not found');
|
|
}
|
|
const everyone = await ImageVisibilityType.findOne({ where: { description: 'everyone' } });
|
|
if (!everyone) {
|
|
throw new Error('Visibility type not found');
|
|
}
|
|
const hasEveryone = await FolderImageVisibility.findOne({ where: { folderId: folder.id, visibilityTypeId: everyone.id } });
|
|
if (!hasEveryone) {
|
|
const err = new Error('Access denied');
|
|
err.status = 403;
|
|
throw err;
|
|
}
|
|
const imagePath = this.buildFilePath(image.hash, 'user');
|
|
if (!fs.existsSync(imagePath)) {
|
|
throw new Error(`File "${imagePath}" not found`);
|
|
}
|
|
return imagePath;
|
|
}
|
|
|
|
async checkUserImageAccess(userId, imageId) {
|
|
const image = await Image.findByPk(imageId);
|
|
if (image.userId === userId) {
|
|
return true;
|
|
}
|
|
const accessRules = await ImageImageVisibility.findAll({
|
|
where: { imageId }
|
|
});
|
|
return accessRules.some(rule => {
|
|
return false;
|
|
});
|
|
}
|
|
|
|
async changeImage(hashedUserId, imageId, title, visibilities) {
|
|
const userId = await this.checkUserAccess(hashedUserId);
|
|
await this.checkUserImageAccess(userId, imageId);
|
|
const image = await Image.findOne({ where: { id: imageId, isAdultContent: false } });
|
|
if (!image) {
|
|
throw new Error('image not found')
|
|
}
|
|
await image.update({ title: title });
|
|
await ImageImageVisibility.destroy({ where: { imageId } });
|
|
for (const visibility of visibilities) {
|
|
await ImageImageVisibility.create({ imageId, visibilityTypeId: visibility.id });
|
|
}
|
|
return image.folderId;
|
|
}
|
|
|
|
async changeAdultImage(hashedUserId, imageId, title, visibilities) {
|
|
const userId = await this.requireAdultAreaAccessByHash(hashedUserId);
|
|
const image = await Image.findOne({
|
|
where: {
|
|
id: imageId,
|
|
userId,
|
|
isAdultContent: true
|
|
}
|
|
});
|
|
if (!image) {
|
|
throw new Error('image not found');
|
|
}
|
|
await image.update({ title });
|
|
await ImageImageVisibility.destroy({ where: { imageId } });
|
|
for (const visibility of visibilities) {
|
|
await ImageImageVisibility.create({ imageId, visibilityTypeId: visibility.id });
|
|
}
|
|
return image.folderId;
|
|
}
|
|
|
|
async getFoldersByUsername(username, hashedUserId) {
|
|
const user = await this.loadUserByName(username);
|
|
if (!user) {
|
|
throw new Error('User not found');
|
|
}
|
|
const requestingUserId = await this.checkUserAccess(hashedUserId);
|
|
let rootFolder = await Folder.findOne({ where: { parentId: null, userId: user.id, isAdultArea: false } });
|
|
if (!rootFolder) {
|
|
return null;
|
|
}
|
|
const accessibleFolders = await this.getAccessibleFolders(rootFolder.id, requestingUserId);
|
|
rootFolder = rootFolder.get();
|
|
rootFolder.children = accessibleFolders;
|
|
return rootFolder;
|
|
}
|
|
|
|
async getAccessibleFolders(folderId, requestingUserId) {
|
|
const folderIdString = String(folderId);
|
|
const requestingUserIdString = String(requestingUserId);
|
|
const requestingUser = await User.findOne({ where: { id: requestingUserIdString } });
|
|
const isAdult = await this.isUserAdult(requestingUser.id);
|
|
const accessibleFolders = await Folder.findAll({
|
|
where: { parentId: folderIdString, isAdultArea: false },
|
|
include: [
|
|
{
|
|
model: ImageVisibilityType,
|
|
through: { model: FolderImageVisibility },
|
|
where: {
|
|
[Op.or]: [
|
|
{ description: 'everyone' },
|
|
{ description: 'adults', ...(isAdult ? {} : { [Op.not]: null }) },
|
|
{ description: 'friends-and-adults', ...(isAdult ? {} : { [Op.not]: null }) }
|
|
]
|
|
},
|
|
required: false
|
|
},
|
|
{
|
|
model: ImageVisibilityUser,
|
|
through: { model: FolderVisibilityUser },
|
|
where: { user_id: requestingUserIdString },
|
|
required: false
|
|
}
|
|
]
|
|
});
|
|
const folderList = [];
|
|
for (let folder of accessibleFolders) {
|
|
const children = await this.getAccessibleFolders(folder.id, requestingUserIdString);
|
|
folder = folder.get();
|
|
folder.children = children;
|
|
folderList.push(folder);
|
|
}
|
|
return folderList;
|
|
}
|
|
|
|
async deleteFolder(hashedUserId, folderId) {
|
|
const userId = await this.checkUserAccess(hashedUserId);
|
|
const folder = await Folder.findOne({
|
|
where: { id: folderId, userId }
|
|
});
|
|
if (!folder) {
|
|
throw new Error('Folder not found or access denied');
|
|
}
|
|
await FolderImageVisibility.destroy({ where: { folderId: folder.id } });
|
|
await folder.destroy();
|
|
return true;
|
|
}
|
|
|
|
async createGuestbookEntry(hashedSenderId, recipientName, htmlContent, image) {
|
|
const sender = await this.loadUserByHash(hashedSenderId);
|
|
if (!sender) {
|
|
throw new Error('Sender not found');
|
|
}
|
|
const recipient = await this.loadUserByName(recipientName);
|
|
if (!recipient) {
|
|
throw new Error('Recipient not found');
|
|
}
|
|
const sanitizedContent = this.sanitizeHtml(htmlContent);
|
|
let uploadedImage = null;
|
|
if (image) {
|
|
uploadedImage = await this.processAndUploadGuestbookImage(image);
|
|
}
|
|
const entry = await GuestbookEntry.create({
|
|
senderId: sender.id,
|
|
recipientId: recipient.id,
|
|
senderUsername: sender.username,
|
|
contentHtml: sanitizedContent,
|
|
imageUrl: uploadedImage
|
|
});
|
|
|
|
return entry;
|
|
}
|
|
|
|
sanitizeHtml(htmlContent) {
|
|
const window = new JSDOM('').window;
|
|
const purify = DOMPurify(window);
|
|
const cleanHtml = purify.sanitize(htmlContent, {
|
|
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'ul', 'ol', 'li', 'br'],
|
|
ALLOWED_ATTR: ['href', 'title'],
|
|
FORBID_TAGS: ['script', 'style', 'iframe', 'img'],
|
|
ALLOWED_URI_REGEXP: /^https?:\/\//,
|
|
});
|
|
|
|
return cleanHtml;
|
|
}
|
|
|
|
async processAndUploadGuestbookImage(image) {
|
|
try {
|
|
const img = sharp(image.buffer);
|
|
const metadata = await img.metadata();
|
|
if (!metadata || !['jpeg', 'png', 'webp', 'gif'].includes(metadata.format)) {
|
|
throw new Error('File is not a valid image');
|
|
}
|
|
if (metadata.width < 20 || metadata.height < 20) {
|
|
throw new Error('Image dimensions are too small. Minimum size is 20x20 pixels.');
|
|
}
|
|
const resizedImg = img.resize({
|
|
width: Math.min(metadata.width, 500),
|
|
height: Math.min(metadata.height, 500),
|
|
fit: sharp.fit.inside,
|
|
withoutEnlargement: true
|
|
});
|
|
const newFileName = this.generateUniqueFileName(image.originalname);
|
|
const filePath = this.buildFilePath(newFileName, 'guestbook');
|
|
await resizedImg.toFile(filePath);
|
|
return newFileName;
|
|
} catch (error) {
|
|
throw new Error(`Failed to process image: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
async getGuestbookEntries(hashedUserId, username, page = 1) {
|
|
const pageSize = 20;
|
|
const offset = (page - 1) * pageSize;
|
|
this.checkUserAccess(hashedUserId);
|
|
const user = await this.loadUserByName(username);
|
|
if (!user) {
|
|
throw new Error('User not found');
|
|
}
|
|
const entries = await GuestbookEntry.findAndCountAll({
|
|
where: { recipientId: user.id },
|
|
include: [
|
|
{ model: User, as: 'sender', attributes: ['username'] },
|
|
],
|
|
limit: pageSize,
|
|
offset: offset,
|
|
order: [['createdAt', 'DESC']]
|
|
});
|
|
const resultList = entries.rows.map(entry => ({
|
|
id: entry.id,
|
|
sender: entry.sender ? entry.sender.username : entry.senderUsername,
|
|
contentHtml: entry.contentHtml,
|
|
withImage: entry.imageUrl !== null,
|
|
createdAt: entry.createdAt
|
|
}));
|
|
return {
|
|
entries: resultList,
|
|
currentPage: page,
|
|
totalPages: Math.ceil(entries.count / pageSize),
|
|
};
|
|
}
|
|
|
|
async getGuestbookImageFilePath(hashedUserId, guestbookOwnerName, entryId) {
|
|
await this.checkUserAccess(hashedUserId);
|
|
|
|
const guestbookOwner = await this.loadUserByName(guestbookOwnerName);
|
|
if (!guestbookOwner) {
|
|
throw new Error('usernotfound');
|
|
}
|
|
const entry = await GuestbookEntry.findOne({
|
|
where: { id: entryId, recipientId: guestbookOwner.id },
|
|
});
|
|
if (!entry) {
|
|
throw new Error('entrynotfound');
|
|
}
|
|
if (!entry.imageUrl) {
|
|
console.log(entry);
|
|
throw new Error('entryhasnoimage');
|
|
}
|
|
console.log(`Image URL: ${entry.imageUrl}`);
|
|
const imagePath = this.buildFilePath(entry.imageUrl, 'guestbook');
|
|
if (!fs.existsSync(imagePath)) {
|
|
throw new Error(imagenotfound);
|
|
}
|
|
return imagePath;
|
|
}
|
|
|
|
async deleteGuestbookEntry(hashedUserId, entryId) {
|
|
const user = await this.loadUserByHash(hashedUserId);
|
|
if (!user) {
|
|
throw new Error('User not found');
|
|
}
|
|
const entry = await GuestbookEntry.findOne({ where: { id: entryId } });
|
|
if (!entry) {
|
|
throw new Error('Entry not found');
|
|
}
|
|
if (entry.senderId !== user.id && entry.recipientId !== user.id) {
|
|
throw new Error('Not authorized to delete this entry');
|
|
}
|
|
await entry.destroy();
|
|
}
|
|
|
|
async createDiaryEntry(hashedUserId, text) {
|
|
const userId = await this.checkUserAccess(hashedUserId);
|
|
const newEntry = await Diary.create({
|
|
userId: userId,
|
|
text: text,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
});
|
|
return newEntry;
|
|
}
|
|
|
|
async updateDiaryEntry(diaryEntryId, hashedUserId, newText) {
|
|
const userId = await this.checkUserAccess(hashedUserId);
|
|
const existingEntry = await Diary.findOne({
|
|
where: { id: diaryEntryId, userId: userId }
|
|
});
|
|
if (!existingEntry) {
|
|
throw new Error('Diary entry not found or unauthorized access');
|
|
}
|
|
existingEntry.text = newText;
|
|
existingEntry.updatedAt = new Date();
|
|
await existingEntry.save();
|
|
return existingEntry;
|
|
}
|
|
|
|
async deleteDiaryEntry(diaryEntryId, hashedUserId) {
|
|
const userId = await this.checkUserAccess(hashedUserId);
|
|
const entryToDelete = await Diary.findOne({
|
|
where: { id: diaryEntryId, userId: userId }
|
|
});
|
|
if (!entryToDelete) {
|
|
throw new Error('Diary entry not found or unauthorized access');
|
|
}
|
|
await entryToDelete.destroy();
|
|
return true;
|
|
}
|
|
|
|
async getDiaryEntries(hashedUserId, page) {
|
|
const userId = await this.checkUserAccess(hashedUserId);
|
|
const entries = await Diary.findAndCountAll({
|
|
where: { userId: userId },
|
|
order: [['createdAt', 'DESC']],
|
|
offset: (page - 1) * 20,
|
|
limit: 20,
|
|
});
|
|
return { entries: entries.rows, totalPages: Math.ceil(entries.count / 20) };
|
|
}
|
|
|
|
async addFriend(hashedUserid, friendUserid) {
|
|
console.log('--------', friendUserid, hashedUserid);
|
|
const requestingUserId = await this.checkUserAccess(hashedUserid);
|
|
const friend = await User.findOne({ where: { hashedId: friendUserid } });
|
|
if (!friend) {
|
|
throw new Error('notfound');
|
|
}
|
|
const friendship = await Friendship.findOne({
|
|
where: {
|
|
[Op.or]: [
|
|
{ user1Id: requestingUserId, user2Id: friend.id },
|
|
{ user1Id: friend.id, user2Id: requestingUserId }
|
|
]
|
|
}
|
|
});
|
|
console.log('friendship', friend, requestingUserId);
|
|
if (friendship) {
|
|
if (friendship.withdrawn && friendship.user1Id === requestingUserId) {
|
|
friendship.update({ withdrawn: false });
|
|
} else {
|
|
throw new Error('alreadyexists');
|
|
}
|
|
} else {
|
|
await Friendship.create({ user1Id: requestingUserId, user2Id: friend.id });
|
|
}
|
|
return { accepted: false, withdrawn: false, denied: false };
|
|
}
|
|
|
|
async removeFriend(hashedUserid, friendUserid) {
|
|
const requestingUserId = await this.checkUserAccess(hashedUserid);
|
|
const friend = await this.loadUserByHash(friendUserid);
|
|
if (!friend) {
|
|
throw new Error('notfound');
|
|
}
|
|
const friendship = await Friendship.findOne({
|
|
where: {
|
|
[Op.or]: [
|
|
{ user1Id: requestingUserId, user2Id: friend.id },
|
|
{ user1Id: friend.id, user2Id: requestingUserId }
|
|
]
|
|
}
|
|
});
|
|
if (!friendship) {
|
|
throw new Error('notfound');
|
|
}
|
|
if (friendship.user1Id === requestingUserId) {
|
|
friendship.update({ withdrawn: true })
|
|
} else {
|
|
friendship.update({ denied: true });
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async acceptFriendship(hashedUserid, friendUserid) {
|
|
const requestingUserId = await this.checkUserAccess(hashedUserid);
|
|
const friend = await this.loadUserByHash(friendUserid);
|
|
if (!friend) {
|
|
throw new Error('notfound');
|
|
}
|
|
const friendship = await Friendship.findOne({
|
|
where: {
|
|
[Op.or]: [
|
|
{ user1Id: requestingUserId, user2Id: friend.id },
|
|
{ user1Id: friend.id, user2Id: requestingUserId }
|
|
]
|
|
}
|
|
});
|
|
if (!friendship) {
|
|
throw new Error('notfound');
|
|
}
|
|
if (friendship.user1Id === requestingUserId && friendship.withdrawn) {
|
|
friendship.update({ withdrawn: false });
|
|
} else if (friendship.user2Id === requestingUserId && friendship.denied) {
|
|
friendship.update({ denied: false, accepted: true });
|
|
} else {
|
|
throw new Error('notfound');
|
|
}
|
|
}
|
|
|
|
async getLoggedInFriends(hashedUserId) {
|
|
const userId = await this.checkUserAccess(hashedUserId);
|
|
const activeFriendships = await Friendship.findAll({
|
|
where: {
|
|
accepted: true,
|
|
denied: false,
|
|
withdrawn: false,
|
|
[Op.or]: [
|
|
{ user1Id: userId },
|
|
{ user2Id: userId }
|
|
]
|
|
}
|
|
});
|
|
const friendIds = activeFriendships.map(friendship =>
|
|
friendship.user1Id === userId ? friendship.user2Id : friendship.user1Id
|
|
);
|
|
const loggedInFriends = [];
|
|
for (const friendId of friendIds) {
|
|
const session = await getUserSession(friendId);
|
|
if (session && session.id) {
|
|
const friend = await User.findOne({ where: { hashedId: session.id } });
|
|
if (friend) {
|
|
loggedInFriends.push({
|
|
id: friend.hashedId,
|
|
username: friend.username,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return loggedInFriends;
|
|
}
|
|
}
|
|
export default SocialNetworkService;
|