Files
yourpart3/backend/services/socialnetworkService.js
Torsten Schulz (local) 53c748a074 feat: Implement blog and blog post models, routes, and services
- Added Blog and BlogPost models with necessary fields and relationships.
- Created blogRouter for handling blog-related API endpoints including CRUD operations.
- Developed BlogService for business logic related to blogs and posts, including sharing functionality.
- Implemented API client methods for frontend to interact with blog-related endpoints.
- Added internationalization support for blog-related text in English and German.
- Created Vue components for blog editing, listing, and viewing, including a rich text editor for post content.
- Enhanced user experience with form validations and dynamic visibility settings based on user input.
2025-08-18 13:41:37 +02:00

886 lines
33 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 ImageVisibilityType from '../models/type/image_visibility.js';
import FolderImageVisibility from '../models/community/folder_image_visibility.js';
import ImageImageVisibility from '../models/community/image_image_visibility.js';
import { v4 as uuidv4 } from 'uuid';
import fs from 'fs';
import fsPromises from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import FolderVisibilityUser from '../models/community/folder_visibility_user.js';
import ImageVisibilityUser from '../models/community/image_visibility_user.js';
import GuestbookEntry from '../models/community/guestbook.js';
import { JSDOM } from 'jsdom';
import DOMPurify from 'dompurify';
import sharp from 'sharp';
import Diary from '../models/community/diary.js';
import Friendship from '../models/community/friendship.js';
import { getUserSession } from '../utils/redis.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
class SocialNetworkService extends BaseService {
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) {
await this.checkUserAccess(hashedUserId);
const user = await this.loadUserByHash(hashedUserId);
if (!user) {
throw new Error('User not found');
}
console.log('given data', data, folderId);
const parentFolder = data.parentId ? await Folder.findOne({
where: { id: data.parentId, userId: user.id }
}) : 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
});
} else {
newFolder = await Folder.findOne({
where: { id: folderId, userId: user.id }
});
if (!newFolder) {
throw new Error('Folder not found or user does not own the folder');
}
newFolder.name = data.name;
await newFolder.save();
}
await FolderImageVisibility.destroy({
where: { folderId: newFolder.id }
});
for (const visibilityId of data.visibilities) {
await FolderImageVisibility.create({
folderId: newFolder.id,
visibilityTypeId: visibilityId
});
}
return newFolder;
}
async getFolders(hashedId) {
const userId = await this.checkUserAccess(hashedId);
let rootFolder = await Folder.findOne({
where: { parentId: null, userId },
include: [{
model: ImageVisibilityType,
through: { model: FolderImageVisibility },
attributes: ['id'],
}]
});
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
});
await FolderImageVisibility.create({
folderId: rootFolder.id,
visibilityTypeId: visibility.id
});
rootFolder = await Folder.findOne({
where: { id: rootFolder.id },
include: [{
model: ImageVisibilityType,
through: { model: FolderImageVisibility },
attributes: ['id'],
}]
});
}
const children = await this.getSubFolders(rootFolder.id, userId);
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) {
const folders = await Folder.findAll({
where: { parentId, userId },
include: [{
model: ImageVisibilityType,
through: { model: FolderImageVisibility },
attributes: ['id'],
}],
order: [
['name', 'asc']
]
});
for (const folder of folders) {
const children = await this.getSubFolders(folder.id, userId);
const visibilityTypeIds = folder.image_visibility_types.map(v => v.id);
folder.setDataValue('visibilityTypeIds', visibilityTypeIds);
folder.setDataValue('children', children);
folder.setDataValue('image_visibility_types', undefined);
}
return folders.map(folder => folder.get());
}
async getFolderImageList(hashedId, folderId) {
const userId = await this.checkUserAccess(hashedId);
const folder = await Folder.findOne({
where: {
id: folderId,
userId
}
});
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 processedImageName = await this.processAndUploadUserImage(file);
const newImage = await this.createImageRecord(formData, userId, file, processedImageName);
await this.saveImageVisibilities(newImage.id, formData.visibility);
return newImage;
}
async processAndUploadUserImage(file) {
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, 'user');
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) {
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 loadUserByHash(hashedId) {
console.log('Loading user by hashedId:', hashedId);
return await User.findOne({ where: { hashedId: hashedId } });
}
async loadUserByName(userName) {
return await User.findOne({ username: userName});
}
validateFolderData(data) {
if (!data.name || typeof data.name !== 'string') throw new Error('Invalid folder data: Name is required');
}
validateImageData(imageData) {
if (!imageData.url || typeof imageData.url !== 'string') throw new Error('Invalid image data: URL is required');
}
buildSearchWhereClause(username) {
const whereClause = { active: true, searchable: true };
if (username) {
whereClause.username = { [Op.iLike]: `%${username}%` };
}
return whereClause;
}
getUserParamsInclude() {
return [
{
model: UserParam,
as: 'user_params',
include: [{ model: UserParamType, as: 'paramType', required: true }]
}
];
}
async filterUsersByCriteria(users, ageFrom, ageTo, genders) {
const results = [];
for (const user of users) {
const userDetails = await this.extractUserDetails(user);
if (this.isUserValid(userDetails, ageFrom, ageTo, genders)) {
results.push(userDetails);
}
}
return results;
}
async extractUserDetails(user) {
const birthdateParam = user.user_params.find(param => param.paramType.description === 'birthdate');
const genderParam = user.user_params.find(param => param.paramType.description === 'gender');
const age = birthdateParam ? this.calculateAge(birthdateParam.value) : null;
const gender = genderParam ? await this.getGenderValue(genderParam.value) : null;
return {
id: user.hashedId,
username: user.username,
email: user.email,
gender,
age
};
}
async getGenderValue(genderId) {
const genderValue = await UserParamValue.findOne({ where: { id: genderId } });
return genderValue ? genderValue.value : null;
}
isUserValid(userDetails, ageFrom, ageTo, genders) {
const { age, gender } = userDetails;
const isWithinAgeRange = (!ageFrom || age >= ageFrom) && (!ageTo || age <= ageTo);
const isGenderValid = !genders || !genders.length || (gender && genders.includes(gender));
return isWithinAgeRange && isGenderValid && age >= 14;
}
async fetchUserProfile(hashedUserId) {
return await User.findOne({
where: { hashedId: hashedUserId, active: true, searchable: true },
include: [
{
model: UserParam,
as: 'user_params',
include: [
{ model: UserParamType, as: 'paramType' },
{ model: UserParamVisibility, as: 'param_visibilities', include: [{ model: UserParamVisibilityType, as: 'visibility_type' }] }
],
order: [['order_id', 'asc']],
},
{
model: Friendship,
as: 'friendSender',
},
{
model: Friendship,
as: 'friendReceiver',
}
]
});
}
async constructUserProfile(user, hashedUserId) {
const userParams = {};
const requestingUser = await this.loadUserByHash(hashedUserId);
if (!requestingUser) {
throw new Error('User not found');
}
const requestingUserAge = await this.getUserAge(requestingUser.id);
for (const param of user.user_params) {
const visibility = param.param_visibilities?.[0]?.visibility_type?.description || 'Invisible';
if (visibility === 'Invisible') continue;
if (this.isVisibleToUser(visibility, requestingUserAge)) {
userParams[param.paramType.description] = {
type: param.paramType.datatype,
value: await this.getParamValue(param)
};
}
}
let friendship = null;
if (user.friendSender && user.friendSender.length > 0) {
friendship = {
isSender: true,
accepted: user.friendSender[0].dataValues.accepted,
denied: user.friendSender[0].dataValues.denied,
withdrawn: user.friendSender[0].dataValues.withdrawn,
}
} else if (user.friendReceiver && user.friendReceiver.length > 0) {
friendship = {
isSender: false,
accepted: user.friendReceiver[0].dataValues.accepted,
denied: user.friendReceiver[0].dataValues.denied,
withdrawn: user.friendReceiver[0].dataValues.withdrawn,
}
}
return {
username: user.username,
registrationDate: user.registrationDate,
friendship: friendship,
params: userParams
};
}
async getUserAge(userId) {
const params = await this.getUserParams(userId, ['birthdate']);
const birthdateParam = params.find(param => param.paramType.description === 'birthdate');
return birthdateParam ? this.calculateAge(birthdateParam.value) : 0;
}
isVisibleToUser(visibility, requestingUserAge) {
return visibility === 'All' ||
(visibility === 'FriendsAndAdults' && requestingUserAge >= 18) ||
(visibility === 'AdultsOnly' && requestingUserAge >= 18);
}
async getParamValue(param) {
let paramValue = param.value;
try {
const parsedValue = JSON.parse(paramValue);
if (Array.isArray(parsedValue)) {
paramValue = await Promise.all(parsedValue.map(value => this.getValueFromDatabase(value, param.paramTypeId)));
} else if (/^\d+$/.test(paramValue)) {
paramValue = await this.getValueFromDatabase(paramValue, param.paramTypeId);
}
} catch (e) {
}
return paramValue;
}
async getValueFromDatabase(value, paramTypeId) {
const userParamValue = await UserParamValue.findOne({
where: { id: parseInt(value, 10), userParamTypeId: paramTypeId }
});
return userParamValue ? userParamValue.value : value;
}
async getPossibleImageVisibilities() {
return await ImageVisibilityType.findAll();
}
async getImageFilePath(hashedUserId, hash) {
const image = await Image.findOne({ where: { hash } });
if (!image) {
throw new Error('Image not found');
}
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;
}
// Public variant used by blog: allow access if the image's folder is visible to 'everyone'.
async getImageFilePathPublicByHash(hash) {
const image = await Image.findOne({ where: { hash } });
if (!image) {
throw new Error('Image not found');
}
const folder = await Folder.findOne({ where: { id: image.folderId } });
if (!folder) {
throw new Error('Folder not found');
}
const everyone = await ImageVisibilityType.findOne({ where: { description: 'everyone' } });
if (!everyone) {
throw new Error('Visibility type not found');
}
const hasEveryone = await FolderImageVisibility.findOne({ where: { folderId: folder.id, visibilityTypeId: everyone.id } });
if (!hasEveryone) {
const err = new Error('Access denied');
err.status = 403;
throw err;
}
const imagePath = this.buildFilePath(image.hash, 'user');
if (!fs.existsSync(imagePath)) {
throw new Error(`File "${imagePath}" not found`);
}
return imagePath;
}
async checkUserImageAccess(userId, imageId) {
const image = await Image.findByPk(imageId);
if (image.userId === userId) {
return true;
}
const accessRules = await ImageImageVisibility.findAll({
where: { imageId }
});
return accessRules.some(rule => {
return false;
});
}
async changeImage(hashedUserId, imageId, title, visibilities) {
const userId = await this.checkUserAccess(hashedUserId);
await this.checkUserImageAccess(userId, imageId);
const image = await Image.findOne({ where: { id: imageId } });
if (!image) {
throw new Error('image not found')
}
await image.update({ title: title });
await ImageImageVisibility.destroy({ where: { imageId } });
for (const visibility of visibilities) {
await ImageImageVisibility.create({ imageId, visibilityTypeId: visibility.id });
}
return image.folderId;
}
async 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 } });
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 = this.isUserAdult(requestingUser.id);
const accessibleFolders = await Folder.findAll({
where: { parentId: folderIdString },
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;