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 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 path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); class SocialNetworkService extends BaseService { async searchUsers({ username, ageFrom, ageTo, genders }) { const whereClause = this.buildSearchWhereClause(username); const users = await User.findAll({ where: whereClause, include: this.getUserParamsInclude() }); return 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) { await this.checkUserAccess(hashedUserId); const user = await User.findOne({ hashedId: hashedUserId }); const parentFolder = Folder.findOne({ id: data.parentId, userId: user.id }); if (!parentFolder) { throw new Error('foldernotfound'); } const newFolder = await Folder.create({ parentId: data.parentId, userId: user.id, name: data.name }); 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); let rootFolder = await Folder.findOne({ where: { parentId: null, userId } }); if (!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, visibilityTypeId: visibility.id }); } const children = await this.getSubFolders(rootFolder.id, userId); rootFolder = rootFolder.get(); rootFolder.children = children; return rootFolder; } async getSubFolders(parentId, userId) { const folders = await Folder.findAll({ where: { parentId, userId } }); for (const folder of folders) { const children = await this.getSubFolders(folder.id, userId); folder.setDataValue('children', children); } 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 } }); if (!folder) throw new Error('Folder not found'); return await Image.findAll({ where: { folderId: folder.id }, order: [ ['title', 'asc'] ] }); } async uploadImage(hashedId, file, formData) { const userId = await this.getUserId(hashedId); const newFileName = this.generateUniqueFileName(file.originalname); const filePath = this.buildFilePath(newFileName); await this.saveFile(file.buffer, filePath); const newImage = await this.createImageRecord(formData, userId, file, newFileName); await this.saveImageVisibilities(newImage.id, formData.visibility); return newImage; } async getUserId(hashedId) { return await this.checkUserAccess(hashedId); } generateUniqueFileName(originalFileName) { const uniqueHash = uuidv4(); return `${uniqueHash}`; } buildFilePath(fileName) { const userImagesPath = path.join(__dirname, '../images/user'); return path.join(userImagesPath, fileName); } async saveFile(buffer, filePath) { try { await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, buffer); } catch (error) { throw new Error(`Failed to save file: ${error.message}`); } } async createImageRecord(formData, userId, file, fileName) { try { return await Image.create({ title: formData.title, description: formData.description || null, originalFileName: file.originalname, hash: fileName, folderId: formData.folderId, userId: userId, }); } 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'); await this.checkUserAccess(image.userId); return image; } async checkUserAccess(hashedId) { const user = await User.findOne({ hashedId: hashedId }); if (!user || !user.active) throw new Error('Access denied: User not found or inactive'); return user.id; } 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 }] } ]; } filterUsersByCriteria(users, ageFrom, ageTo, genders) { const results = []; for (const user of users) { const userDetails = this.extractUserDetails(user); if (this.isUserValid(userDetails, ageFrom, ageTo, genders)) { results.push(userDetails); } } return results; } 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 ? 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']] } ] }); } async constructUserProfile(user, requestingUserId) { const userParams = {}; const requestingUserAge = await this.getUserAge(requestingUserId); 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) }; } } return { username: user.username, registrationDate: user.registrationDate, 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'); } 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); if (!fs.existsSync(imagePath)) { throw new Error('File 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 } }); 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; } } export default SocialNetworkService;