feat(socialnetwork): enhance folder and video management with user visibility options

- 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.
This commit is contained in:
Torsten Schulz (local)
2026-03-27 16:56:45 +01:00
parent 39032570e3
commit 71e120bf20
13 changed files with 1081 additions and 36 deletions

View File

@@ -16,13 +16,16 @@ class SocialNetworkController {
this.getFoldersByUsername = this.getFoldersByUsername.bind(this);
this.deleteFolder = this.deleteFolder.bind(this);
this.getAdultFolders = this.getAdultFolders.bind(this);
this.getAdultFoldersByUsername = this.getAdultFoldersByUsername.bind(this);
this.createAdultFolder = this.createAdultFolder.bind(this);
this.getAdultFolderImageList = this.getAdultFolderImageList.bind(this);
this.uploadAdultImage = this.uploadAdultImage.bind(this);
this.getAdultImageByHash = this.getAdultImageByHash.bind(this);
this.changeAdultImage = this.changeAdultImage.bind(this);
this.listEroticVideos = this.listEroticVideos.bind(this);
this.getEroticVideosByUsername = this.getEroticVideosByUsername.bind(this);
this.uploadEroticVideo = this.uploadEroticVideo.bind(this);
this.changeEroticVideo = this.changeEroticVideo.bind(this);
this.getEroticVideoByHash = this.getEroticVideoByHash.bind(this);
this.reportEroticContent = this.reportEroticContent.bind(this);
this.createGuestbookEntry = this.createGuestbookEntry.bind(this);
@@ -157,8 +160,8 @@ class SocialNetworkController {
try {
const userId = req.headers.userid;
const { imageId } = req.params;
const { title, visibilities } = req.body;
const folderId = await this.socialNetworkService.changeImage(userId, imageId, title, visibilities);
const { title, visibilities, selectedUsers } = req.body;
const folderId = await this.socialNetworkService.changeImage(userId, imageId, title, visibilities, selectedUsers);
console.log('--->', folderId);
res.status(201).json(await this.socialNetworkService.getFolderImageList(userId, folderId));
} catch (error) {
@@ -208,6 +211,21 @@ class SocialNetworkController {
}
}
async getAdultFoldersByUsername(req, res) {
try {
const requestingUserId = req.headers.userid;
const { username } = req.params;
const folders = await this.socialNetworkService.getAdultFoldersByUsername(username, requestingUserId);
if (!folders) {
return res.status(404).json({ error: 'No folders found or access denied.' });
}
res.status(200).json(folders);
} catch (error) {
console.error('Error in getAdultFoldersByUsername:', error);
res.status(error.status || 500).json({ error: error.message });
}
}
async createAdultFolder(req, res) {
try {
const userId = req.headers.userid;
@@ -267,8 +285,8 @@ class SocialNetworkController {
try {
const userId = req.headers.userid;
const { imageId } = req.params;
const { title, visibilities } = req.body;
const folderId = await this.socialNetworkService.changeAdultImage(userId, imageId, title, visibilities);
const { title, visibilities, selectedUsers } = req.body;
const folderId = await this.socialNetworkService.changeAdultImage(userId, imageId, title, visibilities, selectedUsers);
res.status(201).json(await this.socialNetworkService.getAdultFolderImageList(userId, folderId));
} catch (error) {
console.error('Error in changeAdultImage:', error);
@@ -287,6 +305,18 @@ class SocialNetworkController {
}
}
async getEroticVideosByUsername(req, res) {
try {
const userId = req.headers.userid;
const { username } = req.params;
const videos = await this.socialNetworkService.getEroticVideosByUsername(username, userId);
res.status(200).json(videos);
} catch (error) {
console.error('Error in getEroticVideosByUsername:', error);
res.status(error.status || 500).json({ error: error.message });
}
}
async uploadEroticVideo(req, res) {
try {
const userId = req.headers.userid;
@@ -300,6 +330,18 @@ class SocialNetworkController {
}
}
async changeEroticVideo(req, res) {
try {
const userId = req.headers.userid;
const { videoId } = req.params;
const updatedVideo = await this.socialNetworkService.changeEroticVideo(userId, videoId, req.body);
res.status(200).json(updatedVideo);
} catch (error) {
console.error('Error in changeEroticVideo:', error);
res.status(error.status || 500).json({ error: error.message });
}
}
async getEroticVideoByHash(req, res) {
try {
const userId = req.headers.userid;

View File

@@ -0,0 +1,89 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable(
{ schema: 'community', tableName: 'erotic_video_image_visibility' },
{
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
erotic_video_id: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: { schema: 'community', tableName: 'erotic_video' },
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
visibility_type_id: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: { schema: 'type', tableName: 'image_visibility' },
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
}
);
await queryInterface.createTable(
{ schema: 'community', tableName: 'erotic_video_visibility_user' },
{
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
erotic_video_id: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: { schema: 'community', tableName: 'erotic_video' },
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
user_id: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: { schema: 'community', tableName: 'user' },
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
}
);
await queryInterface.sequelize.query(`
INSERT INTO community.erotic_video_image_visibility (erotic_video_id, visibility_type_id)
SELECT ev.id, iv.id
FROM community.erotic_video ev
CROSS JOIN type.image_visibility iv
WHERE iv.description = 'adults'
AND NOT EXISTS (
SELECT 1
FROM community.erotic_video_image_visibility eviv
WHERE eviv.erotic_video_id = ev.id
AND eviv.visibility_type_id = iv.id
)
`);
},
async down(queryInterface) {
await queryInterface.dropTable({ schema: 'community', tableName: 'erotic_video_visibility_user' });
await queryInterface.dropTable({ schema: 'community', tableName: 'erotic_video_image_visibility' });
},
};

View File

@@ -25,6 +25,8 @@ import ImageVisibilityUser from './community/image_visibility_user.js';
import FolderImageVisibility from './community/folder_image_visibility.js';
import ImageImageVisibility from './community/image_image_visibility.js';
import FolderVisibilityUser from './community/folder_visibility_user.js';
import EroticVideoImageVisibility from './community/erotic_video_image_visibility.js';
import EroticVideoVisibilityUser from './community/erotic_video_visibility_user.js';
import GuestbookEntry from './community/guestbook.js';
import Forum from './forum/forum.js';
import Title from './forum/title.js';
@@ -242,6 +244,17 @@ export default function setupAssociations() {
otherKey: 'imageId'
});
EroticVideo.belongsToMany(ImageVisibilityType, {
through: EroticVideoImageVisibility,
foreignKey: 'eroticVideoId',
otherKey: 'visibilityTypeId'
});
ImageVisibilityType.belongsToMany(EroticVideo, {
through: EroticVideoImageVisibility,
foreignKey: 'visibilityTypeId',
otherKey: 'eroticVideoId'
});
Folder.belongsToMany(ImageVisibilityUser, {
through: FolderVisibilityUser,
foreignKey: 'folderId',
@@ -253,6 +266,19 @@ export default function setupAssociations() {
otherKey: 'folderId'
});
EroticVideo.belongsToMany(User, {
through: EroticVideoVisibilityUser,
foreignKey: 'eroticVideoId',
otherKey: 'userId',
as: 'selectedVisibilityUsers'
});
User.belongsToMany(EroticVideo, {
through: EroticVideoVisibilityUser,
foreignKey: 'userId',
otherKey: 'eroticVideoId',
as: 'visibleEroticVideos'
});
// Guestbook related associations
User.hasMany(GuestbookEntry, { foreignKey: 'recipientId', as: 'receivedEntries' });
User.hasMany(GuestbookEntry, { foreignKey: 'senderId', as: 'sentEntries' });

View File

@@ -0,0 +1,26 @@
import { sequelize } from '../../utils/sequelize.js';
import { DataTypes } from 'sequelize';
const EroticVideoImageVisibility = sequelize.define('erotic_video_image_visibility', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
eroticVideoId: {
type: DataTypes.INTEGER,
allowNull: false
},
visibilityTypeId: {
type: DataTypes.INTEGER,
allowNull: false
}
}, {
tableName: 'erotic_video_image_visibility',
timestamps: false,
underscored: true,
schema: 'community'
});
export default EroticVideoImageVisibility;

View File

@@ -0,0 +1,26 @@
import { sequelize } from '../../utils/sequelize.js';
import { DataTypes } from 'sequelize';
const EroticVideoVisibilityUser = sequelize.define('erotic_video_visibility_user', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
eroticVideoId: {
type: DataTypes.INTEGER,
allowNull: false
},
userId: {
type: DataTypes.INTEGER,
allowNull: false
}
}, {
tableName: 'erotic_video_visibility_user',
timestamps: false,
underscored: true,
schema: 'community'
});
export default EroticVideoVisibilityUser;

View File

@@ -25,6 +25,8 @@ import ImageVisibilityUser from './community/image_visibility_user.js';
import FolderImageVisibility from './community/folder_image_visibility.js';
import ImageImageVisibility from './community/image_image_visibility.js';
import FolderVisibilityUser from './community/folder_visibility_user.js';
import EroticVideoImageVisibility from './community/erotic_video_image_visibility.js';
import EroticVideoVisibilityUser from './community/erotic_video_visibility_user.js';
import GuestbookEntry from './community/guestbook.js';
import DiaryHistory from './community/diary_history.js';
import Diary from './community/diary.js';
@@ -179,6 +181,8 @@ const models = {
FolderImageVisibility,
ImageImageVisibility,
FolderVisibilityUser,
EroticVideoImageVisibility,
EroticVideoVisibilityUser,
GuestbookEntry,
DiaryHistory,
Diary,

View File

@@ -17,12 +17,15 @@ router.get('/folder/:folderId', socialNetworkController.getFolderImageList);
router.post('/images', upload.single('image'), socialNetworkController.uploadImage);
router.post('/erotic/folders/:folderId', socialNetworkController.createAdultFolder);
router.get('/erotic/folders', socialNetworkController.getAdultFolders);
router.get('/profile/erotic/folders/:username', socialNetworkController.getAdultFoldersByUsername);
router.get('/profile/erotic/videos/:username', socialNetworkController.getEroticVideosByUsername);
router.get('/erotic/folder/:folderId', socialNetworkController.getAdultFolderImageList);
router.post('/erotic/images', upload.single('image'), socialNetworkController.uploadAdultImage);
router.put('/erotic/images/:imageId', socialNetworkController.changeAdultImage);
router.get('/erotic/image/:hash', socialNetworkController.getAdultImageByHash);
router.get('/erotic/videos', socialNetworkController.listEroticVideos);
router.post('/erotic/videos', upload.single('video'), socialNetworkController.uploadEroticVideo);
router.put('/erotic/videos/:videoId', socialNetworkController.changeEroticVideo);
router.get('/erotic/video/:hash', socialNetworkController.getEroticVideoByHash);
router.post('/erotic/report', socialNetworkController.reportEroticContent);
router.get('/images/:imageId', socialNetworkController.getImage);

View File

@@ -13,6 +13,8 @@ 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';
@@ -74,6 +76,372 @@ class SocialNetworkService extends BaseService {
}
}
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({
@@ -240,6 +608,9 @@ class SocialNetworkService extends BaseService {
visibilityTypeId: visibilityId
});
}
await this.saveFolderSelectedUsers(newFolder.id, data.selectedUsers || data.selectedUsernames || [], user.id, {
adultOnly: isAdultArea
});
return newFolder;
}
@@ -270,6 +641,7 @@ class SocialNetworkService extends BaseService {
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);
}
@@ -286,7 +658,7 @@ class SocialNetworkService extends BaseService {
}
});
if (!folder) throw new Error('Folder not found');
return await Image.findAll({
const images = await Image.findAll({
where: {
folderId: folder.id,
isAdultContent: false
@@ -295,6 +667,7 @@ class SocialNetworkService extends BaseService {
['title', 'asc']
]
});
return this.enrichImageVisibilityMetadata(images);
}
async uploadImage(hashedId, file, formData) {
@@ -302,6 +675,7 @@ class SocialNetworkService extends BaseService {
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;
}
@@ -406,7 +780,11 @@ class SocialNetworkService extends BaseService {
}
async loadUserByName(userName) {
return await User.findOne({ username: userName});
return await User.findOne({
where: {
username: userName
}
});
}
validateFolderData(data) {
@@ -601,27 +979,78 @@ class SocialNetworkService extends BaseService {
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, userId, isAdultArea: true }
where: { id: folderId, isAdultArea: true }
});
if (!folder) {
throw new Error('Folder not found');
}
return await Image.findAll({
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
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) {
@@ -650,6 +1079,9 @@ class SocialNetworkService extends BaseService {
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;
}
@@ -658,13 +1090,17 @@ class SocialNetworkService extends BaseService {
const image = await Image.findOne({
where: {
hash,
userId,
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');
}
@@ -677,10 +1113,33 @@ class SocialNetworkService extends BaseService {
async listEroticVideos(hashedId) {
const userId = await this.requireAdultAreaAccessByHash(hashedId);
return await EroticVideo.findAll({
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) {
@@ -698,7 +1157,7 @@ class SocialNetworkService extends BaseService {
const filePath = this.buildFilePath(fileName, 'erotic-video');
await this.saveFile(file.buffer, filePath);
return await EroticVideo.create({
const video = await EroticVideo.create({
title: formData.title || file.originalname,
description: formData.description || null,
originalFileName: file.originalname,
@@ -706,16 +1165,33 @@ class SocialNetworkService extends BaseService {
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, userId }
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');
}
@@ -726,6 +1202,29 @@ class SocialNetworkService extends BaseService {
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();
@@ -807,7 +1306,7 @@ class SocialNetworkService extends BaseService {
});
}
async changeImage(hashedUserId, imageId, title, visibilities) {
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 } });
@@ -819,10 +1318,11 @@ class SocialNetworkService extends BaseService {
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) {
async changeAdultImage(hashedUserId, imageId, title, visibilities, selectedUsers = []) {
const userId = await this.requireAdultAreaAccessByHash(hashedUserId);
const image = await Image.findOne({
where: {
@@ -839,9 +1339,38 @@ class SocialNetworkService extends BaseService {
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) {