Purging expired notifications and alerts. Fixed manticore stash sync.
This commit is contained in:
@@ -281,6 +281,22 @@ function toggleFilterCombined() {
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-for="scene in alert.scenes"
|
||||||
|
:key="`alert-${alert.id}-${scene.id}`"
|
||||||
|
class="alert-scene"
|
||||||
|
>
|
||||||
|
<time
|
||||||
|
:datetime="scene.date.toISOString()"
|
||||||
|
class="alert-date"
|
||||||
|
>{{ format(scene.date, 'yyyy-MM-dd') }}</time>
|
||||||
|
|
||||||
|
<a
|
||||||
|
:href="`/scene/${scene.id}/${scene.slug}`"
|
||||||
|
class="alert-value alert-title ellipsis link"
|
||||||
|
>{{ scene.title }}</a>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert-meta">
|
<div class="alert-meta">
|
||||||
@@ -476,6 +492,16 @@ function toggleFilterCombined() {
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert-scene {
|
||||||
|
display: flex;
|
||||||
|
gap: .5rem;
|
||||||
|
padding: 0 .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-date {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.alert-actions {
|
.alert-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -108,7 +108,15 @@ onMounted(async () => {
|
|||||||
class="notif-body notif-link nolink"
|
class="notif-body notif-link nolink"
|
||||||
>
|
>
|
||||||
<span class="notif-trigger">
|
<span class="notif-trigger">
|
||||||
<span class="notif-details">
|
<span
|
||||||
|
v-if="notif.matchedSceneIds.length > 0"
|
||||||
|
class="notif-details"
|
||||||
|
>Scene is released</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="notif-details"
|
||||||
|
>
|
||||||
New
|
New
|
||||||
|
|
||||||
<template
|
<template
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
showAlert: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['stashed', 'unstashed']);
|
const emit = defineEmits(['stashed', 'unstashed']);
|
||||||
@@ -118,6 +122,7 @@ async function alertItem() {
|
|||||||
...(props.domain === 'actors' && { actors: [props.item.id] }),
|
...(props.domain === 'actors' && { actors: [props.item.id] }),
|
||||||
...(props.domain === 'tags' && { tags: [props.item.id] }),
|
...(props.domain === 'tags' && { tags: [props.item.id] }),
|
||||||
...(props.domain === 'entities' && { entities: [props.item.id] }),
|
...(props.domain === 'entities' && { entities: [props.item.id] }),
|
||||||
|
...(props.domain === 'scenes' && { scenes: [props.item.id] }),
|
||||||
notify: true,
|
notify: true,
|
||||||
email: false,
|
email: false,
|
||||||
preset: true,
|
preset: true,
|
||||||
@@ -205,7 +210,7 @@ async function reloadStashes(newStash) {
|
|||||||
class="bookmarks"
|
class="bookmarks"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="showSecondary && !!itemAlerts"
|
v-if="showSecondary && showAlert && !!itemAlerts"
|
||||||
class="alert-trigger"
|
class="alert-trigger"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
|
|||||||
@@ -196,4 +196,9 @@ module.exports = {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
crontab: '*/10 * * * *', // every 10 minutes
|
crontab: '*/10 * * * *', // every 10 minutes
|
||||||
},
|
},
|
||||||
|
alerts: {
|
||||||
|
crontab: '*/10 * * * *', // every 10 minutes
|
||||||
|
seenExpiry: 30, // days
|
||||||
|
finalExpiry: 365, // days
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -363,6 +363,8 @@ function copySummary() {
|
|||||||
<Heart
|
<Heart
|
||||||
domain="scenes"
|
domain="scenes"
|
||||||
:item="scene"
|
:item="scene"
|
||||||
|
:show-secondary="true"
|
||||||
|
:show-alert="scene.isUpcoming"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="view">
|
<div class="view">
|
||||||
|
|||||||
134
src/alerts.js
134
src/alerts.js
@@ -1,11 +1,16 @@
|
|||||||
|
import config from 'config';
|
||||||
|
import { CronJob } from 'cron';
|
||||||
import escapeRegexp from 'escape-string-regexp';
|
import escapeRegexp from 'escape-string-regexp';
|
||||||
|
|
||||||
import promiseProps from '../utils/promise-props.js';
|
import promiseProps from '../utils/promise-props.js';
|
||||||
import { getIdsBySlug } from './cache.js';
|
import { getIdsBySlug } from './cache.js';
|
||||||
import { HttpError } from './errors.js';
|
import { HttpError } from './errors.js';
|
||||||
import { knexOwner as knex } from './knex.js';
|
import { knexOwner as knex } from './knex.js';
|
||||||
|
import initLogger from './logger.js';
|
||||||
import { fetchScenesById } from './scenes.js';
|
import { fetchScenesById } from './scenes.js';
|
||||||
|
|
||||||
|
const logger = initLogger();
|
||||||
|
|
||||||
function curateAlert(alert, context = {}) {
|
function curateAlert(alert, context = {}) {
|
||||||
return {
|
return {
|
||||||
id: alert.id,
|
id: alert.id,
|
||||||
@@ -41,6 +46,12 @@ function curateAlert(alert, context = {}) {
|
|||||||
property: match.property,
|
property: match.property,
|
||||||
expression: match.expression,
|
expression: match.expression,
|
||||||
})) || [],
|
})) || [],
|
||||||
|
scenes: context.scenes?.map((scene) => ({
|
||||||
|
id: scene.scene_id,
|
||||||
|
title: scene.scene_title,
|
||||||
|
slug: scene.scene_slug,
|
||||||
|
date: scene.scene_date,
|
||||||
|
})) || [],
|
||||||
stashes: context.stashes?.map((stash) => ({
|
stashes: context.stashes?.map((stash) => ({
|
||||||
id: stash.stash_id,
|
id: stash.stash_id,
|
||||||
name: stash.stash_name,
|
name: stash.stash_name,
|
||||||
@@ -58,11 +69,13 @@ export async function fetchAlerts(user, alertIds) {
|
|||||||
actors,
|
actors,
|
||||||
tags,
|
tags,
|
||||||
entities,
|
entities,
|
||||||
|
scenes,
|
||||||
matches,
|
matches,
|
||||||
stashes,
|
stashes,
|
||||||
} = await promiseProps({
|
} = await promiseProps({
|
||||||
alerts: knex('alerts')
|
alerts: knex('alerts')
|
||||||
.where('user_id', user.id)
|
.where('user_id', user.id)
|
||||||
|
.where('is_expired', false)
|
||||||
.where((builder) => {
|
.where((builder) => {
|
||||||
if (alertIds) {
|
if (alertIds) {
|
||||||
builder.whereIn('id', alertIds);
|
builder.whereIn('id', alertIds);
|
||||||
@@ -74,6 +87,7 @@ export async function fetchAlerts(user, alertIds) {
|
|||||||
.leftJoin('alerts', 'alerts.id', 'alerts_actors.alert_id')
|
.leftJoin('alerts', 'alerts.id', 'alerts_actors.alert_id')
|
||||||
.leftJoin('actors', 'actors.id', 'alerts_actors.actor_id')
|
.leftJoin('actors', 'actors.id', 'alerts_actors.actor_id')
|
||||||
.where('alerts.user_id', user.id)
|
.where('alerts.user_id', user.id)
|
||||||
|
.where('alerts.is_expired', false)
|
||||||
.where((builder) => {
|
.where((builder) => {
|
||||||
if (alertIds) {
|
if (alertIds) {
|
||||||
builder.whereIn('alerts.id', alertIds);
|
builder.whereIn('alerts.id', alertIds);
|
||||||
@@ -84,6 +98,7 @@ export async function fetchAlerts(user, alertIds) {
|
|||||||
.leftJoin('alerts', 'alerts.id', 'alerts_tags.alert_id')
|
.leftJoin('alerts', 'alerts.id', 'alerts_tags.alert_id')
|
||||||
.leftJoin('tags', 'tags.id', 'alerts_tags.tag_id')
|
.leftJoin('tags', 'tags.id', 'alerts_tags.tag_id')
|
||||||
.where('alerts.user_id', user.id)
|
.where('alerts.user_id', user.id)
|
||||||
|
.where('alerts.is_expired', false)
|
||||||
.where((builder) => {
|
.where((builder) => {
|
||||||
if (alertIds) {
|
if (alertIds) {
|
||||||
builder.whereIn('alerts.id', alertIds);
|
builder.whereIn('alerts.id', alertIds);
|
||||||
@@ -94,6 +109,18 @@ export async function fetchAlerts(user, alertIds) {
|
|||||||
.leftJoin('alerts', 'alerts.id', 'alerts_entities.alert_id')
|
.leftJoin('alerts', 'alerts.id', 'alerts_entities.alert_id')
|
||||||
.leftJoin('entities', 'entities.id', 'alerts_entities.entity_id')
|
.leftJoin('entities', 'entities.id', 'alerts_entities.entity_id')
|
||||||
.where('alerts.user_id', user.id)
|
.where('alerts.user_id', user.id)
|
||||||
|
.where('alerts.is_expired', false)
|
||||||
|
.where((builder) => {
|
||||||
|
if (alertIds) {
|
||||||
|
builder.whereIn('alerts.id', alertIds);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
scenes: knex('alerts_scenes')
|
||||||
|
.select('alerts_scenes.*', 'releases.id as scene_id', 'releases.slug as scene_slug', 'releases.title as scene_title', 'releases.date as scene_date')
|
||||||
|
.leftJoin('alerts', 'alerts.id', 'alerts_scenes.alert_id')
|
||||||
|
.leftJoin('releases', 'releases.id', 'alerts_scenes.scene_id')
|
||||||
|
.where('alerts.user_id', user.id)
|
||||||
|
.where('alerts.is_expired', false)
|
||||||
.where((builder) => {
|
.where((builder) => {
|
||||||
if (alertIds) {
|
if (alertIds) {
|
||||||
builder.whereIn('alerts.id', alertIds);
|
builder.whereIn('alerts.id', alertIds);
|
||||||
@@ -103,6 +130,7 @@ export async function fetchAlerts(user, alertIds) {
|
|||||||
.select('alerts_matches.*')
|
.select('alerts_matches.*')
|
||||||
.leftJoin('alerts', 'alerts.id', 'alerts_matches.alert_id')
|
.leftJoin('alerts', 'alerts.id', 'alerts_matches.alert_id')
|
||||||
.where('alerts.user_id', user.id)
|
.where('alerts.user_id', user.id)
|
||||||
|
.where('alerts.is_expired', false)
|
||||||
.where((builder) => {
|
.where((builder) => {
|
||||||
if (alertIds) {
|
if (alertIds) {
|
||||||
builder.whereIn('alerts.id', alertIds);
|
builder.whereIn('alerts.id', alertIds);
|
||||||
@@ -113,6 +141,7 @@ export async function fetchAlerts(user, alertIds) {
|
|||||||
.leftJoin('alerts', 'alerts.id', 'alerts_stashes.alert_id')
|
.leftJoin('alerts', 'alerts.id', 'alerts_stashes.alert_id')
|
||||||
.leftJoin('stashes', 'stashes.id', 'alerts_stashes.stash_id')
|
.leftJoin('stashes', 'stashes.id', 'alerts_stashes.stash_id')
|
||||||
.where('alerts.user_id', user.id)
|
.where('alerts.user_id', user.id)
|
||||||
|
.where('alerts.is_expired', false)
|
||||||
.where((builder) => {
|
.where((builder) => {
|
||||||
if (alertIds) {
|
if (alertIds) {
|
||||||
builder.whereIn('alerts.id', alertIds);
|
builder.whereIn('alerts.id', alertIds);
|
||||||
@@ -125,6 +154,7 @@ export async function fetchAlerts(user, alertIds) {
|
|||||||
tags: tags.filter((tag) => tag.alert_id === alert.id),
|
tags: tags.filter((tag) => tag.alert_id === alert.id),
|
||||||
entities: entities.filter((entity) => entity.alert_id === alert.id),
|
entities: entities.filter((entity) => entity.alert_id === alert.id),
|
||||||
matches: matches.filter((match) => match.alert_id === alert.id),
|
matches: matches.filter((match) => match.alert_id === alert.id),
|
||||||
|
scenes: scenes.filter((scene) => scene.alert_id === alert.id),
|
||||||
stashes: stashes.filter((stash) => stash.alert_id === alert.id),
|
stashes: stashes.filter((stash) => stash.alert_id === alert.id),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -140,9 +170,10 @@ export async function createAlert(alert, reqUser) {
|
|||||||
const entityIds = (await getIdsBySlug(alert.entities || [], 'entities')).concat(alert.entityIds || []);
|
const entityIds = (await getIdsBySlug(alert.entities || [], 'entities')).concat(alert.entityIds || []);
|
||||||
const actorIds = [...(alert.actors || []), ...(alert.actorIds || [])]; // for consistency with tagIds and entityIds
|
const actorIds = [...(alert.actors || []), ...(alert.actorIds || [])]; // for consistency with tagIds and entityIds
|
||||||
const stashIds = [...(alert.stashes || []), ...(alert.stashIds || [])]; // for consistency with tagIds and entityIds
|
const stashIds = [...(alert.stashes || []), ...(alert.stashIds || [])]; // for consistency with tagIds and entityIds
|
||||||
|
const sceneIds = [...(alert.scenes || []), ...(alert.sceneIds || [])]; // for consistency with tagIds and entityIds
|
||||||
|
|
||||||
if (actorIds.length === 0 && tagIds.length === 0 && entityIds.length === 0 && (!alert.matches || alert.matches.length === 0)) {
|
if (actorIds.length === 0 && tagIds.length === 0 && entityIds.length === 0 && (!alert.matches || alert.matches.length === 0) && sceneIds.length === 0) {
|
||||||
throw new HttpError('Alert must contain at least one actor, tag or entity', 400);
|
throw new HttpError('Alert must contain at least one actor, tag, entity or scene', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (alert.matches?.some((match) => !match.property || !match.expression)) {
|
if (alert.matches?.some((match) => !match.property || !match.expression)) {
|
||||||
@@ -197,12 +228,17 @@ export async function createAlert(alert, reqUser) {
|
|||||||
alert_id: alertId,
|
alert_id: alertId,
|
||||||
entity_id: entityId,
|
entity_id: entityId,
|
||||||
})).slice(0, alert.allEntities ? 1 : Infinity)), // one scene can never match multiple entities in AND mode
|
})).slice(0, alert.allEntities ? 1 : Infinity)), // one scene can never match multiple entities in AND mode
|
||||||
|
sceneIds?.length > 0 && knex('alerts_scenes').insert(sceneIds.map((sceneId) => ({
|
||||||
|
alert_id: alertId,
|
||||||
|
scene_id: sceneId,
|
||||||
|
}))),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
alert.actors?.length > 0 && knex.schema.refreshMaterializedView('alerts_users_actors'),
|
actorIds?.length > 0 && knex.schema.refreshMaterializedView('alerts_users_actors'),
|
||||||
alert.tags?.length > 0 && knex.schema.refreshMaterializedView('alerts_users_tags'),
|
tagIds?.length > 0 && knex.schema.refreshMaterializedView('alerts_users_tags'),
|
||||||
alert.entities?.length > 0 && knex.schema.refreshMaterializedView('alerts_users_entities'),
|
entityIds?.length > 0 && knex.schema.refreshMaterializedView('alerts_users_entities'),
|
||||||
|
sceneIds?.length > 0 && knex.schema.refreshMaterializedView('alerts_users_scenes'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const [newAlert] = await fetchAlerts(reqUser, [alertId]);
|
const [newAlert] = await fetchAlerts(reqUser, [alertId]);
|
||||||
@@ -219,11 +255,13 @@ export async function removeAlert(alertId, reqUser) {
|
|||||||
knex.raw('coalesce(array_agg(distinct alerts_actors.actor_id) filter (where alerts_actors.actor_id is not null), \'{}\') as actor_ids'),
|
knex.raw('coalesce(array_agg(distinct alerts_actors.actor_id) filter (where alerts_actors.actor_id is not null), \'{}\') as actor_ids'),
|
||||||
knex.raw('coalesce(array_agg(distinct alerts_entities.entity_id) filter (where alerts_entities.entity_id is not null), \'{}\') as entity_ids'),
|
knex.raw('coalesce(array_agg(distinct alerts_entities.entity_id) filter (where alerts_entities.entity_id is not null), \'{}\') as entity_ids'),
|
||||||
knex.raw('coalesce(array_agg(distinct alerts_tags.tag_id) filter (where alerts_tags.tag_id is not null), \'{}\') as tag_ids'),
|
knex.raw('coalesce(array_agg(distinct alerts_tags.tag_id) filter (where alerts_tags.tag_id is not null), \'{}\') as tag_ids'),
|
||||||
|
knex.raw('coalesce(array_agg(distinct alerts_scenes.scene_id) filter (where alerts_scenes.scene_id is not null), \'{}\') as scene_ids'),
|
||||||
)
|
)
|
||||||
.leftJoin('alerts_actors', 'alerts_actors.alert_id', 'alerts.id')
|
.leftJoin('alerts_actors', 'alerts_actors.alert_id', 'alerts.id')
|
||||||
.leftJoin('alerts_entities', 'alerts_entities.alert_id', 'alerts.id')
|
.leftJoin('alerts_entities', 'alerts_entities.alert_id', 'alerts.id')
|
||||||
.leftJoin('alerts_tags', 'alerts_tags.alert_id', 'alerts.id')
|
.leftJoin('alerts_tags', 'alerts_tags.alert_id', 'alerts.id')
|
||||||
.leftJoin('alerts_matches', 'alerts_matches.alert_id', 'alerts.id')
|
.leftJoin('alerts_matches', 'alerts_matches.alert_id', 'alerts.id')
|
||||||
|
.leftJoin('alerts_scenes', 'alerts_scenes.alert_id', 'alerts.id')
|
||||||
.groupBy('alerts.id')
|
.groupBy('alerts.id')
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
@@ -246,6 +284,7 @@ export async function removeAlert(alertId, reqUser) {
|
|||||||
alert.actor_ids.length > 0 && knex.schema.refreshMaterializedView('alerts_users_actors'),
|
alert.actor_ids.length > 0 && knex.schema.refreshMaterializedView('alerts_users_actors'),
|
||||||
alert.tag_ids.length > 0 && knex.schema.refreshMaterializedView('alerts_users_tags'),
|
alert.tag_ids.length > 0 && knex.schema.refreshMaterializedView('alerts_users_tags'),
|
||||||
alert.entity_ids?.length > 0 && knex.schema.refreshMaterializedView('alerts_users_entities'),
|
alert.entity_ids?.length > 0 && knex.schema.refreshMaterializedView('alerts_users_entities'),
|
||||||
|
alert?.scene_ids?.length > 0 && knex.schema.refreshMaterializedView('alerts_users_scenes'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return curateAlert(removed);
|
return curateAlert(removed);
|
||||||
@@ -259,6 +298,7 @@ function curateNotification(notification, scenes) {
|
|||||||
sceneId: notification.scene_id,
|
sceneId: notification.scene_id,
|
||||||
scene,
|
scene,
|
||||||
alertId: notification.alert_id,
|
alertId: notification.alert_id,
|
||||||
|
matchedSceneIds: notification.alert_scenes,
|
||||||
matchedActors: scene.actors.filter((actor) => notification.alert_actors.includes(actor.id)),
|
matchedActors: scene.actors.filter((actor) => notification.alert_actors.includes(actor.id)),
|
||||||
matchedTags: scene.tags.filter((tag) => notification.alert_tags.includes(tag.id)),
|
matchedTags: scene.tags.filter((tag) => notification.alert_tags.includes(tag.id)),
|
||||||
matchedEntity: [scene.channel, scene.network].find((entity) => notification.alert_entities.includes(entity?.id)) || null,
|
matchedEntity: [scene.channel, scene.network].find((entity) => notification.alert_entities.includes(entity?.id)) || null,
|
||||||
@@ -301,6 +341,7 @@ export async function fetchNotifications(reqUser, options = {}) {
|
|||||||
knex.raw('coalesce(array_agg(alerts_actors.actor_id) filter (where alerts_actors.id is not null), \'{}\') as alert_actors'),
|
knex.raw('coalesce(array_agg(alerts_actors.actor_id) filter (where alerts_actors.id is not null), \'{}\') as alert_actors'),
|
||||||
knex.raw('coalesce(array_agg(alerts_tags.tag_id) filter (where alerts_tags.id is not null), \'{}\') as alert_tags'),
|
knex.raw('coalesce(array_agg(alerts_tags.tag_id) filter (where alerts_tags.id is not null), \'{}\') as alert_tags'),
|
||||||
knex.raw('coalesce(array_agg(alerts_entities.entity_id) filter (where alerts_entities.id is not null), \'{}\') as alert_entities'),
|
knex.raw('coalesce(array_agg(alerts_entities.entity_id) filter (where alerts_entities.id is not null), \'{}\') as alert_entities'),
|
||||||
|
knex.raw('coalesce(array_agg(alerts_scenes.scene_id) filter (where alerts_scenes.id is not null), \'{}\') as alert_scenes'),
|
||||||
knex.raw('coalesce(json_agg(alerts_matches) filter (where alerts_matches.id is not null), \'[]\') as alert_matches'),
|
knex.raw('coalesce(json_agg(alerts_matches) filter (where alerts_matches.id is not null), \'[]\') as alert_matches'),
|
||||||
)
|
)
|
||||||
.leftJoin('alerts', 'alerts.id', 'notifications.alert_id')
|
.leftJoin('alerts', 'alerts.id', 'notifications.alert_id')
|
||||||
@@ -308,6 +349,7 @@ export async function fetchNotifications(reqUser, options = {}) {
|
|||||||
.leftJoin('alerts_tags', 'alerts_tags.alert_id', 'alerts.id')
|
.leftJoin('alerts_tags', 'alerts_tags.alert_id', 'alerts.id')
|
||||||
.leftJoin('alerts_entities', 'alerts_entities.alert_id', 'alerts.id')
|
.leftJoin('alerts_entities', 'alerts_entities.alert_id', 'alerts.id')
|
||||||
.leftJoin('alerts_matches', 'alerts_matches.alert_id', 'alerts.id')
|
.leftJoin('alerts_matches', 'alerts_matches.alert_id', 'alerts.id')
|
||||||
|
.leftJoin('alerts_scenes', 'alerts_scenes.alert_id', 'alerts.id')
|
||||||
.where('notifications.user_id', reqUser.id)
|
.where('notifications.user_id', reqUser.id)
|
||||||
.limit(options.limit)
|
.limit(options.limit)
|
||||||
.groupBy('notifications.id', 'alerts.id')
|
.groupBy('notifications.id', 'alerts.id')
|
||||||
@@ -345,7 +387,89 @@ export async function updateNotifications(updatedNotification, reqUser) {
|
|||||||
.where('user_id', reqUser.id)
|
.where('user_id', reqUser.id)
|
||||||
.update({
|
.update({
|
||||||
seen: updatedNotification.seen,
|
seen: updatedNotification.seen,
|
||||||
|
seen_at: updatedNotification.seen ? knex.fn.now() : null,
|
||||||
});
|
});
|
||||||
|
|
||||||
return updatedCount;
|
return updatedCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function alertSceneReleases() {
|
||||||
|
const alerts = await knex('alerts_scenes')
|
||||||
|
.select(
|
||||||
|
'alerts.id',
|
||||||
|
'alerts.user_id',
|
||||||
|
'releases.id as scene_id',
|
||||||
|
'releases.title as scene_title',
|
||||||
|
'releases.slug as scene_slug',
|
||||||
|
'releases.date as scene_date',
|
||||||
|
)
|
||||||
|
.leftJoin('alerts', 'alerts.id', 'alerts_scenes.alert_id')
|
||||||
|
.leftJoin('releases', 'releases.id', 'alerts_scenes.scene_id')
|
||||||
|
.where('alerts.is_expired', false)
|
||||||
|
.where('releases.date', '>=', knex.fn.now());
|
||||||
|
|
||||||
|
if (alerts.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trx = await knex.transaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await trx('notifications')
|
||||||
|
.insert(alerts.map((alert) => ({
|
||||||
|
user_id: alert.user_id,
|
||||||
|
scene_id: alert.scene_id,
|
||||||
|
alert_id: alert.id,
|
||||||
|
})));
|
||||||
|
|
||||||
|
await trx('alerts')
|
||||||
|
.whereIn('id', alerts.map((alert) => alert.id))
|
||||||
|
.update('is_expired', true);
|
||||||
|
|
||||||
|
await trx.commit();
|
||||||
|
await knex.schema.refreshMaterializedView('alerts_users_scenes');
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
await trx.rollback();
|
||||||
|
logger.error(`Failed to notify alerts: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function purgeExpiredAlerts() {
|
||||||
|
await knex('notifications')
|
||||||
|
.where((builder) => {
|
||||||
|
builder
|
||||||
|
.where('seen', true)
|
||||||
|
.where((seenBuilder) => {
|
||||||
|
seenBuilder
|
||||||
|
.whereNull('seen_at') // marked seen before seen_at was added
|
||||||
|
.orWhereRaw('seen_at < now() - interval \'? days\'', [config.alerts.seenExpiry]);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.orWhereRaw('created_at < now() - interval \'? days\'', [config.alerts.finalExpiry]) // don't accumulate notifications indefinitely
|
||||||
|
.delete();
|
||||||
|
|
||||||
|
await knex('alerts')
|
||||||
|
.where('is_expired', true)
|
||||||
|
.whereNotExists(
|
||||||
|
knex('notifications')
|
||||||
|
.whereRaw('notifications.alert_id = alerts.id'),
|
||||||
|
)
|
||||||
|
.delete();
|
||||||
|
|
||||||
|
await knex.schema.refreshMaterializedView('alerts_users_scenes');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initAlertCron() {
|
||||||
|
CronJob.from({
|
||||||
|
cronTime: config.alerts.crontab,
|
||||||
|
async onTick() {
|
||||||
|
await Promise.all([
|
||||||
|
alertSceneReleases(),
|
||||||
|
purgeExpiredAlerts(),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
start: config.alerts.enabled,
|
||||||
|
runOnInit: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { initAlertCron } from './alerts.js';
|
||||||
import { initCaches } from './cache.js';
|
import { initCaches } from './cache.js';
|
||||||
import { initSyncCron } from './sync.js';
|
import { initSyncCron } from './sync.js';
|
||||||
import initServer from './web/server.js';
|
import initServer from './web/server.js';
|
||||||
@@ -7,6 +8,7 @@ async function init() {
|
|||||||
|
|
||||||
initServer();
|
initServer();
|
||||||
initSyncCron();
|
initSyncCron();
|
||||||
|
initAlertCron();
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ function curateScene(rawScene, assets, reqUser, context) {
|
|||||||
entryId: rawScene.entry_id,
|
entryId: rawScene.entry_id,
|
||||||
date: rawScene.date,
|
date: rawScene.date,
|
||||||
datePrecision: rawScene.date_precision,
|
datePrecision: rawScene.date_precision,
|
||||||
|
isUpcoming: rawScene.date && rawScene.date - new Date() > 0,
|
||||||
createdAt: rawScene.created_at,
|
createdAt: rawScene.created_at,
|
||||||
effectiveDate: rawScene.effective_date,
|
effectiveDate: rawScene.effective_date,
|
||||||
description: censor(rawScene.description, context.restriction),
|
description: censor(rawScene.description, context.restriction),
|
||||||
@@ -131,7 +132,6 @@ function curateScene(rawScene, assets, reqUser, context) {
|
|||||||
poster: curateMedia(assets.poster, { type: 'poster' }),
|
poster: curateMedia(assets.poster, { type: 'poster' }),
|
||||||
photos: assets.photos?.map((photo) => curateMedia(photo, { type: 'photo' })) || [],
|
photos: assets.photos?.map((photo) => curateMedia(photo, { type: 'photo' })) || [],
|
||||||
caps: assets.caps?.map((cap) => curateMedia(cap, { type: 'cap' })) || [],
|
caps: assets.caps?.map((cap) => curateMedia(cap, { type: 'cap' })) || [],
|
||||||
stashes: assets.stashes?.map((stash) => curateStash(stash)) || [],
|
|
||||||
fingerprints: assets.fingerprints?.map((fingerprint) => ({
|
fingerprints: assets.fingerprints?.map((fingerprint) => ({
|
||||||
hash: fingerprint.hash,
|
hash: fingerprint.hash,
|
||||||
type: fingerprint.type,
|
type: fingerprint.type,
|
||||||
@@ -143,6 +143,11 @@ function curateScene(rawScene, assets, reqUser, context) {
|
|||||||
createdBatchId: rawScene.created_batch_id,
|
createdBatchId: rawScene.created_batch_id,
|
||||||
updatedBatchId: rawScene.updated_batch_id,
|
updatedBatchId: rawScene.updated_batch_id,
|
||||||
isNew: assets.lastBatchId === rawScene.created_batch_id,
|
isNew: assets.lastBatchId === rawScene.created_batch_id,
|
||||||
|
stashes: assets.stashes?.map((stash) => curateStash(stash)) || [],
|
||||||
|
alerts: {
|
||||||
|
only: assets.alerts?.filter((alert) => alert.is_only).flatMap((alert) => alert.alert_ids) || [],
|
||||||
|
multi: assets.alerts?.filter((alert) => !alert.is_only).flatMap((alert) => alert.alert_ids) || [],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const isVideoRestricted = config.media.videoRestrictions.includes(curatedScene.channel.slug) || config.media.videoRestrictions.includes(`_${curatedScene.network?.slug}`);
|
const isVideoRestricted = config.media.videoRestrictions.includes(curatedScene.channel.slug) || config.media.videoRestrictions.includes(`_${curatedScene.network?.slug}`);
|
||||||
@@ -179,6 +184,7 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
|||||||
teasers,
|
teasers,
|
||||||
fingerprints,
|
fingerprints,
|
||||||
stashes,
|
stashes,
|
||||||
|
alerts,
|
||||||
lastBatch,
|
lastBatch,
|
||||||
} = await promiseProps({
|
} = await promiseProps({
|
||||||
scenes: knex('releases').whereIn('releases.id', sceneIds),
|
scenes: knex('releases').whereIn('releases.id', sceneIds),
|
||||||
@@ -356,6 +362,11 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
|||||||
.where('stashes.user_id', reqUser.id)
|
.where('stashes.user_id', reqUser.id)
|
||||||
.whereIn('stashes_scenes.scene_id', sceneIds)
|
.whereIn('stashes_scenes.scene_id', sceneIds)
|
||||||
: [],
|
: [],
|
||||||
|
alerts: reqUser
|
||||||
|
? knex('alerts_users_scenes')
|
||||||
|
.where('user_id', reqUser.id)
|
||||||
|
.whereIn('scene_id', sceneIds)
|
||||||
|
: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const actorStashes = reqUser && context.actorStashes
|
const actorStashes = reqUser && context.actorStashes
|
||||||
@@ -388,6 +399,7 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
|||||||
const sceneFingerprints = fingerprints.filter((fingerprint) => fingerprint.scene_id === sceneId);
|
const sceneFingerprints = fingerprints.filter((fingerprint) => fingerprint.scene_id === sceneId);
|
||||||
const sceneStashes = stashes.filter((stash) => stash.scene_id === sceneId);
|
const sceneStashes = stashes.filter((stash) => stash.scene_id === sceneId);
|
||||||
const sceneActorStashes = sceneActors.map((actor) => actorStashes.find((stash) => stash.actor_id === actor.id)).filter(Boolean);
|
const sceneActorStashes = sceneActors.map((actor) => actorStashes.find((stash) => stash.actor_id === actor.id)).filter(Boolean);
|
||||||
|
const sceneAlerts = alerts.filter((alert) => alert.scene_id === sceneId);
|
||||||
|
|
||||||
return curateScene(scene, {
|
return curateScene(scene, {
|
||||||
channel: sceneChannel,
|
channel: sceneChannel,
|
||||||
@@ -406,6 +418,7 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
|||||||
fingerprints: sceneFingerprints,
|
fingerprints: sceneFingerprints,
|
||||||
stashes: sceneStashes,
|
stashes: sceneStashes,
|
||||||
actorStashes: sceneActorStashes,
|
actorStashes: sceneActorStashes,
|
||||||
|
alerts: sceneAlerts,
|
||||||
lastBatchId: lastBatch?.id,
|
lastBatchId: lastBatch?.id,
|
||||||
}, reqUser, context);
|
}, reqUser, context);
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export async function syncStashes(domain = 'scene', ids) {
|
|||||||
const docs = searchResponse?.hits?.hits ?? [];
|
const docs = searchResponse?.hits?.hits ?? [];
|
||||||
|
|
||||||
const orphanedIds = docs
|
const orphanedIds = docs
|
||||||
.map((hit) => hit.id)
|
.map((hit) => hit._id)
|
||||||
.filter((manticoreId) => !validStashedIds.has(manticoreId));
|
.filter((manticoreId) => !validStashedIds.has(manticoreId));
|
||||||
|
|
||||||
if (orphanedIds.length === 0) {
|
if (orphanedIds.length === 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user