Added georestriction with SFW mode.

This commit is contained in:
2026-02-04 05:39:14 +01:00
parent ce107e6b65
commit 1a84f899e7
35 changed files with 777 additions and 112 deletions

View File

@@ -21,7 +21,7 @@
"no-console": 0, "no-console": 0,
"no-param-reassign": ["error", { "no-param-reassign": ["error", {
"props": true, "props": true,
"ignorePropertyModificationsFor": ["state", "acc"] "ignorePropertyModificationsFor": ["state", "acc", "req"]
}], }],
"vue/multi-word-component-names": 0, "vue/multi-word-component-names": 0,
"vue/no-reserved-component-names": 0, "vue/no-reserved-component-names": 0,
@@ -32,7 +32,8 @@
"vue/html-indent": ["error", "tab"], "vue/html-indent": ["error", "tab"],
"vue/multiline-html-element-content-newline": 0, "vue/multiline-html-element-content-newline": 0,
"vue/no-v-html": 0, "vue/no-v-html": 0,
"vue/singleline-html-element-content-newline": 0 "vue/singleline-html-element-content-newline": 0,
"vue/comment-directive": 0,
}, },
"settings": { "settings": {
"import/resolver": { "import/resolver": {

2
.gitignore vendored
View File

@@ -5,3 +5,5 @@ config/*
log/ log/
/media /media
data/ data/
assets/*.mmdb
assets/.geoipupdate.lock

125
assets/sfw.ejs Normal file
View File

@@ -0,0 +1,125 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="apple-touch-icon" sizes="180x180" href="/img/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon/favicon-16x16.png">
<link rel="manifest" href="/img/favicon/site.webmanifest">
<link rel="mask-icon" href="/img/favicon/safari-pinned-tab.svg" color="#5bbad5">
<link rel="shortcut icon" href="/img/favicon/favicon.ico">
<meta name="msapplication-TileColor" content="#b91d47">
<meta name="msapplication-config" content="/img/favicon/browserconfig.xml">
<meta name="theme-color" content="#f65596">
<meta property="og:title" content="traxxx" />
<meta property="og:image" content="https://traxxx.me/img/og_logo.png" />
<meta name="viewport" content="width=device-width,height=device-height,initial-scale=1.0,interactive-widget=resizes-content" />
<title>traxxx - None shall pass</title>
<style>
:root {
--primary-dark-10: #e54485;
--primary: #f65596;
--primary-light-10: #f075a6;
--primary-light-20: #f2a6c4;
--primary-light-30: #f7c9dc;
}
html,
body {
height: 100%;
margin: 0;
}
.content {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
box-sizing: border-box;
padding: 1rem;
}
.explainer {
margin-bottom: 3rem;
font-size: 1.25rem;
text-align: justify;
line-height: 1.5;
}
.useful {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.links {
display: flex;
gap: 1rem;
list-style: none;
padding: 0;
margin: 1rem 0 0 0;
}
.links li {
padding: .25rem 0;
margin: 0;
font-size: 1.25rem;
}
.links a {
padding: .5rem 1rem;
border-radius: .5rem;
background: var(--primary);
color: white;
text-decoration: none;
white-space: nowrap;
flex-shrink: 0;
}
.links a:hover {
background: var(--primary-dark-10);
}
</style>
</head>
<body>
<div class="content">
<h2 class="heading">Not so fast, rascal.</h2>
<p class="explainer">The content offered by traxxx is restricted in your jurisdiction.</p>
<% if (!noVpn) { %>
<div class="useful">
Useful links:
<ul class="links">
<li>
<a
href="https://mullvad.net/"
target="_blank"
rel="noopener"
class="link"
>Mullvad VPN</a>
</li>
<li>
<a
href="https://protonvpn.com/"
target="_blank"
rel="noopener"
class="link"
>Proton VPN</a>
</li>
</ul>
</div>
<% } %>
</div>
</body>
</html>

View File

@@ -11,7 +11,7 @@
class="avatar-link no-link" class="avatar-link no-link"
> >
<img <img
v-if="actor.avatar" v-if="actor.avatar && !restriction"
:src="getPath(actor.avatar, 'thumbnail')" :src="getPath(actor.avatar, 'thumbnail')"
:style="{ 'background-image': `url(${getPath(actor.avatar, 'lazy')})` }" :style="{ 'background-image': `url(${getPath(actor.avatar, 'lazy')})` }"
loading="lazy" loading="lazy"
@@ -103,7 +103,7 @@ const props = defineProps({
}); });
const pageContext = inject('pageContext'); const pageContext = inject('pageContext');
const { user } = pageContext; const { user, restriction } = pageContext;
const pageStash = pageContext.pageProps.stash; const pageStash = pageContext.pageProps.stash;
const currentStash = pageStash || pageContext.assets?.primaryStash; const currentStash = pageStash || pageContext.assets?.primaryStash;

View File

@@ -1,6 +1,17 @@
<template> <template>
<div
v-if="restriction"
class="restricted"
>
<div>Traxxx is restricted in your region</div>
<a
href="/sfw"
class="link"
>Learn more</a>
</div>
<iframe <iframe
v-if="campaign?.banner?.type === 'html'" v-else-if="campaign?.banner?.type === 'html'"
ref="iframe" ref="iframe"
:width="campaign.banner.width" :width="campaign.banner.width"
:height="campaign.banner.height" :height="campaign.banner.height"
@@ -31,6 +42,11 @@
</template> </template>
<script setup> <script setup>
import { inject } from 'vue';
const pageContext = inject('pageContext');
const { restriction } = pageContext;
const props = defineProps({ const props = defineProps({
campaign: { campaign: {
type: Object, type: Object,
@@ -75,4 +91,15 @@ const bannerSrc = (() => {
max-width: 100%; max-width: 100%;
object-fit: contain; object-fit: contain;
} }
.restricted {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: .5rem;
font-weight: bold;
padding: .5rem;
}
</style> </style>

View File

@@ -130,7 +130,7 @@ const props = defineProps({
}); });
const pageContext = inject('pageContext'); const pageContext = inject('pageContext');
const user = pageContext.user; const { user, restriction } = pageContext;
const pageStash = pageContext.pageProps.stash; const pageStash = pageContext.pageProps.stash;
const currentStash = pageStash || pageContext.assets?.primaryStash; const currentStash = pageStash || pageContext.assets?.primaryStash;

View File

@@ -9,14 +9,13 @@
v-for="photo in photos" v-for="photo in photos"
:key="`photo-${photo.id}`" :key="`photo-${photo.id}`"
:title="photo.comment" :title="photo.comment"
:href="`/img/${photo.path}`"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="photo-container" class="photo-container"
> >
<img <img
:src="`/${photo.thumbnail}`" :src="getPath(photo, 'thumbnail')"
:style="{ 'background-image': `url(/${photo.lazy})` }" :style="{ 'background-image': `url(${getPath(photo, 'lazy')})` }"
:alt="photo.comment" :alt="photo.comment"
:width="photo.width" :width="photo.width"
:height="photo.height" :height="photo.height"
@@ -47,6 +46,8 @@ import { computed, inject } from 'vue';
import Logo from '#/components/tags/logo.vue'; import Logo from '#/components/tags/logo.vue';
import Campaign from '#/components/campaigns/campaign.vue'; import Campaign from '#/components/campaigns/campaign.vue';
import getPath from '#/src/get-path.js';
const props = defineProps({ const props = defineProps({
tag: { tag: {
type: Object, type: Object,

View File

@@ -58,6 +58,91 @@ module.exports = {
address: 'http://localhost:3000/script.js', address: 'http://localhost:3000/script.js',
siteId: '1b28ac3b-d229-43bf-aec9-75cf0a72a466', siteId: '1b28ac3b-d229-43bf-aec9-75cf0a72a466',
}, },
restrictions: {
enabled: false,
modes: [
null, // easier for 0 to mean disabled
'block', // 1
'censor', // 2
],
regions: {
// Europe
DE: 1, // Germany
FR: 1, // France
GB: 1, // Great Britain / United Kingdom
IT: 1, // Italy
// Asia & Oceania
AU: 1, // Australia
CN: 1, // China
// Americas
US: {
AL: 1, // Alabama
AR: 1, // Arkansas
AZ: 1, // Arizona
FL: 1, // Florida
GA: 1, // Georgia
ID: 1, // Idaho
IN: 1, // Indiana
KS: 1, // Kansas
KY: 1, // Kentucky
LA: 1, // Louisiana
MO: 1, // Missouri
MS: 1, // Mississippi
MT: 1, // Montana
NC: 1, // North Carolina
ND: 1, // North Dakota
NE: 1, // Nebraska
OH: 1, // Ohio
OK: 1, // Oklahoma
SC: 1, // South Carolina
SD: 1, // South Dakota
TN: 1, // Tennessee
TX: 1, // Texas
UT: 1, // Utah
VA: 1, // Virginia
WY: 1, // Wyoming
}, // only Florida
},
noVpn: [
'AE', // United Arab Emirates
'BY', // Belarus
'CN', // China
'IQ', // Iraq
'IR', // Iran
'KP', // North Korea
'OM', // Oman
'RU', // Russia
'TM', // Turkmenistan
'TR', // Turkey
],
censors: [ // additional to default filter
'ball',
'bisexual',
'blow',
'blowbang',
'condom',
'cowgirl',
'creampie',
'doggy',
'facial',
'finger',
'gay',
'hole',
'horny',
'lesbian',
'masturbation',
'milf',
'missionary',
'prolapse',
'nymph',
'sex',
'swallowing',
'squirt',
'sucking',
'threesome',
'trans',
],
},
auth: { auth: {
login: true, login: true,
signup: true, signup: true,
@@ -101,7 +186,7 @@ module.exports = {
}, },
media: { media: {
path: './media', path: './media',
assetPath: '/img', assetPath: '',
mediaPath: '/media', mediaPath: '/media',
s3Path: 'https://s3.wasabisys.com', s3Path: 'https://s3.wasabisys.com',
videoRestrictions: [], // entity slugs, _ prefix for networks, hides trailer and teaser videos videoRestrictions: [], // entity slugs, _ prefix for networks, hides trailer and teaser videos

205
package-lock.json generated
View File

@@ -14,6 +14,7 @@
"@floating-ui/dom": "^1.5.3", "@floating-ui/dom": "^1.5.3",
"@floating-ui/vue": "^1.0.2", "@floating-ui/vue": "^1.0.2",
"@hcaptcha/vue3-hcaptcha": "^1.3.0", "@hcaptcha/vue3-hcaptcha": "^1.3.0",
"@maxmind/geoip2-node": "^6.3.4",
"@resvg/resvg-js": "^2.6.0", "@resvg/resvg-js": "^2.6.0",
"@toycode/markdown-it-class": "^1.2.4", "@toycode/markdown-it-class": "^1.2.4",
"@vitejs/plugin-vue": "^4.5.2", "@vitejs/plugin-vue": "^4.5.2",
@@ -29,6 +30,7 @@
"cron": "^3.1.6", "cron": "^3.1.6",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"date-fns": "^3.0.0", "date-fns": "^3.0.0",
"ejs": "^4.0.1",
"error-stack-parser": "^2.1.4", "error-stack-parser": "^2.1.4",
"escape-string-regexp": "^5.0.0", "escape-string-regexp": "^5.0.0",
"express": "^4.18.2", "express": "^4.18.2",
@@ -52,6 +54,7 @@
"mysql": "^2.18.1", "mysql": "^2.18.1",
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"object.omit": "^3.0.0", "object.omit": "^3.0.0",
"obscenity": "^0.4.6",
"path-to-regexp": "^6.2.1", "path-to-regexp": "^6.2.1",
"pg": "^8.11.3", "pg": "^8.11.3",
"redis": "^4.6.12", "redis": "^4.6.12",
@@ -3224,6 +3227,15 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@maxmind/geoip2-node": {
"version": "6.3.4",
"resolved": "https://registry.npmjs.org/@maxmind/geoip2-node/-/geoip2-node-6.3.4.tgz",
"integrity": "sha512-BTRFHCX7Uie4wVSPXsWQfg0EVl4eGZgLCts0BTKAP+Eiyt1zmF2UPyuUZkaj0R59XSDYO+84o1THAtaenUoQYg==",
"license": "Apache-2.0",
"dependencies": {
"maxmind": "^5.0.0"
}
},
"node_modules/@modyfi/vite-plugin-yaml": { "node_modules/@modyfi/vite-plugin-yaml": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@modyfi/vite-plugin-yaml/-/vite-plugin-yaml-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@modyfi/vite-plugin-yaml/-/vite-plugin-yaml-1.1.0.tgz",
@@ -4527,9 +4539,10 @@
"peer": true "peer": true
}, },
"node_modules/async": { "node_modules/async": {
"version": "3.2.5", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"license": "MIT"
}, },
"node_modules/asynckit": { "node_modules/asynckit": {
"version": "0.4.0", "version": "0.4.0",
@@ -5577,6 +5590,21 @@
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
}, },
"node_modules/ejs": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-4.0.1.tgz",
"integrity": "sha512-krvQtxc0btwSm/nvnt1UpnaFDFVJpJ0fdckmALpCgShsr/iGYHTnJiUliZTgmzq/UxTX33TtOQVKaNigMQp/6Q==",
"license": "Apache-2.0",
"dependencies": {
"jake": "^10.9.1"
},
"bin": {
"ejs": "bin/cli.js"
},
"engines": {
"node": ">=0.12.18"
}
},
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.4.616", "version": "1.4.616",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz",
@@ -6790,6 +6818,36 @@
"moment": "^2.29.1" "moment": "^2.29.1"
} }
}, },
"node_modules/filelist": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
"license": "Apache-2.0",
"dependencies": {
"minimatch": "^5.0.1"
}
},
"node_modules/filelist/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/filelist/node_modules/minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -8045,6 +8103,23 @@
"@pkgjs/parseargs": "^0.11.0" "@pkgjs/parseargs": "^0.11.0"
} }
}, },
"node_modules/jake": {
"version": "10.9.4",
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz",
"integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==",
"license": "Apache-2.0",
"dependencies": {
"async": "^3.2.6",
"filelist": "^1.0.4",
"picocolors": "^1.1.1"
},
"bin": {
"jake": "bin/cli.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/javascript-natural-sort": { "node_modules/javascript-natural-sort": {
"version": "0.7.1", "version": "0.7.1",
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
@@ -8510,6 +8585,20 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/maxmind": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/maxmind/-/maxmind-5.0.5.tgz",
"integrity": "sha512-1lcH2kMjbBpCFhuHaMU32vz8CuOsKttRcWMQyXvtlklopCzN7NNHSVR/h9RYa8JPuFTGmkn2vYARm+7cIGuqDw==",
"license": "MIT",
"dependencies": {
"mmdb-lib": "3.0.2",
"tiny-lru": "11.4.7"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/mdurl": { "node_modules/mdurl": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
@@ -8653,6 +8742,16 @@
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
}, },
"node_modules/mmdb-lib": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/mmdb-lib/-/mmdb-lib-3.0.2.tgz",
"integrity": "sha512-7e87vk0DdWT647wjcfEtWeMtjm+zVGqNohN/aeIymbUfjHQ2T4Sx5kM+1irVDBSloNC3CkGKxswdMoo8yhqTDg==",
"license": "MIT",
"engines": {
"node": ">=10",
"npm": ">=6"
}
},
"node_modules/moment": { "node_modules/moment": {
"version": "2.30.1", "version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
@@ -8991,6 +9090,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/obscenity": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/obscenity/-/obscenity-0.4.6.tgz",
"integrity": "sha512-pHk7kNN7j3L3zGhhGnwxjvXIGsPpLrcZl2r58fqWh/V/rH6b/dafscj2sMmAY+A/9/wPsocLmimgGk2DKeKsFQ==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/on-finished": { "node_modules/on-finished": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@ -10734,6 +10842,15 @@
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
}, },
"node_modules/tiny-lru": {
"version": "11.4.7",
"resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.4.7.tgz",
"integrity": "sha512-w/Te7uMUVeH0CR8vZIjr+XiN41V+30lkDdK+NRIDCUYKKuL9VcmaUEmaPISuwGhLlrTGh5yu18lENtR9axSxYw==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/tmp": { "node_modules/tmp": {
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.2.tgz", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.2.tgz",
@@ -13920,6 +14037,14 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"@maxmind/geoip2-node": {
"version": "6.3.4",
"resolved": "https://registry.npmjs.org/@maxmind/geoip2-node/-/geoip2-node-6.3.4.tgz",
"integrity": "sha512-BTRFHCX7Uie4wVSPXsWQfg0EVl4eGZgLCts0BTKAP+Eiyt1zmF2UPyuUZkaj0R59XSDYO+84o1THAtaenUoQYg==",
"requires": {
"maxmind": "^5.0.0"
}
},
"@modyfi/vite-plugin-yaml": { "@modyfi/vite-plugin-yaml": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@modyfi/vite-plugin-yaml/-/vite-plugin-yaml-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@modyfi/vite-plugin-yaml/-/vite-plugin-yaml-1.1.0.tgz",
@@ -14759,9 +14884,9 @@
"peer": true "peer": true
}, },
"async": { "async": {
"version": "3.2.5", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="
}, },
"asynckit": { "asynckit": {
"version": "0.4.0", "version": "0.4.0",
@@ -15518,6 +15643,14 @@
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
}, },
"ejs": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-4.0.1.tgz",
"integrity": "sha512-krvQtxc0btwSm/nvnt1UpnaFDFVJpJ0fdckmALpCgShsr/iGYHTnJiUliZTgmzq/UxTX33TtOQVKaNigMQp/6Q==",
"requires": {
"jake": "^10.9.1"
}
},
"electron-to-chromium": { "electron-to-chromium": {
"version": "1.4.616", "version": "1.4.616",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz",
@@ -16434,6 +16567,32 @@
"moment": "^2.29.1" "moment": "^2.29.1"
} }
}, },
"filelist": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
"requires": {
"minimatch": "^5.0.1"
},
"dependencies": {
"brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"requires": {
"balanced-match": "^1.0.0"
}
},
"minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"requires": {
"brace-expansion": "^2.0.1"
}
}
}
},
"fill-range": { "fill-range": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -17277,6 +17436,16 @@
"@pkgjs/parseargs": "^0.11.0" "@pkgjs/parseargs": "^0.11.0"
} }
}, },
"jake": {
"version": "10.9.4",
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz",
"integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==",
"requires": {
"async": "^3.2.6",
"filelist": "^1.0.4",
"picocolors": "^1.1.1"
}
},
"javascript-natural-sort": { "javascript-natural-sort": {
"version": "0.7.1", "version": "0.7.1",
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
@@ -17637,6 +17806,15 @@
"typed-function": "^4.1.1" "typed-function": "^4.1.1"
} }
}, },
"maxmind": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/maxmind/-/maxmind-5.0.5.tgz",
"integrity": "sha512-1lcH2kMjbBpCFhuHaMU32vz8CuOsKttRcWMQyXvtlklopCzN7NNHSVR/h9RYa8JPuFTGmkn2vYARm+7cIGuqDw==",
"requires": {
"mmdb-lib": "3.0.2",
"tiny-lru": "11.4.7"
}
},
"mdurl": { "mdurl": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
@@ -17738,6 +17916,11 @@
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
}, },
"mmdb-lib": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/mmdb-lib/-/mmdb-lib-3.0.2.tgz",
"integrity": "sha512-7e87vk0DdWT647wjcfEtWeMtjm+zVGqNohN/aeIymbUfjHQ2T4Sx5kM+1irVDBSloNC3CkGKxswdMoo8yhqTDg=="
},
"moment": { "moment": {
"version": "2.30.1", "version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
@@ -17989,6 +18172,11 @@
"es-object-atoms": "^1.0.0" "es-object-atoms": "^1.0.0"
} }
}, },
"obscenity": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/obscenity/-/obscenity-0.4.6.tgz",
"integrity": "sha512-pHk7kNN7j3L3zGhhGnwxjvXIGsPpLrcZl2r58fqWh/V/rH6b/dafscj2sMmAY+A/9/wPsocLmimgGk2DKeKsFQ=="
},
"on-finished": { "on-finished": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@ -19227,6 +19415,11 @@
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
}, },
"tiny-lru": {
"version": "11.4.7",
"resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.4.7.tgz",
"integrity": "sha512-w/Te7uMUVeH0CR8vZIjr+XiN41V+30lkDdK+NRIDCUYKKuL9VcmaUEmaPISuwGhLlrTGh5yu18lENtR9axSxYw=="
},
"tmp": { "tmp": {
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.2.tgz", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.2.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"scripts": { "scripts": {
"dev": "npm run server:dev", "dev": "node ./src/app",
"prod": "npm run build && npm run server:prod", "prod": "npm run build && npm run server:prod",
"build": "vite build", "build": "vite build",
"server:dev": "node ./src/app", "server:dev": "node ./src/app",
@@ -14,6 +14,7 @@
"@floating-ui/dom": "^1.5.3", "@floating-ui/dom": "^1.5.3",
"@floating-ui/vue": "^1.0.2", "@floating-ui/vue": "^1.0.2",
"@hcaptcha/vue3-hcaptcha": "^1.3.0", "@hcaptcha/vue3-hcaptcha": "^1.3.0",
"@maxmind/geoip2-node": "^6.3.4",
"@resvg/resvg-js": "^2.6.0", "@resvg/resvg-js": "^2.6.0",
"@toycode/markdown-it-class": "^1.2.4", "@toycode/markdown-it-class": "^1.2.4",
"@vitejs/plugin-vue": "^4.5.2", "@vitejs/plugin-vue": "^4.5.2",
@@ -29,6 +30,7 @@
"cron": "^3.1.6", "cron": "^3.1.6",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"date-fns": "^3.0.0", "date-fns": "^3.0.0",
"ejs": "^4.0.1",
"error-stack-parser": "^2.1.4", "error-stack-parser": "^2.1.4",
"escape-string-regexp": "^5.0.0", "escape-string-regexp": "^5.0.0",
"express": "^4.18.2", "express": "^4.18.2",
@@ -52,6 +54,7 @@
"mysql": "^2.18.1", "mysql": "^2.18.1",
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"object.omit": "^3.0.0", "object.omit": "^3.0.0",
"obscenity": "^0.4.6",
"path-to-regexp": "^6.2.1", "path-to-regexp": "^6.2.1",
"pg": "^8.11.3", "pg": "^8.11.3",
"redis": "^4.6.12", "redis": "^4.6.12",

View File

@@ -3,7 +3,9 @@ import { fetchEntities } from '#/src/entities.js';
export async function onBeforeRender(pageContext) { export async function onBeforeRender(pageContext) {
const networks = await fetchEntities(pageContext.urlParsed.search.q const networks = await fetchEntities(pageContext.urlParsed.search.q
? { query: pageContext.urlParsed.search.q } ? { query: pageContext.urlParsed.search.q }
: { type: 'primary' }); : { type: 'primary' }, {
restriction: pageContext.restriction,
});
return { return {
pageContext: { pageContext: {

View File

@@ -20,7 +20,9 @@ async function fetchReleases(pageContext, entityId) {
page: Number(pageContext.routeParams.page) || 1, page: Number(pageContext.routeParams.page) || 1,
limit: Number(pageContext.urlParsed.search.limit) || 30, limit: Number(pageContext.urlParsed.search.limit) || 30,
aggregate: true, aggregate: true,
}, pageContext.user); }, pageContext.user, {
restriction: pageContext.restriction,
});
} }
return fetchScenes(await curateScenesQuery({ return fetchScenes(await curateScenesQuery({
@@ -32,7 +34,9 @@ async function fetchReleases(pageContext, entityId) {
page: Number(pageContext.routeParams.page) || 1, page: Number(pageContext.routeParams.page) || 1,
limit: Number(pageContext.urlParsed.search.limit) || 30, limit: Number(pageContext.urlParsed.search.limit) || 30,
aggregate: true, aggregate: true,
}, pageContext.user); }, pageContext.user, {
restriction: pageContext.restriction,
});
} }
export async function onBeforeRender(pageContext) { export async function onBeforeRender(pageContext) {
@@ -47,7 +51,9 @@ export async function onBeforeRender(pageContext) {
[entity], [entity],
entityReleases, entityReleases,
] = await Promise.all([ ] = await Promise.all([
fetchEntitiesById([Number(entityId)], { includeChildren: true }, pageContext.user), fetchEntitiesById([Number(entityId)], { includeChildren: true }, pageContext.user, {
restriction: pageContext.restriction,
}),
fetchReleases(pageContext, entityId), fetchReleases(pageContext, entityId),
]); ]);

View File

@@ -9,7 +9,9 @@ export async function onBeforeRender(pageContext) {
page: Number(pageContext.routeParams.page) || 1, page: Number(pageContext.routeParams.page) || 1,
limit: Number(pageContext.urlParsed.search.limit) || 50, limit: Number(pageContext.urlParsed.search.limit) || 50,
dedupe: true, dedupe: true,
}, pageContext.user); }, pageContext.user, {
restriction: pageContext.restriction,
});
return { return {
pageContext: { pageContext: {

View File

@@ -18,7 +18,7 @@ function getTitle(movie) {
export async function onBeforeRender(pageContext) { export async function onBeforeRender(pageContext) {
const [[movie], movieScenes] = await Promise.all([ const [[movie], movieScenes] = await Promise.all([
fetchMoviesById([Number(pageContext.routeParams.movieId)], pageContext.user), fetchMoviesById([Number(pageContext.routeParams.movieId)], pageContext.user, { restriction: pageContext.restriction }),
fetchScenes(await curateScenesQuery({ fetchScenes(await curateScenesQuery({
...pageContext.urlQuery, ...pageContext.urlQuery,
scope: 'oldest', scope: 'oldest',
@@ -27,7 +27,9 @@ export async function onBeforeRender(pageContext) {
page: Number(pageContext.routeParams.page) || 1, page: Number(pageContext.routeParams.page) || 1,
limit: Number(pageContext.urlParsed.search.limit) || 30, limit: Number(pageContext.urlParsed.search.limit) || 30,
aggregate: true, aggregate: true,
}, pageContext.user), }, pageContext.user, {
restriction: pageContext.restriction,
}),
]); ]);
if (!movie) { if (!movie) {

View File

@@ -25,6 +25,7 @@ export async function onBeforeRender(pageContext) {
includeAssets: true, includeAssets: true,
includePartOf: true, includePartOf: true,
actorStashes: true, actorStashes: true,
restriction: pageContext.restriction,
}); });
const [campaigns, tagIds] = await Promise.all([ const [campaigns, tagIds] = await Promise.all([

View File

@@ -17,7 +17,9 @@ export async function onBeforeRender(pageContext) {
limit: Number(pageContext.urlParsed.search.limit) || 29, limit: Number(pageContext.urlParsed.search.limit) || 29,
aggregate: true, aggregate: true,
dedupe: true, dedupe: true,
}, pageContext.user), }, pageContext.user, {
restriction: pageContext.restriction,
}),
getRandomCampaigns([ getRandomCampaigns([
{ minRatio: 0.75, maxRatio: 1.25 }, { minRatio: 0.75, maxRatio: 1.25 },
{ minRatio: 1.5 }, { minRatio: 1.5 },

View File

@@ -75,8 +75,8 @@
> >
<img <img
v-if="tag.poster" v-if="tag.poster"
:src="`/${tag.poster.thumbnail}`" :src="getPath(tag.poster, 'thumbnail')"
:style="{ 'background-image': `url(/${tag.poster.lazy})` }" :style="{ 'background-image': `url(/${getPath(tag.poster, 'lazy')})` }"
:title="tag.poster.comment" :title="tag.poster.comment"
class="thumb" class="thumb"
loading="lazy" loading="lazy"
@@ -111,6 +111,7 @@ import { ref, onMounted, inject } from 'vue';
import navigate from '#/src/navigate.js'; import navigate from '#/src/navigate.js';
import events from '#/src/events.js'; import events from '#/src/events.js';
import getPath from '#/src/get-path.js';
import Logo from '#/components/tags/logo.vue'; import Logo from '#/components/tags/logo.vue';
@@ -324,6 +325,7 @@ onMounted(() => {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
aspect-ratio: 5/3;
border-radius: .25rem; border-radius: .25rem;
background-size: cover; background-size: cover;
background-position: center; background-position: center;

View File

@@ -1,4 +1,5 @@
import { fetchTags, fetchTagsById } from '#/src/tags.js'; import { fetchTags, fetchTagsById } from '#/src/tags.js';
import { censor } from '#/src/censor.js';
const tagSlugs = { const tagSlugs = {
popular: [ popular: [
@@ -136,13 +137,13 @@ export async function onBeforeRender(pageContext) {
return searchTags(pageContext); return searchTags(pageContext);
} }
const tags = await fetchTagsById(Object.values(tagSlugs).flat()); const tags = await fetchTagsById(Object.values(tagSlugs).flat(), {}, pageContext.user, { restriction: pageContext.restriction });
const filteredTags = tags.filter((tag) => !pageContext.tagFilter.includes(tag.name) && !pageContext.tagFilter.includes(tag.slug)); const filteredTags = tags.filter((tag) => !pageContext.tagFilter.includes(tag.name) && !pageContext.tagFilter.includes(tag.slug));
const tagsBySlug = Object.fromEntries(filteredTags.map((tag) => [tag.slug, tag])); const tagsBySlug = Object.fromEntries(filteredTags.map((tag) => [tag.slug, tag]));
const tagShowcase = Object.fromEntries(Object.entries(tagSlugs).map(([category, categorySlugs]) => [ const tagShowcase = Object.fromEntries(Object.entries(tagSlugs).map(([category, categorySlugs]) => [
category, censor(category, pageContext.restriction),
categorySlugs.map((slug) => tagsBySlug[slug]).filter(Boolean), categorySlugs.map((slug) => tagsBySlug[slug]).filter(Boolean),
])); ]));

View File

@@ -21,7 +21,9 @@ async function fetchReleases(pageContext) {
page: Number(pageContext.routeParams.page) || 1, page: Number(pageContext.routeParams.page) || 1,
limit: Number(pageContext.urlParsed.search.limit) || 30, limit: Number(pageContext.urlParsed.search.limit) || 30,
aggregate: true, aggregate: true,
}, pageContext.user); }, pageContext.user, {
restriction: pageContext.restriction,
});
} }
return fetchScenes(await curateScenesQuery({ return fetchScenes(await curateScenesQuery({
@@ -33,14 +35,16 @@ async function fetchReleases(pageContext) {
page: Number(pageContext.routeParams.page) || 1, page: Number(pageContext.routeParams.page) || 1,
limit: Number(pageContext.urlParsed.search.limit) || 30, limit: Number(pageContext.urlParsed.search.limit) || 30,
aggregate: true, aggregate: true,
}, pageContext.user); }, pageContext.user, {
restriction: pageContext.restriction,
});
} }
export async function onBeforeRender(pageContext) { export async function onBeforeRender(pageContext) {
const tagSlug = pageContext.routeParams.tagSlug; const tagSlug = pageContext.routeParams.tagSlug;
const [[tag], tagReleases, campaigns] = await Promise.all([ const [[tag], tagReleases, campaigns] = await Promise.all([
fetchTagsById([tagSlug], {}, pageContext.user), fetchTagsById([tagSlug], {}, pageContext.user, { restriction: pageContext.restriction }),
fetchReleases(pageContext), fetchReleases(pageContext),
getRandomCampaigns([ getRandomCampaigns([
{ tagSlugs: [tagSlug], minRatio: 0.75, maxRatio: 1.25 }, { tagSlugs: [tagSlug], minRatio: 0.75, maxRatio: 1.25 },

View File

@@ -19,7 +19,7 @@ export async function onBeforeRender(pageContext) {
limit: Number(pageContext.urlParsed.search.limit) || 29, limit: Number(pageContext.urlParsed.search.limit) || 29,
aggregate: withQuery, aggregate: withQuery,
dedupe: true, dedupe: true,
}, pageContext.user), }, pageContext.user, { restriction: pageContext.restriction }),
getRandomCampaigns([ getRandomCampaigns([
{ minRatio: 2.0, maxRatio: 5 }, { minRatio: 2.0, maxRatio: 5 },
{ minRatio: 0.75, maxRatio: 1.25 }, { minRatio: 0.75, maxRatio: 1.25 },

View File

@@ -9,5 +9,6 @@ export default {
'assets', 'assets',
'campaigns', 'campaigns',
'meta', 'meta',
'restriction',
], ],
}; };

View File

@@ -122,7 +122,10 @@ export function curateActor(actor, context = {}) {
state: actor.residence_state, state: actor.residence_state,
}, },
agency: actor.agency, agency: actor.agency,
avatar: curateMedia(actor.avatar), avatar: actor.avatar && curateMedia({
...actor.avatar,
sfw_media: actor.sfw_avatar,
}),
socials: context.socials?.map((social) => ({ socials: context.socials?.map((social) => ({
id: social.id, id: social.id,
url: social.url, url: social.url,
@@ -214,18 +217,21 @@ export async function fetchActorsById(actorIds, options = {}, reqUser) {
'residence_countries.alpha2 as residence_country_alpha2', 'residence_countries.alpha2 as residence_country_alpha2',
knex.raw('COALESCE(residence_countries.alias, residence_countries.name) as residence_country_name'), knex.raw('COALESCE(residence_countries.alias, residence_countries.name) as residence_country_name'),
knex.raw('row_to_json(entities) as entity'), knex.raw('row_to_json(entities) as entity'),
knex.raw('row_to_json(sfw_media) as sfw_avatar'),
) )
.leftJoin('actors_meta', 'actors_meta.actor_id', 'actors.id') .leftJoin('actors_meta', 'actors_meta.actor_id', 'actors.id')
.leftJoin('countries as birth_countries', 'birth_countries.alpha2', 'actors.birth_country_alpha2') .leftJoin('countries as birth_countries', 'birth_countries.alpha2', 'actors.birth_country_alpha2')
.leftJoin('countries as residence_countries', 'residence_countries.alpha2', 'actors.residence_country_alpha2') .leftJoin('countries as residence_countries', 'residence_countries.alpha2', 'actors.residence_country_alpha2')
.leftJoin('media as avatars', 'avatars.id', 'actors.avatar_media_id') .leftJoin('media as avatars', 'avatars.id', 'actors.avatar_media_id')
.leftJoin('media as sfw_media', 'sfw_media.id', 'avatars.sfw_media_id')
.leftJoin('entities', 'entities.id', 'actors.entity_id') .leftJoin('entities', 'entities.id', 'actors.entity_id')
.whereIn('actors.id', actorIds) .whereIn('actors.id', actorIds)
.modify((builder) => { .modify((builder) => {
if (options.order) { if (options.order) {
builder.orderBy(...options.order); builder.orderBy(...options.order);
} }
}), })
.groupBy('actors.id', 'avatars.id', 'sfw_media.id', 'entities.id', 'actors_meta.stashed', 'birth_countries.alpha2', 'residence_countries.alpha2'),
knex('actors_profiles') knex('actors_profiles')
.select( .select(
'actors_profiles.*', 'actors_profiles.*',
@@ -245,10 +251,12 @@ export async function fetchActorsById(actorIds, options = {}, reqUser) {
'media.*', 'media.*',
'actors_avatars.actor_id', 'actors_avatars.actor_id',
knex.raw('json_agg(actors_avatars.profile_id) as profile_ids'), knex.raw('json_agg(actors_avatars.profile_id) as profile_ids'),
knex.raw('row_to_json(sfw_media) as sfw_media'),
) )
.whereIn('actor_id', actorIds) .whereIn('actor_id', actorIds)
.leftJoin('media', 'media.id', 'actors_avatars.media_id') .leftJoin('media', 'media.id', 'actors_avatars.media_id')
.groupBy('media.id', 'actors_avatars.actor_id') .leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
.groupBy('media.id', 'sfw_media.id', 'actors_avatars.actor_id')
.orderBy(knex.raw('max(actors_avatars.created_at)'), 'desc'), .orderBy(knex.raw('max(actors_avatars.created_at)'), 'desc'),
knex('actors_socials') knex('actors_socials')
.whereIn('actor_id', actorIds), .whereIn('actor_id', actorIds),

View File

@@ -1,11 +1,15 @@
import yargs from 'yargs'; import yargs from 'yargs';
const { argv } = yargs() const { argv } = yargs(process.argv.slice(2))
.command('npm start')
.option('debug', { .option('debug', {
describe: 'Show error stack traces', describe: 'Show error stack traces and inputs',
type: 'boolean', type: 'boolean',
default: process.env.NODE_ENV === 'development', default: process.env.NODE_ENV === 'development',
})
.option('ip', {
describe: 'Mock IP address',
type: 'string',
default: null,
}); });
export default argv; export default argv;

55
src/censor.js Normal file
View File

@@ -0,0 +1,55 @@
import config from 'config';
import {
TextCensor,
RegExpMatcher,
englishDataset,
englishRecommendedTransformers,
DataSet,
pattern,
// asteriskCensorStrategy,
} from 'obscenity';
const textCensor = new TextCensor();
// built-in asterisk strategy replaces entire word
textCensor.setStrategy(({
input,
startIndex,
endIndex,
matchLength,
}) => {
if (matchLength <= 2) {
return '*'.repeat(matchLength);
}
return `${input.at(startIndex)}${'*'.repeat(matchLength - 2)}${input.at(endIndex)}`;
});
const dataset = new DataSet().addAll(englishDataset);
config.restrictions.censors.forEach((word) => {
dataset.addPhrase((phrase) => phrase
.setMetadata({ originalWord: word })
.addPattern(pattern`${word}`));
});
const matcher = new RegExpMatcher({
...dataset.build(),
...englishRecommendedTransformers,
});
export function censor(text, restriction) {
if (!text) {
return null;
}
if (!restriction) {
return text;
}
const censorMatches = matcher.getAllMatches(text);
const censoredText = textCensor.applyTo(text, censorMatches);
return censoredText;
}

View File

@@ -3,29 +3,36 @@ import redis from './redis.js';
import initLogger from './logger.js'; import initLogger from './logger.js';
import entityPrefixes from './entities-prefixes.js'; import entityPrefixes from './entities-prefixes.js';
import { getAffiliateEntityUrl } from './affiliates.js'; import { getAffiliateEntityUrl } from './affiliates.js';
import { censor } from './censor.js';
const logger = initLogger(); const logger = initLogger();
export function curateEntity(entity, context) { export function curateEntity(entity, context = {}) {
if (!entity) { if (!entity) {
return null; return null;
} }
const curatedEntity = { const curatedEntity = {
id: entity.id, id: entity.id,
name: entity.name, name: censor(entity.name, context.restriction),
slug: entity.slug, slug: entity.slug,
type: entity.type, type: entity.type,
url: entity.url, url: entity.url,
isIndependent: entity.independent, isIndependent: entity.independent,
hasLogo: entity.has_logo, hasLogo: context.restriction ? false : entity.has_logo,
parent: curateEntity(entity.parent, context), parent: curateEntity(entity.parent, context),
tags: context?.tags?.map((tag) => ({ tags: context?.tags?.map((tag) => ({
id: tag.id, id: tag.id,
name: tag.name, name: tag.name,
slug: tag.slug, slug: tag.slug,
})), })),
children: context?.children?.filter((child) => child.parent_id === entity.id).map((child) => curateEntity({ ...child, parent: entity }, { parent: entity })) || [], children: context?.children?.filter((child) => child.parent_id === entity.id).map((child) => curateEntity({
...child,
parent: entity,
}, {
parent: entity,
restriction: context.restriction,
})) || [],
affiliate: entity.affiliate ? { affiliate: entity.affiliate ? {
id: entity.affiliate.id, id: entity.affiliate.id,
entityId: entity.affiliate.entity_id, entityId: entity.affiliate.entity_id,
@@ -44,7 +51,7 @@ export function curateEntity(entity, context) {
return curatedEntity; return curatedEntity;
} }
export async function fetchEntities(options = {}) { export async function fetchEntities(options = {}, context) {
const entities = await knex('entities') const entities = await knex('entities')
.select('entities.*', knex.raw('row_to_json(parents) as parent')) .select('entities.*', knex.raw('row_to_json(parents) as parent'))
.modify((builder) => { .modify((builder) => {
@@ -93,11 +100,12 @@ export async function fetchEntities(options = {}) {
.whereIn('entity_id', entities.map((entity) => entity.id)); .whereIn('entity_id', entities.map((entity) => entity.id));
return entities.map((entityEntry) => curateEntity(entityEntry, { return entities.map((entityEntry) => curateEntity(entityEntry, {
...context,
tags: entitiesTags.filter((tag) => tag.entity_id === entityEntry.id), tags: entitiesTags.filter((tag) => tag.entity_id === entityEntry.id),
})); }));
} }
export async function fetchEntitiesById(entityIds, options = {}, reqUser) { export async function fetchEntitiesById(entityIds, options = {}, reqUser, context) {
const [entities, children, tags, alerts] = await Promise.all([ const [entities, children, tags, alerts] = await Promise.all([
knex('entities') knex('entities')
.select( .select(
@@ -136,6 +144,7 @@ export async function fetchEntitiesById(entityIds, options = {}, reqUser) {
if (options.order) { if (options.order) {
return entities.map((entityEntry) => curateEntity(entityEntry, { return entities.map((entityEntry) => curateEntity(entityEntry, {
...context,
append: options.append, append: options.append,
children: children.filter((channel) => channel.parent_id === entityEntry.id), children: children.filter((channel) => channel.parent_id === entityEntry.id),
alerts: alerts.filter((alert) => alert.entity_id === entityEntry.id), alerts: alerts.filter((alert) => alert.entity_id === entityEntry.id),
@@ -151,6 +160,7 @@ export async function fetchEntitiesById(entityIds, options = {}, reqUser) {
} }
return curateEntity(entity, { return curateEntity(entity, {
...context,
append: options.append, append: options.append,
children: children.filter((channel) => channel.parent_id === entity.id), children: children.filter((channel) => channel.parent_id === entity.id),
tags: tags.filter((tag) => tag.entity_id === entity.id), tags: tags.filter((tag) => tag.entity_id === entity.id),

View File

@@ -1,12 +1,9 @@
// import config from 'config';
import { pageContext } from '../renderer/usePageContext.js'; import { pageContext } from '../renderer/usePageContext.js';
function getBasePath(media, type, options) { function getBasePath(media, options) {
/* if (pageContext.restriction) {
if (store.state.ui.sfw) { return pageContext.env.media.assetPath;
return config.media.assetPath;
} }
*/
if (media.isS3) { if (media.isS3) {
return options.s3Path; return options.s3Path;
@@ -20,15 +17,13 @@ function getBasePath(media, type, options) {
} }
function getFilename(media, type, options) { function getFilename(media, type, options) {
/* if (pageContext.restriction && type && !options?.original) {
if (store.state.ui.sfw && type && !options?.original) { return media.sfw?.[type];
return media.sfw[type];
} }
if (store.state.ui.sfw) { if (pageContext.restriction) {
return media.sfw.path; return media.sfw?.path;
} }
*/
if (type && !options?.original) { if (type && !options?.original) {
return media[type]; return media[type];
@@ -42,7 +37,7 @@ export default function getPath(media, type, options) {
return null; return null;
} }
const path = getBasePath(media, type, { ...pageContext.env.media, ...options }); const path = getBasePath(media, { ...pageContext.env.media, ...options });
const filename = getFilename(media, type, { ...pageContext.env.media, ...options }); const filename = getFilename(media, type, { ...pageContext.env.media, ...options });
return `${path}/${filename}`; return `${path}/${filename}`;

View File

@@ -30,6 +30,7 @@ export function curateMedia(media, context = {}) {
parent: media.entity_parent, parent: media.entity_parent,
}), }),
type: context.type || null, type: context.type || null,
sfw: curateMedia(media.sfw_media),
isRestricted: context.isRestricted, isRestricted: context.isRestricted,
}; };
} }

View File

@@ -8,29 +8,30 @@ import { curateMedia } from './media.js';
import { fetchTagsById } from './tags.js'; import { fetchTagsById } from './tags.js';
import { fetchEntitiesById } from './entities.js'; import { fetchEntitiesById } from './entities.js';
import { curateStash } from './stashes.js'; import { curateStash } from './stashes.js';
import { censor } from './censor.js';
import escape from '../utils/escape-manticore.js'; import escape from '../utils/escape-manticore.js';
import promiseProps from '../utils/promise-props.js'; import promiseProps from '../utils/promise-props.js';
function curateMovie(rawMovie, assets) { function curateMovie(rawMovie, assets, context = {}) {
if (!rawMovie) { if (!rawMovie) {
return null; return null;
} }
return { return {
id: rawMovie.id, id: rawMovie.id,
title: rawMovie.title, title: censor(rawMovie.title, context.restriction),
slug: rawMovie.slug, slug: rawMovie.slug,
url: rawMovie.url, url: rawMovie.url,
date: rawMovie.date, date: rawMovie.date,
datePrecision: rawMovie.date_precision, datePrecision: rawMovie.date_precision,
createdAt: rawMovie.created_at, createdAt: rawMovie.created_at,
effectiveDate: rawMovie.effective_date, effectiveDate: rawMovie.effective_date,
description: rawMovie.description, description: censor(rawMovie.description, context.restriction),
duration: rawMovie.duration, duration: rawMovie.duration,
channel: { channel: {
id: assets.channel.id, id: assets.channel.id,
slug: assets.channel.slug, slug: assets.channel.slug,
name: assets.channel.name, name: censor(assets.channel.name, context.restriction),
type: assets.channel.type, type: assets.channel.type,
isIndependent: assets.channel.independent, isIndependent: assets.channel.independent,
hasLogo: assets.channel.has_logo, hasLogo: assets.channel.has_logo,
@@ -38,7 +39,7 @@ function curateMovie(rawMovie, assets) {
network: assets.channel.network_id ? { network: assets.channel.network_id ? {
id: assets.channel.network_id, id: assets.channel.network_id,
slug: assets.channel.network_slug, slug: assets.channel.network_slug,
name: assets.channel.network_name, name: censor(assets.channel.network_name, context.restriction),
type: assets.channel.network_type, type: assets.channel.network_type,
hasLogo: assets.channel.has_logo, hasLogo: assets.channel.has_logo,
} : null, } : null,
@@ -51,7 +52,7 @@ function curateMovie(rawMovie, assets) {
tags: assets.tags.map((tag) => ({ tags: assets.tags.map((tag) => ({
id: tag.id, id: tag.id,
slug: tag.slug, slug: tag.slug,
name: tag.name, name: censor(tag.name, context.restriction),
})), })),
// poster: curateMedia(assets.poster), // poster: curateMedia(assets.poster),
covers: assets.covers.map((cover) => curateMedia(cover, { type: 'cover' })), covers: assets.covers.map((cover) => curateMedia(cover, { type: 'cover' })),
@@ -64,7 +65,7 @@ function curateMovie(rawMovie, assets) {
}; };
} }
export async function fetchMoviesById(movieIds, reqUser) { export async function fetchMoviesById(movieIds, reqUser, context) {
const { const {
movies, movies,
channels, channels,
@@ -123,20 +124,25 @@ export async function fetchMoviesById(movieIds, reqUser) {
.leftJoin('tags', 'tags.id', 'releases_tags.tag_id') .leftJoin('tags', 'tags.id', 'releases_tags.tag_id')
.orderBy('priority', 'desc'), .orderBy('priority', 'desc'),
covers: knex('movies_covers') covers: knex('movies_covers')
.select('media.*', 'movies_covers.movie_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
.whereIn('movie_id', movieIds) .whereIn('movie_id', movieIds)
.leftJoin('media', 'media.id', 'movies_covers.media_id') .leftJoin('media', 'media.id', 'movies_covers.media_id')
.orderBy('media.index'), .leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
photos: knex.transaction(async (trx) => { .orderBy('media.index')
.groupBy('media.id', 'movies_covers.movie_id', 'sfw_media.id'),
photos: context.restriction ? [] : knex.transaction(async (trx) => {
if (reqUser) { if (reqUser) {
await trx.select(knex.raw('set_config(\'user.id\', :userId, true)', { userId: reqUser.id })); await trx.select(knex.raw('set_config(\'user.id\', :userId, true)', { userId: reqUser.id }));
} }
return trx('movies_scenes') return trx('movies_scenes')
.select('media.*', 'movies_scenes.movie_id') .select('media.*', 'movies_scenes.movie_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
.whereIn('movies_scenes.movie_id', movieIds) .whereIn('movies_scenes.movie_id', movieIds)
.whereNotNull('media.id') .whereNotNull('media.id')
.leftJoin('releases_photos', 'releases_photos.release_id', 'movies_scenes.scene_id') .leftJoin('releases_photos', 'releases_photos.release_id', 'movies_scenes.scene_id')
.leftJoin('media', 'media.id', 'releases_photos.media_id'); .leftJoin('media', 'media.id', 'releases_photos.media_id')
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
.groupBy('media.id', 'movies_scenes.movie_id', 'sfw_media.id');
}), }),
caps: knex.transaction(async (trx) => { caps: knex.transaction(async (trx) => {
if (reqUser) { if (reqUser) {
@@ -144,11 +150,13 @@ export async function fetchMoviesById(movieIds, reqUser) {
} }
return trx('movies_scenes') return trx('movies_scenes')
.select('media.*', 'movies_scenes.movie_id') .select('media.*', 'movies_scenes.movie_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
.whereIn('movies_scenes.movie_id', movieIds) .whereIn('movies_scenes.movie_id', movieIds)
.whereNotNull('media.id') .whereNotNull('media.id')
.leftJoin('releases_caps', 'releases_caps.release_id', 'movies_scenes.scene_id') .leftJoin('releases_caps', 'releases_caps.release_id', 'movies_scenes.scene_id')
.leftJoin('media', 'media.id', 'releases_caps.media_id'); .leftJoin('media', 'media.id', 'releases_caps.media_id')
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
.groupBy('media.id', 'movies_scenes.movie_id', 'sfw_media.id');
}), }),
trailers: knex('movies_trailers') trailers: knex('movies_trailers')
.whereIn('movie_id', movieIds) .whereIn('movie_id', movieIds)
@@ -192,7 +200,7 @@ export async function fetchMoviesById(movieIds, reqUser) {
caps: movieCaps, caps: movieCaps,
trailer: movieTrailer, trailer: movieTrailer,
stashes: movieStashes, stashes: movieStashes,
}); }, context);
}).filter(Boolean); }).filter(Boolean);
} }
@@ -398,7 +406,7 @@ function countAggregations(buckets) {
return Object.fromEntries(buckets.map((bucket) => [bucket.key, { count: bucket.doc_count }])); return Object.fromEntries(buckets.map((bucket) => [bucket.key, { count: bucket.doc_count }]));
} }
export async function fetchMovies(filters, rawOptions, reqUser) { export async function fetchMovies(filters, rawOptions, reqUser, context) {
const options = curateOptions(rawOptions); const options = curateOptions(rawOptions);
console.log(options); console.log(options);
@@ -413,13 +421,13 @@ export async function fetchMovies(filters, rawOptions, reqUser) {
const channelCounts = options.aggregateChannels && countAggregations(result.aggregations?.channelIds); const channelCounts = options.aggregateChannels && countAggregations(result.aggregations?.channelIds);
const [aggActors, aggTags, aggChannels] = await Promise.all([ const [aggActors, aggTags, aggChannels] = await Promise.all([
options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: actorCounts }) : [], options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: actorCounts }, reqUser) : [],
options.aggregateTags ? fetchTagsById(result.aggregations.tagIds.map((bucket) => bucket.key), { order: [knex.raw('lower(name)'), 'asc'], append: tagCounts }) : [], options.aggregateTags ? fetchTagsById(result.aggregations.tagIds.map((bucket) => bucket.key), { order: [knex.raw('lower(name)'), 'asc'], append: tagCounts }, reqUser, context) : [],
options.aggregateChannels ? fetchEntitiesById(result.aggregations.channelIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: channelCounts }) : [], options.aggregateChannels ? fetchEntitiesById(result.aggregations.channelIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: channelCounts }, reqUser, context) : [],
]); ]);
const movieIds = result.movies.map((movie) => Number(movie.id)); const movieIds = result.movies.map((movie) => Number(movie.id));
const movies = await fetchMoviesById(movieIds, reqUser); const movies = await fetchMoviesById(movieIds, reqUser, context);
return { return {
movies, movies,

View File

@@ -1,6 +1,7 @@
import config from 'config'; import config from 'config';
import { MerkleJson } from 'merkle-json'; import { MerkleJson } from 'merkle-json';
import argv from './argv.js';
import { knexQuery as knex, knexOwner, knexManticore } from './knex.js'; import { knexQuery as knex, knexOwner, knexManticore } from './knex.js';
import { utilsApi } from './manticore.js'; import { utilsApi } from './manticore.js';
import { HttpError } from './errors.js'; import { HttpError } from './errors.js';
@@ -15,18 +16,19 @@ import promiseProps from '../utils/promise-props.js';
import initLogger from './logger.js'; import initLogger from './logger.js';
import { curateRevision } from './revisions.js'; import { curateRevision } from './revisions.js';
import { getAffiliateSceneUrl } from './affiliates.js'; import { getAffiliateSceneUrl } from './affiliates.js';
import { censor } from './censor.js';
const logger = initLogger(); const logger = initLogger();
const mj = new MerkleJson(); const mj = new MerkleJson();
function curateScene(rawScene, assets, reqUser) { function curateScene(rawScene, assets, reqUser, context) {
if (!rawScene) { if (!rawScene) {
return null; return null;
} }
const curatedScene = { const curatedScene = {
id: rawScene.id, id: rawScene.id,
title: rawScene.title, title: censor(rawScene.title, context.restriction),
slug: rawScene.slug, slug: rawScene.slug,
url: rawScene.url, url: rawScene.url,
entryId: rawScene.entry_id, entryId: rawScene.entry_id,
@@ -34,14 +36,14 @@ function curateScene(rawScene, assets, reqUser) {
datePrecision: rawScene.date_precision, datePrecision: rawScene.date_precision,
createdAt: rawScene.created_at, createdAt: rawScene.created_at,
effectiveDate: rawScene.effective_date, effectiveDate: rawScene.effective_date,
description: rawScene.description, description: censor(rawScene.description, context.restriction),
duration: rawScene.duration, duration: rawScene.duration,
shootId: rawScene.shoot_id, shootId: rawScene.shoot_id,
productionDate: rawScene.production_date, productionDate: rawScene.production_date,
channel: { channel: {
id: assets.channel.id, id: assets.channel.id,
slug: assets.channel.slug, slug: assets.channel.slug,
name: assets.channel.name, name: censor(assets.channel.name, context.restriction),
type: assets.channel.type, type: assets.channel.type,
isIndependent: assets.channel.independent, isIndependent: assets.channel.independent,
hasLogo: assets.channel.has_logo, hasLogo: assets.channel.has_logo,
@@ -49,7 +51,7 @@ function curateScene(rawScene, assets, reqUser) {
network: assets.channel.network_id ? { network: assets.channel.network_id ? {
id: assets.channel.network_id, id: assets.channel.network_id,
slug: assets.channel.network_slug, slug: assets.channel.network_slug,
name: assets.channel.network_name, name: censor(assets.channel.network_name, context.restriction),
type: assets.channel.network_type, type: assets.channel.network_type,
hasLogo: assets.channel.network_has_logo, hasLogo: assets.channel.network_has_logo,
} : null, } : null,
@@ -78,7 +80,7 @@ function curateScene(rawScene, assets, reqUser) {
tags: assets.tags.map((tag) => ({ tags: assets.tags.map((tag) => ({
id: tag.id, id: tag.id,
slug: tag.slug, slug: tag.slug,
name: tag.name, name: censor(tag.name, context.restriction),
priority: tag.priority, priority: tag.priority,
})), })),
chapters: assets.chapters.map((chapter) => ({ chapters: assets.chapters.map((chapter) => ({
@@ -86,7 +88,9 @@ function curateScene(rawScene, assets, reqUser) {
title: chapter.title, title: chapter.title,
time: chapter.time, time: chapter.time,
duration: chapter.duration, duration: chapter.duration,
poster: curateMedia(chapter.chapter_poster), poster: context.restriction
? null
: (chapter.chapter_poster),
tags: chapter.chapter_tags.map((tag) => ({ tags: chapter.chapter_tags.map((tag) => ({
id: tag.id, id: tag.id,
name: tag.name, name: tag.name,
@@ -98,14 +102,18 @@ function curateScene(rawScene, assets, reqUser) {
movies: assets.movies.map((movie) => ({ movies: assets.movies.map((movie) => ({
id: movie.id, id: movie.id,
slug: movie.slug, slug: movie.slug,
title: movie.title, title: censor(movie.title, context.restriction),
covers: movie.movie_covers?.map((cover) => curateMedia(cover, { type: 'cover' })).toSorted((coverA, coverB) => coverA.index - coverB.index) || [], covers: movie.movie_covers && !context.restriction
? movie.movie_covers.map((cover) => curateMedia(cover, { type: 'cover' })).toSorted((coverA, coverB) => coverA.index - coverB.index)
: [],
})), })),
series: assets.series.map((serie) => ({ series: assets.series.map((serie) => ({
id: serie.id, id: serie.id,
slug: serie.slug, slug: serie.slug,
title: serie.title, title: serie.title,
poster: curateMedia(serie.serie_poster, { type: 'poster' }), poster: context.restriction
? null
: (serie.serie_poster, { type: 'poster' }),
})), })),
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' })) || [],
@@ -189,14 +197,17 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
.select( .select(
'actors.*', 'actors.*',
knex.raw('row_to_json(avatars) as avatar'), knex.raw('row_to_json(avatars) as avatar'),
knex.raw('row_to_json(sfw_media) as sfw_avatar'),
'countries.name as birth_country_name', 'countries.name as birth_country_name',
'countries.alias as birth_country_alias', 'countries.alias as birth_country_alias',
'releases_actors.release_id', 'releases_actors.release_id',
) )
.leftJoin('actors', 'actors.id', 'releases_actors.actor_id') .leftJoin('actors', 'actors.id', 'releases_actors.actor_id')
.leftJoin('media as avatars', 'avatars.id', 'actors.avatar_media_id') .leftJoin('media as avatars', 'avatars.id', 'actors.avatar_media_id')
.leftJoin('media as sfw_media', 'sfw_media.id', 'avatars.sfw_media_id')
.leftJoin('countries', 'countries.alpha2', 'actors.birth_country_alpha2') .leftJoin('countries', 'countries.alpha2', 'actors.birth_country_alpha2')
.whereIn('release_id', sceneIds), .whereIn('release_id', sceneIds)
.groupBy('actors.id', 'releases_actors.release_id', 'avatars.id', 'countries.name', 'countries.alias', 'sfw_media.id'),
directors: knex('releases_directors') directors: knex('releases_directors')
.whereIn('release_id', sceneIds) .whereIn('release_id', sceneIds)
.leftJoin('actors as directors', 'directors.id', 'releases_directors.director_id'), .leftJoin('actors as directors', 'directors.id', 'releases_directors.director_id'),
@@ -234,17 +245,23 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
.whereIn('scene_id', sceneIds) .whereIn('scene_id', sceneIds)
.groupBy('series.id', 'series_scenes.scene_id', 'media.*') : [], .groupBy('series.id', 'series_scenes.scene_id', 'media.*') : [],
posters: knex('releases_posters') posters: knex('releases_posters')
.select('media.*', 'releases_posters.release_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
.whereIn('release_id', sceneIds) .whereIn('release_id', sceneIds)
.leftJoin('media', 'media.id', 'releases_posters.media_id'), .leftJoin('media', 'media.id', 'releases_posters.media_id')
photos: context.includeAssets ? knex.transaction(async (trx) => { .leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
.groupBy('media.id', 'releases_posters.release_id', 'sfw_media.id'),
photos: context.includeAssets && !context.restriction ? knex.transaction(async (trx) => {
if (reqUser) { if (reqUser) {
await trx.select(knex.raw('set_config(\'user.id\', :userId, true)', { userId: reqUser.id })); await trx.select(knex.raw('set_config(\'user.id\', :userId, true)', { userId: reqUser.id }));
} }
return trx('releases_photos') return trx('releases_photos')
.select('media.*', 'releases_photos.release_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
.leftJoin('media', 'media.id', 'releases_photos.media_id') .leftJoin('media', 'media.id', 'releases_photos.media_id')
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
.whereIn('release_id', sceneIds) .whereIn('release_id', sceneIds)
.orderBy('index'); .orderBy('index')
.groupBy('media.id', 'releases_photos.release_id', 'sfw_media.id');
}) : [], }) : [],
caps: context.includeAssets ? knex.transaction(async (trx) => { caps: context.includeAssets ? knex.transaction(async (trx) => {
if (reqUser) { if (reqUser) {
@@ -252,9 +269,12 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
} }
return trx('releases_caps') return trx('releases_caps')
.select('media.*', 'releases_caps.release_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
.leftJoin('media', 'media.id', 'releases_caps.media_id') .leftJoin('media', 'media.id', 'releases_caps.media_id')
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
.whereIn('release_id', sceneIds) .whereIn('release_id', sceneIds)
.orderBy('index'); .orderBy('index')
.groupBy('media.id', 'releases_caps.release_id', 'sfw_media.id');
}) : [], }) : [],
trailers: context.includeAssets ? knex.transaction(async (trx) => { trailers: context.includeAssets ? knex.transaction(async (trx) => {
if (reqUser) { if (reqUser) {
@@ -350,7 +370,7 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
stashes: sceneStashes, stashes: sceneStashes,
actorStashes: sceneActorStashes, actorStashes: sceneActorStashes,
lastBatchId: lastBatch?.id, lastBatchId: lastBatch?.id,
}, reqUser); }, reqUser, context);
}).filter(Boolean); }).filter(Boolean);
} }
@@ -545,7 +565,7 @@ async function queryManticoreSql(filters, options, _reqUser) {
? sqlQuery ? sqlQuery
: sqlQuery.replace(/scenes\./g, ''); : sqlQuery.replace(/scenes\./g, '');
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development' && argv.debug) {
console.log(curatedSqlQuery); console.log(curatedSqlQuery);
} }
@@ -603,14 +623,18 @@ function countAggregations(buckets) {
return Object.fromEntries(buckets.map((bucket) => [bucket.key, { count: bucket.doc_count }])); return Object.fromEntries(buckets.map((bucket) => [bucket.key, { count: bucket.doc_count }]));
} }
export async function fetchScenes(filters, rawOptions, reqUser) { export async function fetchScenes(filters, rawOptions, reqUser, context) {
const options = curateOptions(rawOptions); const options = curateOptions(rawOptions);
console.log('filters', filters); if (argv.debug) {
console.log('options', options); console.log('filters', filters);
console.log('options', options);
}
console.time('manticore sql'); console.time('manticore sql');
const result = await queryManticoreSql(filters, options, reqUser); const result = await queryManticoreSql(filters, options, reqUser);
console.timeEnd('manticore sql'); console.timeEnd('manticore sql');
const aggYears = options.aggregateYears && result.aggregations.years.map((bucket) => ({ year: bucket.key, count: bucket.doc_count })); const aggYears = options.aggregateYears && result.aggregations.years.map((bucket) => ({ year: bucket.key, count: bucket.doc_count }));
@@ -623,16 +647,16 @@ export async function fetchScenes(filters, rawOptions, reqUser) {
console.time('fetch aggregations'); console.time('fetch aggregations');
const [aggActors, aggTags, aggChannels] = await Promise.all([ const [aggActors, aggTags, aggChannels] = await Promise.all([
options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: actorCounts }) : [], options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: actorCounts }, reqUser) : [],
options.aggregateTags ? fetchTagsById(result.aggregations.tagIds.map((bucket) => bucket.key), { order: [knex.raw('lower(name)'), 'asc'], append: tagCounts }) : [], options.aggregateTags ? fetchTagsById(result.aggregations.tagIds.map((bucket) => bucket.key), { order: [knex.raw('lower(name)'), 'asc'], append: tagCounts }, reqUser, context) : [],
options.aggregateChannels ? fetchEntitiesById(entityIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: channelCounts }) : [], options.aggregateChannels ? fetchEntitiesById(entityIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: channelCounts }, reqUser, context) : [],
]); ]);
console.timeEnd('fetch aggregations'); console.timeEnd('fetch aggregations');
console.time('fetch full'); console.time('fetch full');
const sceneIds = result.scenes.map((scene) => Number(scene.id)); const sceneIds = result.scenes.map((scene) => Number(scene.id));
const scenes = await fetchScenesById(sceneIds, { reqUser }); const scenes = await fetchScenesById(sceneIds, { reqUser, ...context });
console.timeEnd('fetch full'); console.timeEnd('fetch full');
return { return {

View File

@@ -1,6 +1,7 @@
import knex from './knex.js'; import knex from './knex.js';
import redis from './redis.js'; import redis from './redis.js';
import initLogger from './logger.js'; import initLogger from './logger.js';
import { censor } from './censor.js';
import { curateMedia } from './media.js'; import { curateMedia } from './media.js';
@@ -9,9 +10,9 @@ const logger = initLogger();
function curateTag(tag, context) { function curateTag(tag, context) {
return { return {
id: tag.id, id: tag.id,
name: tag.name, name: censor(tag.name, context.restriction),
slug: tag.slug, slug: tag.slug,
description: tag.description, description: context.restriction ? null : tag.description, // censor interferes with markdown
priority: tag.priority, priority: tag.priority,
poster: tag.poster && curateMedia(tag.poster), poster: tag.poster && curateMedia(tag.poster),
photos: tag.photos?.map((photo) => curateMedia(photo)) || [], photos: tag.photos?.map((photo) => curateMedia(photo)) || [],
@@ -55,10 +56,13 @@ export async function fetchTags(options = {}) {
} }
}), }),
knex('tags_posters') knex('tags_posters')
.select('media.*', 'tags_posters.tag_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
.select('tags_posters.tag_id', 'media.*', knex.raw('row_to_json(entities) as entity'), knex.raw('row_to_json(parents) as entity_parent')) .select('tags_posters.tag_id', 'media.*', knex.raw('row_to_json(entities) as entity'), knex.raw('row_to_json(parents) as entity_parent'))
.leftJoin('media', 'media.id', 'tags_posters.media_id') .leftJoin('media', 'media.id', 'tags_posters.media_id')
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
.leftJoin('entities', 'entities.id', 'media.entity_id') .leftJoin('entities', 'entities.id', 'media.entity_id')
.leftJoin('entities as parents', 'parents.id', 'entities.parent_id'), .leftJoin('entities as parents', 'parents.id', 'entities.parent_id')
.groupBy('media.id', 'tags_posters.tag_id', 'sfw_media.id', 'entities.id', 'parents.id'),
]); ]);
const postersByTagId = Object.fromEntries(posters.map((poster) => [poster.tag_id, poster])); const postersByTagId = Object.fromEntries(posters.map((poster) => [poster.tag_id, poster]));
@@ -69,7 +73,7 @@ export async function fetchTags(options = {}) {
})); }));
} }
export async function fetchTagsById(tagIds, options = {}, reqUser) { export async function fetchTagsById(tagIds, options = {}, reqUser, context = {}) {
const [tags, posters, photos, alerts] = await Promise.all([ const [tags, posters, photos, alerts] = await Promise.all([
knex('tags') knex('tags')
.whereIn('tags.id', tagIds.filter((tagId) => typeof tagId === 'number')) .whereIn('tags.id', tagIds.filter((tagId) => typeof tagId === 'number'))
@@ -80,14 +84,17 @@ export async function fetchTagsById(tagIds, options = {}, reqUser) {
} }
}), }),
knex('tags_posters') knex('tags_posters')
.select('media.*', 'tags_posters.tag_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
.select('tags_posters.tag_id', 'media.*', knex.raw('row_to_json(entities) as entity'), knex.raw('row_to_json(parents) as entity_parent')) .select('tags_posters.tag_id', 'media.*', knex.raw('row_to_json(entities) as entity'), knex.raw('row_to_json(parents) as entity_parent'))
.leftJoin('tags', 'tags.id', 'tags_posters.tag_id') .leftJoin('tags', 'tags.id', 'tags_posters.tag_id')
.leftJoin('media', 'media.id', 'tags_posters.media_id') .leftJoin('media', 'media.id', 'tags_posters.media_id')
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
.leftJoin('entities', 'entities.id', 'media.entity_id') .leftJoin('entities', 'entities.id', 'media.entity_id')
.leftJoin('entities as parents', 'parents.id', 'entities.parent_id') .leftJoin('entities as parents', 'parents.id', 'entities.parent_id')
.whereIn('tags.id', tagIds.filter((tagId) => typeof tagId === 'number')) .whereIn('tags.id', tagIds.filter((tagId) => typeof tagId === 'number'))
.orWhereIn('tags.slug', tagIds.filter((tagId) => typeof tagId === 'string')), .orWhereIn('tags.slug', tagIds.filter((tagId) => typeof tagId === 'string'))
knex('tags_photos') .groupBy('media.id', 'tags_posters.tag_id', 'sfw_media.id', 'entities.id', 'parents.id'),
context.restriction ? [] : knex('tags_photos')
.select('tags_photos.tag_id', 'media.*', knex.raw('row_to_json(entities) as entity'), knex.raw('row_to_json(parents) as entity_parent')) .select('tags_photos.tag_id', 'media.*', knex.raw('row_to_json(entities) as entity'), knex.raw('row_to_json(parents) as entity_parent'))
.leftJoin('tags', 'tags.id', 'tags_photos.tag_id') .leftJoin('tags', 'tags.id', 'tags_photos.tag_id')
.leftJoin('media', 'media.id', 'tags_photos.media_id') .leftJoin('media', 'media.id', 'tags_photos.media_id')
@@ -118,6 +125,7 @@ export async function fetchTagsById(tagIds, options = {}, reqUser) {
}, { }, {
alerts: alerts.filter((alert) => alert.tag_id === tagEntry.id), alerts: alerts.filter((alert) => alert.tag_id === tagEntry.id),
append: options.append, append: options.append,
...context,
})); }));
} }
@@ -136,6 +144,7 @@ export async function fetchTagsById(tagIds, options = {}, reqUser) {
}, { }, {
alerts: alerts.filter((alert) => alert.tag_id === tag.id), alerts: alerts.filter((alert) => alert.tag_id === tag.id),
append: options.append, append: options.append,
...context,
}); });
}).filter(Boolean); }).filter(Boolean);

