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;