import config from 'config'; import { knexOwner as knex } from './knex.js'; import { indexApi } from './manticore.js'; import { HttpError } from './errors.js'; import slugify from './utils/slugify.js'; import initLogger from './logger.js'; const logger = initLogger(); let lastActorsViewRefresh = 0; export function curateStash(stash, assets = {}) { if (!stash) { return null; } const curatedStash = { id: stash.id, name: stash.name, slug: stash.slug, primary: stash.primary, public: stash.public, createdAt: stash.created_at, stashedScenes: stash.stashed_scenes || null, stashedMovies: stash.stashed_movies || null, stashedActors: stash.stashed_actors || null, user: assets.user ? { id: assets.user.id, username: assets.user.username, avatar: `/media/avatars/${assets.user.id}_${assets.user.username}.png`, createdAt: assets.user.created_at, } : null, }; return curatedStash; } function curateStashEntry(stash, user) { const curatedStashEntry = { user_id: user.id, name: stash.name, slug: slugify(stash.name), public: false, }; return curatedStashEntry; } function verifyStashAccess(stash, sessionUser) { if (!stash || (!stash.public && stash.user_id !== sessionUser?.id)) { throw new HttpError('This stash does not exist, or you are not allowed access.', 404); } } export async function fetchStashById(stashId, sessionUser) { const stash = await knex('stashes') .where('id', stashId) .first(); verifyStashAccess(stash, sessionUser); return curateStash(stash); } export async function fetchStashByUsernameAndSlug(username, stashSlug, sessionUser) { const user = await knex('users').where('username', username).first(); if (!user) { throw new HttpError('This user does not exist.', 404); } const stash = await knex('stashes') .select('stashes.*', 'stashes_meta.*') .leftJoin('stashes_meta', 'stashes_meta.stash_id', 'stashes.id') .where('slug', stashSlug) .where('user_id', user.id) .first(); verifyStashAccess(stash, sessionUser); return curateStash(stash, { user }); } export async function fetchStashes(domain, itemId, sessionUser) { const stashes = await knex(`stashes_${domain}s`) .select('stashes.*') .where({ [`${domain}_id`]: itemId, user_id: sessionUser.id, }) .leftJoin('stashes', 'stashes.id', `stashes_${domain}s.stash_id`); return stashes.map((stash) => curateStash(stash)); } export async function createStash(newStash, sessionUser) { if (!sessionUser) { throw new HttpError('You are not authenthicated', 401); } try { const stash = await knex('stashes') .insert(curateStashEntry(newStash, sessionUser)) .returning('*'); return curateStash(stash); } catch (error) { if (error.routine === '_bt_check_unique') { throw new HttpError('Stash name should be unique', 409); } throw error; } } export async function updateStash(stashId, newStash, sessionUser) { if (!sessionUser) { throw new HttpError('You are not authenthicated', 401); } const stash = await knex('stashes') .where({ id: stashId, user_id: sessionUser.id, }) .update(newStash) .returning('*'); if (!stash) { throw new HttpError('You are not authorized to modify this stash', 403); } return curateStash(stash); } export async function removeStash(stashId, sessionUser) { if (!sessionUser) { throw new HttpError('You are not authenthicated', 401); } const removed = await knex('stashes') .where({ id: stashId, user_id: sessionUser.id, primary: false, }) .delete(); if (removed === 0) { throw new HttpError('Unable to remove this stash', 400); } } export async function refreshActorsView() { if (new Date() - lastActorsViewRefresh > config.stashes.viewRefreshCooldown * 60000) { // don't refresh actors view more than once an hour lastActorsViewRefresh = new Date(); logger.debug('Refreshing actors view'); return knex.schema.refreshMaterializedView('actors_meta'); } logger.silly('Skipping actors view refresh'); return false; } export async function stashActor(actorId, stashId, sessionUser) { const stash = await fetchStashById(stashId, sessionUser); const [stashed] = await knex('stashes_actors') .insert({ stash_id: stash.id, actor_id: actorId, }) .returning(['id', 'created_at']); await indexApi.replace({ index: 'actors_stashed', id: stashed.id, doc: { actor_id: actorId, user_id: sessionUser.id, stash_id: stashId, created_at: Math.round(stashed.created_at.getTime() / 1000), }, }); logger.verbose(`${sessionUser.username} (${sessionUser.id}) stashed actor ${actorId} to stash ${stash.id} (${stash.name})`); refreshActorsView(); return fetchStashes('actor', actorId, sessionUser); } export async function unstashActor(actorId, stashId, sessionUser) { await knex .from('stashes_actors AS deletable') .where('deletable.actor_id', actorId) .where('deletable.stash_id', stashId) .whereExists(knex('stashes_actors') // verify user owns this stash, complimentary to row-level security .leftJoin('stashes', 'stashes.id', 'stashes_actors.stash_id') .where('stashes_actors.stash_id', knex.raw('deletable.stash_id')) .where('stashes.user_id', sessionUser.id)) .delete(); try { await indexApi.callDelete({ index: 'actors_stashed', query: { bool: { must: [ { equals: { actor_id: actorId } }, { equals: { stash_id: stashId } }, { equals: { user_id: sessionUser.id } }, ], }, }, }); } catch (error) { console.log(error); } logger.verbose(`${sessionUser.username} (${sessionUser.id}) unstashed actor ${actorId} from stash ${stashId}`); refreshActorsView(); return fetchStashes('actor', actorId, sessionUser); } export async function stashScene(sceneId, stashId, sessionUser) { const stash = await fetchStashById(stashId, sessionUser); const [stashed] = await knex('stashes_scenes') .insert({ stash_id: stash.id, scene_id: sceneId, }) .returning(['id', 'created_at']); await indexApi.replace({ index: 'scenes_stashed', id: stashed.id, doc: { // ...doc.replace.doc, scene_id: sceneId, user_id: sessionUser.id, stash_id: stashId, created_at: Math.round(stashed.created_at.getTime() / 1000), }, }); logger.verbose(`${sessionUser.username} (${sessionUser.id}) stashed scene ${sceneId} to stash ${stash.id} (${stash.name})`); return fetchStashes('scene', sceneId, sessionUser); } export async function unstashScene(sceneId, stashId, sessionUser) { await knex .from('stashes_scenes AS deletable') .where('deletable.scene_id', sceneId) .where('deletable.stash_id', stashId) .whereExists(knex('stashes_scenes') // verify user owns this stash, complimentary to row-level security .leftJoin('stashes', 'stashes.id', 'stashes_scenes.stash_id') .where('stashes_scenes.stash_id', knex.raw('deletable.stash_id')) .where('stashes.user_id', sessionUser.id)) .delete(); await indexApi.callDelete({ index: 'scenes_stashed', query: { bool: { must: [ { equals: { scene_id: sceneId } }, { equals: { stash_id: stashId } }, { equals: { user_id: sessionUser.id } }, ], }, }, }); logger.verbose(`${sessionUser.username} (${sessionUser.id}) unstashed scene ${sceneId} from stash ${stashId}`); return fetchStashes('scene', sceneId, sessionUser); } export async function stashMovie(movieId, stashId, sessionUser) { const stash = await fetchStashById(stashId, sessionUser); await knex('stashes_movies') .insert({ stash_id: stash.id, movie_id: movieId, }); return fetchStashes('movie', movieId, sessionUser); } export async function unstashMovie(movieId, stashId, sessionUser) { await knex .from('stashes_movies AS deletable') .where('deletable.movie_id', movieId) .where('deletable.stash_id', stashId) .whereExists(knex('stashes_movies') // verify user owns this stash, complimentary to row-level security .leftJoin('stashes', 'stashes.id', 'stashes_movies.stash_id') .where('stashes_movies.stash_id', knex.raw('deletable.stash_id')) .where('stashes.user_id', sessionUser.id)) .delete(); return fetchStashes('movie', movieId, sessionUser); }