View File

@@ -1,6 +1,7 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import { stringify } from '@brillout/json-serializer/stringify'; /* eslint-disable-line import/extensions */ import { stringify } from '@brillout/json-serializer/stringify'; /* eslint-disable-line import/extensions */
import IPCIDR from 'ip-cidr'; import IPCIDR from 'ip-cidr';
import argv from '../argv.js';
import { import {
login, login,
@@ -14,6 +15,10 @@ import {
import { fetchUser } from '../users.js'; import { fetchUser } from '../users.js';
function getIp(req) { function getIp(req) {
if (argv.ip) {
return argv.ip;
}
const ip = req.headers['x-forwarded-for']?.split(',')[0] || req.connection.remoteAddress; const ip = req.headers['x-forwarded-for']?.split(',')[0] || req.connection.remoteAddress;
const unmappedIp = ip?.includes('.') const unmappedIp = ip?.includes('.')

View File

@@ -51,6 +51,7 @@ export default async function mainHandler(req, res, next) {
siteKey: config.auth.captcha.siteKey, siteKey: config.auth.captcha.siteKey,
}, },
}, },
restriction: req.restriction,
meta: { meta: {
now: new Date().toISOString(), now: new Date().toISOString(),
}, },

80
src/web/restrictions.js Normal file
View File

@@ -0,0 +1,80 @@
import config from 'config';
import path from 'path';
import { Reader } from '@maxmind/geoip2-node';
import initLogger from '../logger.js';
const logger = initLogger();
const regions = config.restrictions.regions;
export default async function initRestrictionHandler() {
const reader = await Reader.open('assets/GeoLite2-City.mmdb');
function getRestriction(req) {
if (req.session.restriction && req.session.country && req.session.restrictionIp === req.userIp) {
return {
restriction: req.session.restriction,
country: req.session.country,
};
}
const location = reader.city(req.userIp);
const country = location.country.isoCode;
const subdivision = location.subdivisions?.[0]?.isoCode;
if (regions[country]?.[subdivision]) {
// state or province restriction
return {
restriction: config.restrictions.modes[regions[country][subdivision]],
country,
};
}
if (regions[country]) {
// country restriction
return {
restriction: config.restrictions.modes[regions[country]],
country,
};
}
return {
restriction: null,
country,
};
}
function restrictionHandler(req, res, next) {
if (!config.restrictions.enabled) {
next();
return;
}
try {
const { restriction, country } = getRestriction(req);
if (restriction === 'block' || req.path === '/sfw/') {
res.render(path.join(import.meta.dirname, '../../assets/sfw.ejs'), {
noVpn: config.restrictions.noVpn.includes(country),
});
return;
}
if (req.session.restriction !== restriction) {
req.session.restrictionIp = req.userIp;
req.session.restriction = restriction;
req.session.country = country;
}
req.restriction = restriction;
req.country = country;
} catch (error) {
logger.error(`Failed Maxmind IP lookup for ${req.ip}: ${error.message}`);
}
next();
}
return restrictionHandler;
}

View File

@@ -12,6 +12,7 @@ import redis from '../redis.js';
import errorHandler from './error.js'; import errorHandler from './error.js';
import consentHandler from './consent.js'; import consentHandler from './consent.js';
import initRestrictionHandler from './restrictions.js';
import { scenesRouter } from './scenes.js'; import { scenesRouter } from './scenes.js';
import { actorsRouter } from './actors.js'; import { actorsRouter } from './actors.js';
@@ -48,9 +49,11 @@ const isProduction = process.env.NODE_ENV === 'production';
export default async function initServer() { export default async function initServer() {
const app = express(); const app = express();
const router = Router(); const router = Router();
const restrictionHandler = await initRestrictionHandler();
app.use(compression()); app.use(compression());
app.disable('x-powered-by'); app.disable('x-powered-by');
app.set('view engine', 'ejs');
router.use(boolParser()); router.use(boolParser());
@@ -58,7 +61,7 @@ export default async function initServer() {
router.use('/', express.static('static')); router.use('/', express.static('static'));
router.use('/media', express.static(config.media.path)); router.use('/media', express.static(config.media.path));
router.use((req, res, next) => { router.use((req, _res, next) => {
if (req.headers.cookie) { if (req.headers.cookie) {
const cookies = cookie.parse(req.headers.cookie); const cookies = cookie.parse(req.headers.cookie);
@@ -109,11 +112,13 @@ export default async function initServer() {
router.use(viteDevMiddleware); router.use(viteDevMiddleware);
} }
router.get('/consent', (req, res) => { router.use(restrictionHandler);
router.get('/consent', (_req, res) => {
res.sendFile(path.join(import.meta.dirname, '../../assets/consent.html')); res.sendFile(path.join(import.meta.dirname, '../../assets/consent.html'));
}); });
router.use('/api/*', async (req, res, next) => { router.use('/api/*', async (req, _res, next) => {
if (req.headers['api-user']) { if (req.headers['api-user']) {
await verifyKey(req.headers['api-user'], req.headers['api-key'], req); await verifyKey(req.headers['api-user'], req.headers['api-key'], req);
@@ -159,7 +164,7 @@ export default async function initServer() {
router.use(consentHandler); router.use(consentHandler);
router.use((req, res, next) => { router.use((_req, res, next) => {
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
res.set('Accept-CH', 'Sec-CH-Prefers-Color-Scheme'); res.set('Accept-CH', 'Sec-CH-Prefers-Color-Scheme');
res.set('Vary', 'Sec-CH-Prefers-Color-Scheme'); res.set('Vary', 'Sec-CH-Prefers-Color-Scheme');

2
static

Submodule static updated: 4b6b7e5e73...389d659cb2