- Added functionality to manage selected users for adult folders and erotic videos, allowing for more granular visibility control. - Introduced new endpoints and methods in the SocialNetworkController and SocialNetworkService to handle selected users. - Updated the frontend components to include input fields for selected users in CreateFolderDialog, EditImageDialog, and EroticPicturesView. - Enhanced the routing to support fetching erotic folders and videos by username, improving user experience in profile views.
1735 lines
64 KiB
JavaScript
1735 lines
64 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 EroticVideoImageVisibility from '../models/community/erotic_video_image_visibility.js';
|
|
import EroticVideoVisibilityUser from '../models/community/erotic_video_visibility_user.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;
|
|
}
|
|
}
|
|
|
|
parseSelectedUsers(selectedUsers) {
|
|
if (!selectedUsers) return [];
|
|
if (Array.isArray(selectedUsers)) {
|
|
return selectedUsers.map(value => String(value || '').trim()).filter(Boolean);
|
|
}
|
|
if (typeof selectedUsers === 'string') {
|
|
try {
|
|
const parsed = JSON.parse(selectedUsers);
|
|
if (Array.isArray(parsed)) {
|
|
return parsed.map(value => String(value || '').trim()).filter(Boolean);
|
|
}
|
|
} catch (error) {
|
|
// Fallback to comma-separated values below.
|
|
}
|
|
return selectedUsers
|
|
.split(',')
|
|
.map(value => value.trim())
|
|
.filter(Boolean);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
async getActiveFriendIds(userId) {
|
|
const friendships = await Friendship.findAll({
|
|
where: {
|
|
accepted: true,
|
|
denied: false,
|
|
withdrawn: false,
|
|
[Op.or]: [
|
|
{ user1Id: userId },
|
|
{ user2Id: userId }
|
|
]
|
|
}
|
|
});
|
|
return friendships.map(friendship => (
|
|
friendship.user1Id === userId ? friendship.user2Id : friendship.user1Id
|
|
));
|
|
}
|
|
|
|
async areUsersFriends(userId, otherUserId) {
|
|
if (!userId || !otherUserId) return false;
|
|
const friendship = await Friendship.findOne({
|
|
where: {
|
|
accepted: true,
|
|
denied: false,
|
|
withdrawn: false,
|
|
[Op.or]: [
|
|
{ user1Id: userId, user2Id: otherUserId },
|
|
{ user1Id: otherUserId, user2Id: userId }
|
|
]
|
|
}
|
|
});
|
|
return Boolean(friendship);
|
|
}
|
|
|
|
async resolveSelectedUserIds(ownerId, selectedUsers, { adultOnly = false } = {}) {
|
|
const usernames = [...new Set(this.parseSelectedUsers(selectedUsers))];
|
|
if (!usernames.length) {
|
|
return [];
|
|
}
|
|
|
|
const users = await User.findAll({
|
|
where: {
|
|
[Op.or]: usernames.map(username => ({
|
|
username: {
|
|
[Op.iLike]: username
|
|
}
|
|
}))
|
|
},
|
|
attributes: ['id', 'username']
|
|
});
|
|
|
|
const matchedUsers = [];
|
|
for (const requestedName of usernames) {
|
|
const user = users.find(candidate => (
|
|
String(candidate.username || '').toLowerCase() === requestedName.toLowerCase()
|
|
));
|
|
if (!user) {
|
|
throw new Error(`User "${requestedName}" not found`);
|
|
}
|
|
if (user.id === ownerId) {
|
|
continue;
|
|
}
|
|
if (adultOnly) {
|
|
const access = await this.getAdultAccessState(user.id);
|
|
if (!access.adultAccessEnabled) {
|
|
throw new Error(`User "${user.username}" is not approved for the adult area`);
|
|
}
|
|
}
|
|
matchedUsers.push(user.id);
|
|
}
|
|
|
|
return [...new Set(matchedUsers)];
|
|
}
|
|
|
|
async saveFolderSelectedUsers(folderId, selectedUsers, ownerId, { adultOnly = false } = {}) {
|
|
await FolderVisibilityUser.destroy({ where: { folderId } });
|
|
const selectedUserIds = await this.resolveSelectedUserIds(ownerId, selectedUsers, { adultOnly });
|
|
for (const userId of selectedUserIds) {
|
|
await FolderVisibilityUser.create({ folderId, visibilityUserId: userId });
|
|
}
|
|
}
|
|
|
|
async saveImageSelectedUsers(imageId, selectedUsers, ownerId, { adultOnly = false } = {}) {
|
|
await ImageVisibilityUser.destroy({ where: { imageId } });
|
|
const selectedUserIds = await this.resolveSelectedUserIds(ownerId, selectedUsers, { adultOnly });
|
|
for (const userId of selectedUserIds) {
|
|
await ImageVisibilityUser.create({ imageId, userId });
|
|
}
|
|
}
|
|
|
|
async getFolderSelectedUsernames(folderId) {
|
|
const selectedUserLinks = await FolderVisibilityUser.findAll({ where: { folderId } });
|
|
if (!selectedUserLinks.length) return [];
|
|
const users = await User.findAll({
|
|
where: { id: selectedUserLinks.map(link => link.visibilityUserId) },
|
|
attributes: ['id', 'username']
|
|
});
|
|
return users.map(user => user.username).sort((a, b) => a.localeCompare(b));
|
|
}
|
|
|
|
async getImageSelectedUsernames(imageId) {
|
|
const selectedUsers = await ImageVisibilityUser.findAll({ where: { imageId } });
|
|
if (!selectedUsers.length) return [];
|
|
const users = await User.findAll({
|
|
where: { id: selectedUsers.map(entry => entry.userId) },
|
|
attributes: ['id', 'username']
|
|
});
|
|
return users.map(user => user.username).sort((a, b) => a.localeCompare(b));
|
|
}
|
|
|
|
async saveEroticVideoSelectedUsers(videoId, selectedUsers, ownerId, { adultOnly = false } = {}) {
|
|
await EroticVideoVisibilityUser.destroy({ where: { eroticVideoId: videoId } });
|
|
const selectedUserIds = await this.resolveSelectedUserIds(ownerId, selectedUsers, { adultOnly });
|
|
for (const userId of selectedUserIds) {
|
|
await EroticVideoVisibilityUser.create({ eroticVideoId: videoId, userId });
|
|
}
|
|
}
|
|
|
|
async getEroticVideoSelectedUsernames(videoId) {
|
|
const selectedUsers = await EroticVideoVisibilityUser.findAll({ where: { eroticVideoId: videoId } });
|
|
if (!selectedUsers.length) return [];
|
|
const users = await User.findAll({
|
|
where: { id: selectedUsers.map(entry => entry.userId) },
|
|
attributes: ['id', 'username']
|
|
});
|
|
return users.map(user => user.username).sort((a, b) => a.localeCompare(b));
|
|
}
|
|
|
|
async saveEroticVideoVisibilities(videoId, visibilities) {
|
|
let normalizedVisibilities = visibilities;
|
|
if (typeof normalizedVisibilities === 'string') {
|
|
normalizedVisibilities = JSON.parse(normalizedVisibilities);
|
|
}
|
|
if (!Array.isArray(normalizedVisibilities) || !normalizedVisibilities.length) {
|
|
throw new Error('Invalid visibilities provided');
|
|
}
|
|
|
|
await EroticVideoImageVisibility.destroy({ where: { eroticVideoId: videoId } });
|
|
for (const visibility of normalizedVisibilities) {
|
|
const visibilityTypeId = typeof visibility === 'object' ? visibility.id : visibility;
|
|
await EroticVideoImageVisibility.create({ eroticVideoId: videoId, visibilityTypeId });
|
|
}
|
|
}
|
|
|
|
async getEroticVideoVisibilityEntries(videoId) {
|
|
return await ImageVisibilityType.findAll({
|
|
include: [{
|
|
model: EroticVideo,
|
|
where: { id: videoId },
|
|
attributes: [],
|
|
through: { attributes: [] }
|
|
}]
|
|
});
|
|
}
|
|
|
|
async enrichEroticVideoVisibilityMetadata(videos) {
|
|
const enrichedVideos = [];
|
|
for (const videoRecord of videos) {
|
|
const video = videoRecord.get ? videoRecord.get() : { ...videoRecord };
|
|
const visibilities = await this.getEroticVideoVisibilityEntries(video.id);
|
|
video.visibilities = visibilities.map(entry => ({ id: entry.id, description: entry.description }));
|
|
video.selectedUsers = await this.getEroticVideoSelectedUsernames(video.id);
|
|
enrichedVideos.push(video);
|
|
}
|
|
return enrichedVideos;
|
|
}
|
|
|
|
async enrichImageVisibilityMetadata(images) {
|
|
const enrichedImages = [];
|
|
for (const imageRecord of images) {
|
|
const image = imageRecord.get ? imageRecord.get() : { ...imageRecord };
|
|
const visibilities = await ImageVisibilityType.findAll({
|
|
include: [{
|
|
model: Image,
|
|
where: { id: image.id },
|
|
attributes: [],
|
|
through: { attributes: [] }
|
|
}]
|
|
});
|
|
image.visibilities = visibilities.map(entry => ({ id: entry.id, description: entry.description }));
|
|
image.selectedUsers = await this.getImageSelectedUsernames(image.id);
|
|
enrichedImages.push(image);
|
|
}
|
|
return enrichedImages;
|
|
}
|
|
|
|
async canRequesterAccessAdultFolder(folder, requesterId) {
|
|
if (!folder || !requesterId) return false;
|
|
if (folder.userId === requesterId) {
|
|
return true;
|
|
}
|
|
|
|
const adultAccess = await this.getAdultAccessState(requesterId);
|
|
if (!adultAccess.adultAccessEnabled) {
|
|
return false;
|
|
}
|
|
|
|
const folderVisibilities = await ImageVisibilityType.findAll({
|
|
include: [{
|
|
model: Folder,
|
|
where: { id: folder.id },
|
|
attributes: [],
|
|
through: { attributes: [] }
|
|
}]
|
|
});
|
|
const descriptions = folderVisibilities.map(entry => entry.description);
|
|
if (!descriptions.length) {
|
|
return false;
|
|
}
|
|
|
|
if (descriptions.includes('adults') || descriptions.includes('everyone')) {
|
|
return true;
|
|
}
|
|
|
|
if ((descriptions.includes('friends') || descriptions.includes('friends-and-adults')) &&
|
|
await this.areUsersFriends(folder.userId, requesterId)) {
|
|
return true;
|
|
}
|
|
|
|
if (descriptions.includes('selected-users')) {
|
|
const selectedLink = await FolderVisibilityUser.findOne({
|
|
where: {
|
|
folderId: folder.id,
|
|
visibilityUserId: requesterId
|
|
}
|
|
});
|
|
if (selectedLink) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
async canRequesterAccessAdultImage(image, requesterId) {
|
|
if (!image || !requesterId) return false;
|
|
if (image.userId === requesterId) {
|
|
return true;
|
|
}
|
|
if (image.isModeratedHidden) {
|
|
return false;
|
|
}
|
|
|
|
const folder = await Folder.findOne({
|
|
where: {
|
|
id: image.folderId,
|
|
userId: image.userId,
|
|
isAdultArea: true
|
|
}
|
|
});
|
|
if (!folder) {
|
|
return false;
|
|
}
|
|
|
|
const folderAccess = await this.canRequesterAccessAdultFolder(folder, requesterId);
|
|
if (!folderAccess) {
|
|
return false;
|
|
}
|
|
|
|
const adultAccess = await this.getAdultAccessState(requesterId);
|
|
if (!adultAccess.adultAccessEnabled) {
|
|
return false;
|
|
}
|
|
|
|
const imageVisibilities = await ImageVisibilityType.findAll({
|
|
include: [{
|
|
model: Image,
|
|
where: { id: image.id },
|
|
attributes: [],
|
|
through: { attributes: [] }
|
|
}]
|
|
});
|
|
const descriptions = imageVisibilities.map(entry => entry.description);
|
|
if (!descriptions.length) {
|
|
return true;
|
|
}
|
|
|
|
if (descriptions.includes('adults') || descriptions.includes('everyone')) {
|
|
return true;
|
|
}
|
|
|
|
if ((descriptions.includes('friends') || descriptions.includes('friends-and-adults')) &&
|
|
await this.areUsersFriends(image.userId, requesterId)) {
|
|
return true;
|
|
}
|
|
|
|
if (descriptions.includes('selected-users')) {
|
|
const selectedLink = await ImageVisibilityUser.findOne({
|
|
where: {
|
|
imageId: image.id,
|
|
userId: requesterId
|
|
}
|
|
});
|
|
if (selectedLink) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
async canRequesterAccessEroticVideo(video, requesterId) {
|
|
if (!video || !requesterId) return false;
|
|
if (video.userId === requesterId) {
|
|
return true;
|
|
}
|
|
if (video.isModeratedHidden) {
|
|
return false;
|
|
}
|
|
|
|
const adultAccess = await this.getAdultAccessState(requesterId);
|
|
if (!adultAccess.adultAccessEnabled) {
|
|
return false;
|
|
}
|
|
|
|
const videoVisibilities = await this.getEroticVideoVisibilityEntries(video.id);
|
|
const descriptions = videoVisibilities.map(entry => entry.description);
|
|
if (!descriptions.length) {
|
|
return false;
|
|
}
|
|
|
|
if (descriptions.includes('adults') || descriptions.includes('everyone')) {
|
|
return true;
|
|
}
|
|
|
|
if ((descriptions.includes('friends') || descriptions.includes('friends-and-adults')) &&
|
|
await this.areUsersFriends(video.userId, requesterId)) {
|
|
return true;
|
|
}
|
|
|
|
if (descriptions.includes('selected-users')) {
|
|
const selectedLink = await EroticVideoVisibilityUser.findOne({
|
|
where: {
|
|
eroticVideoId: video.id,
|
|
userId: requesterId
|
|
}
|
|
});
|
|
if (selectedLink) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
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
|
|
});
|
|
}
|
|
await this.saveFolderSelectedUsers(newFolder.id, data.selectedUsers || data.selectedUsernames || [], user.id, {
|
|
adultOnly: isAdultArea
|
|
});
|
|
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('selectedUsers', await this.getFolderSelectedUsernames(folder.id));
|
|
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');
|
|
const images = await Image.findAll({
|
|
where: {
|
|
folderId: folder.id,
|
|
isAdultContent: false
|
|
},
|
|
order: [
|
|
['title', 'asc']
|
|
]
|
|
});
|
|
return this.enrichImageVisibilityMetadata(images);
|
|
}
|
|
|
|
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);
|
|
await this.saveImageSelectedUsers(newImage.id, formData.selectedUsers || formData.selectedUsernames || [], userId);
|
|
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({
|
|
where: {
|
|
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);
|
|
data.selectedUsers = await this.getFolderSelectedUsernames(rootFolder.id);
|
|
delete data.image_visibility_types;
|
|
data.children = children;
|
|
return data;
|
|
}
|
|
|
|
async getAdultFoldersByUsername(username, hashedUserId) {
|
|
const requestingUserId = await this.requireAdultAreaAccessByHash(hashedUserId);
|
|
const owner = await this.loadUserByName(username);
|
|
if (!owner) {
|
|
throw new Error('User not found');
|
|
}
|
|
|
|
const ownerRoot = await Folder.findOne({
|
|
where: {
|
|
userId: owner.id,
|
|
isAdultArea: true,
|
|
name: 'Erotik'
|
|
},
|
|
include: [{
|
|
model: ImageVisibilityType,
|
|
through: { model: FolderImageVisibility },
|
|
attributes: ['id']
|
|
}]
|
|
});
|
|
if (!ownerRoot) {
|
|
return null;
|
|
}
|
|
|
|
if (!(await this.canRequesterAccessAdultFolder(ownerRoot, requestingUserId))) {
|
|
const error = new Error('Adult folder access denied');
|
|
error.status = 403;
|
|
throw error;
|
|
}
|
|
|
|
const children = await this.getAccessibleAdultFolders(ownerRoot.id, owner.id, requestingUserId);
|
|
const rootFolder = ownerRoot.get();
|
|
rootFolder.visibilityTypeIds = ownerRoot.image_visibility_types.map(v => v.id);
|
|
rootFolder.selectedUsers = await this.getFolderSelectedUsernames(ownerRoot.id);
|
|
delete rootFolder.image_visibility_types;
|
|
rootFolder.children = children;
|
|
return rootFolder;
|
|
}
|
|
|
|
async getAdultFolderImageList(hashedId, folderId) {
|
|
const userId = await this.requireAdultAreaAccessByHash(hashedId);
|
|
const folder = await Folder.findOne({
|
|
where: { id: folderId, isAdultArea: true }
|
|
});
|
|
if (!folder) {
|
|
throw new Error('Folder not found');
|
|
}
|
|
if (!(await this.canRequesterAccessAdultFolder(folder, userId))) {
|
|
const error = new Error('Access denied');
|
|
error.status = 403;
|
|
throw error;
|
|
}
|
|
const images = await Image.findAll({
|
|
where: {
|
|
folderId: folder.id,
|
|
isAdultContent: true,
|
|
userId: folder.userId
|
|
},
|
|
order: [['title', 'asc']]
|
|
});
|
|
const visibleImages = [];
|
|
for (const image of images) {
|
|
if (await this.canRequesterAccessAdultImage(image, userId)) {
|
|
visibleImages.push(image);
|
|
}
|
|
}
|
|
return this.enrichImageVisibilityMetadata(visibleImages);
|
|
}
|
|
|
|
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);
|
|
await this.saveImageSelectedUsers(newImage.id, formData.selectedUsers || formData.selectedUsernames || [], userId, {
|
|
adultOnly: true
|
|
});
|
|
return newImage;
|
|
}
|
|
|
|
async getAdultImageFilePath(hashedId, hash) {
|
|
const userId = await this.requireAdultAreaAccessByHash(hashedId);
|
|
const image = await Image.findOne({
|
|
where: {
|
|
hash,
|
|
isAdultContent: true
|
|
}
|
|
});
|
|
if (!image) {
|
|
throw new Error('Image not found');
|
|
}
|
|
if (!(await this.canRequesterAccessAdultImage(image, userId))) {
|
|
const error = new Error('Access denied');
|
|
error.status = 403;
|
|
throw error;
|
|
}
|
|
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);
|
|
const videos = await EroticVideo.findAll({
|
|
where: { userId },
|
|
order: [['createdAt', 'DESC']]
|
|
});
|
|
return this.enrichEroticVideoVisibilityMetadata(videos);
|
|
}
|
|
|
|
async getEroticVideosByUsername(username, hashedId) {
|
|
const requestingUserId = await this.requireAdultAreaAccessByHash(hashedId);
|
|
const owner = await this.loadUserByName(username);
|
|
if (!owner) {
|
|
throw new Error('User not found');
|
|
}
|
|
|
|
const videos = await EroticVideo.findAll({
|
|
where: { userId: owner.id },
|
|
order: [['createdAt', 'DESC']]
|
|
});
|
|
|
|
const visibleVideos = [];
|
|
for (const video of videos) {
|
|
if (await this.canRequesterAccessEroticVideo(video, requestingUserId)) {
|
|
visibleVideos.push(video);
|
|
}
|
|
}
|
|
|
|
return this.enrichEroticVideoVisibilityMetadata(visibleVideos);
|
|
}
|
|
|
|
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);
|
|
|
|
const video = await EroticVideo.create({
|
|
title: formData.title || file.originalname,
|
|
description: formData.description || null,
|
|
originalFileName: file.originalname,
|
|
hash: fileName,
|
|
mimeType: file.mimetype,
|
|
userId
|
|
});
|
|
|
|
const visibility = formData.visibility || JSON.stringify(
|
|
(await this.getPossibleImageVisibilities())
|
|
.filter(entry => entry.description === 'adults')
|
|
.map(entry => entry.id)
|
|
);
|
|
await this.saveEroticVideoVisibilities(video.id, visibility);
|
|
await this.saveEroticVideoSelectedUsers(video.id, formData.selectedUsers || formData.selectedUsernames || [], userId, {
|
|
adultOnly: true
|
|
});
|
|
|
|
return video;
|
|
}
|
|
|
|
async getEroticVideoFilePath(hashedId, hash) {
|
|
const userId = await this.requireAdultAreaAccessByHash(hashedId);
|
|
const video = await EroticVideo.findOne({
|
|
where: { hash }
|
|
});
|
|
if (!video) {
|
|
throw new Error('Video not found');
|
|
}
|
|
if (!(await this.canRequesterAccessEroticVideo(video, userId))) {
|
|
const error = new Error('Access denied');
|
|
error.status = 403;
|
|
throw error;
|
|
}
|
|
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 changeEroticVideo(hashedUserId, videoId, payload) {
|
|
const userId = await this.requireAdultAreaAccessByHash(hashedUserId);
|
|
const video = await EroticVideo.findOne({
|
|
where: {
|
|
id: videoId,
|
|
userId
|
|
}
|
|
});
|
|
if (!video) {
|
|
throw new Error('Video not found');
|
|
}
|
|
|
|
await video.update({
|
|
title: payload.title || video.title,
|
|
description: payload.description ?? video.description
|
|
});
|
|
await this.saveEroticVideoVisibilities(videoId, payload.visibilities);
|
|
await this.saveEroticVideoSelectedUsers(videoId, payload.selectedUsers || [], userId, {
|
|
adultOnly: true
|
|
});
|
|
return video;
|
|
}
|
|
|
|
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, selectedUsers = []) {
|
|
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 });
|
|
}
|
|
await this.saveImageSelectedUsers(imageId, selectedUsers, userId);
|
|
return image.folderId;
|
|
}
|
|
|
|
async changeAdultImage(hashedUserId, imageId, title, visibilities, selectedUsers = []) {
|
|
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 });
|
|
}
|
|
await this.saveImageSelectedUsers(imageId, selectedUsers, userId, {
|
|
adultOnly: true
|
|
});
|
|
return image.folderId;
|
|
}
|
|
|
|
async getAccessibleAdultFolders(parentId, ownerUserId, requestingUserId) {
|
|
const folders = await Folder.findAll({
|
|
where: { parentId, userId: ownerUserId, isAdultArea: true },
|
|
include: [{
|
|
model: ImageVisibilityType,
|
|
through: { model: FolderImageVisibility },
|
|
attributes: ['id']
|
|
}],
|
|
order: [['name', 'asc']]
|
|
});
|
|
|
|
const result = [];
|
|
for (const folderRecord of folders) {
|
|
if (!(await this.canRequesterAccessAdultFolder(folderRecord, requestingUserId))) {
|
|
continue;
|
|
}
|
|
const folder = folderRecord.get();
|
|
folder.visibilityTypeIds = folderRecord.image_visibility_types.map(v => v.id);
|
|
folder.selectedUsers = await this.getFolderSelectedUsernames(folder.id);
|
|
delete folder.image_visibility_types;
|
|
folder.children = await this.getAccessibleAdultFolders(folder.id, ownerUserId, requestingUserId);
|
|
result.push(folder);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
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;
|