Compare commits
95 Commits
5ac7cfbc9a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a45fd152df | |||
| 60f01d859e | |||
| 6d4e033fb7 | |||
| bf635df863 | |||
| 1732b4cf4d | |||
| a2e268913a | |||
| dfb04e5e01 | |||
| c0ce844169 | |||
| 56acc42f17 | |||
| 7cee6639e7 | |||
| 62753a4af0 | |||
| 2d4d2b1105 | |||
| 09ed130327 | |||
| 1414a846ec | |||
| 19dc029e28 | |||
| 67176db933 | |||
| 95e8982696 | |||
| b8a03cd6fb | |||
| aad4ff8079 | |||
| 8821b3a00d | |||
| db43102487 | |||
| a3072a4967 | |||
| 983e24835f | |||
| 1c982124b0 | |||
| 6aaa3ad30c | |||
| 57dfa621df | |||
| e98254d444 | |||
| 56defe2c6f | |||
| a2f08c540c | |||
| 217decee06 | |||
| d93baf80ab | |||
| e409f3c214 | |||
| a1e080c20d | |||
| 6c8fce49d6 | |||
| 1a84f899e7 | |||
| ce107e6b65 | |||
| 515d3885c7 | |||
| 5194c5e156 | |||
| 5b53f53fd3 | |||
| 750b30d896 | |||
| b9afa61e01 | |||
| 490be8800a | |||
| 49ee6b4eee | |||
| ada81340ef | |||
| 5ae3b5d91c | |||
| bff3bc6a0b | |||
| 5496bced59 | |||
| 030d6dc835 | |||
| fc46ae00f8 | |||
| e22978cbe4 | |||
| 70049ed495 | |||
| 9e20af925f | |||
| 457afa5043 | |||
| c536a75f3d | |||
| a2d5828fda | |||
| 52d041c591 | |||
| 3bee1ac97d | |||
| 428d86b1ee | |||
| 5facacd066 | |||
| 0bf0b716b2 | |||
| 31c62e01f9 | |||
| a57b66cd95 | |||
| e4675e6e97 | |||
| bac0b768e2 | |||
| 74c69c698e | |||
| 87604ed848 | |||
| b6ca08727f | |||
| af99491533 | |||
| 461f6cf8fd | |||
| 2ad17c2279 | |||
| 9f3bc6e8de | |||
| 6dedf10846 | |||
| 6b6e31a1bb | |||
| f7016609a0 | |||
| fde2d607b8 | |||
| 54e9fd9f6a | |||
| 886f02c5fc | |||
| 8d57dfd2d2 | |||
| f9ba519dea | |||
| 2eb4678afc | |||
| 9558ce80b4 | |||
| 4569930a81 | |||
| 52b012402e | |||
| 82ff813225 | |||
| b7bd0fac03 | |||
| 9933b4fbf0 | |||
| c272e6c8b3 | |||
| 302f6a0621 | |||
| 23155520d2 | |||
| b788d78aab | |||
| cd9e4a5e8d | |||
| 8ec48ec43e | |||
| a27bc2c815 | |||
| 16f43066a4 | |||
| 6191e17c4e |
@@ -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
2
.gitignore
vendored
@@ -5,3 +5,5 @@ config/*
|
|||||||
log/
|
log/
|
||||||
/media
|
/media
|
||||||
data/
|
data/
|
||||||
|
assets/*.mmdb
|
||||||
|
assets/.geoipupdate.lock
|
||||||
|
|||||||
52
assets/img/icons/matrix-full.svg
Normal file
52
assets/img/icons/matrix-full.svg
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 75 32"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="matrix-full.svg"
|
||||||
|
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="12.000629"
|
||||||
|
inkscape:cx="27.79021"
|
||||||
|
inkscape:cy="22.165505"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1020"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="32"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg1" />
|
||||||
|
<title
|
||||||
|
id="title1">Matrix (protocol) logo</title>
|
||||||
|
<g
|
||||||
|
id="g1">
|
||||||
|
<path
|
||||||
|
d="m0.936 0.732v30.52h2.194v0.732h-3.035v-31.98h3.034v0.732zm8.45 9.675v1.544h0.044a4.461 4.461 0 0 1 1.487-1.368c0.58-0.323 1.245-0.485 1.993-0.485 0.72 0 1.377 0.14 1.972 0.42 0.595 0.279 1.047 0.771 1.355 1.477 0.338-0.5 0.796-0.941 1.377-1.323 0.58-0.383 1.266-0.574 2.06-0.574 0.602 0 1.16 0.074 1.674 0.22 0.514 0.148 0.954 0.383 1.322 0.707 0.366 0.323 0.653 0.746 0.859 1.268 0.205 0.522 0.308 1.15 0.308 1.887v7.633h-3.127v-6.464c0-0.383-0.015-0.743-0.044-1.082a2.305 2.305 0 0 0-0.242-0.882 1.473 1.473 0 0 0-0.584-0.596c-0.257-0.146-0.606-0.22-1.047-0.22-0.44 0-0.796 0.085-1.068 0.253-0.272 0.17-0.485 0.39-0.639 0.662a2.654 2.654 0 0 0-0.308 0.927 7.074 7.074 0 0 0-0.078 1.048v6.354h-3.128v-6.398c0-0.338-7e-3 -0.673-0.021-1.004a2.825 2.825 0 0 0-0.188-0.916 1.411 1.411 0 0 0-0.55-0.673c-0.258-0.168-0.636-0.253-1.135-0.253a2.33 2.33 0 0 0-0.584 0.1 1.94 1.94 0 0 0-0.705 0.374c-0.228 0.184-0.422 0.449-0.584 0.794-0.161 0.346-0.242 0.798-0.242 1.357v6.619h-3.129v-11.41zm16.46 1.677a3.751 3.751 0 0 1 1.233-1.17 5.37 5.37 0 0 1 1.685-0.629 9.579 9.579 0 0 1 1.884-0.187c0.573 0 1.153 0.04 1.74 0.121 0.588 0.081 1.124 0.24 1.609 0.475 0.484 0.235 0.88 0.562 1.19 0.981 0.308 0.42 0.462 0.975 0.462 1.666v5.934c0 0.516 0.03 1.008 0.088 1.478 0.058 0.471 0.161 0.824 0.308 1.06h-3.171a4.435 4.435 0 0 1-0.22-1.104c-0.5 0.515-1.087 0.876-1.762 1.081a7.084 7.084 0 0 1-2.071 0.31c-0.544 0-1.05-0.067-1.52-0.2a3.472 3.472 0 0 1-1.234-0.617 2.87 2.87 0 0 1-0.826-1.059c-0.199-0.426-0.298-0.934-0.298-1.522 0-0.647 0.114-1.18 0.342-1.6 0.227-0.419 0.52-0.753 0.881-1.004 0.36-0.25 0.771-0.437 1.234-0.562 0.462-0.125 0.929-0.224 1.399-0.298 0.47-0.073 0.932-0.132 1.387-0.176 0.456-0.044 0.86-0.11 1.212-0.199 0.353-0.088 0.631-0.217 0.837-0.386s0.301-0.415 0.287-0.74c0-0.337-0.055-0.606-0.166-0.804a1.217 1.217 0 0 0-0.44-0.464 1.737 1.737 0 0 0-0.639-0.22 5.292 5.292 0 0 0-0.782-0.055c-0.617 0-1.101 0.132-1.454 0.397-0.352 0.264-0.558 0.706-0.617 1.323h-3.128c0.044-0.735 0.227-1.345 0.55-1.83zm6.179 4.423a5.095 5.095 0 0 1-0.639 0.165 9.68 9.68 0 0 1-0.716 0.11c-0.25 0.03-0.5 0.067-0.749 0.11a5.616 5.616 0 0 0-0.694 0.177 2.057 2.057 0 0 0-0.594 0.298c-0.17 0.125-0.305 0.284-0.408 0.474-0.103 0.192-0.154 0.434-0.154 0.728 0 0.28 0.051 0.515 0.154 0.706 0.103 0.192 0.242 0.342 0.419 0.453 0.176 0.11 0.381 0.187 0.617 0.231 0.234 0.044 0.477 0.066 0.726 0.066 0.617 0 1.094-0.102 1.432-0.309 0.338-0.205 0.587-0.452 0.75-0.739 0.16-0.286 0.26-0.576 0.297-0.87 0.036-0.295 0.055-0.53 0.055-0.707v-1.17a1.4 1.4 0 0 1-0.496 0.277zm11.86-6.1v2.096h-2.291v5.647c0 0.53 0.088 0.883 0.264 1.059 0.176 0.177 0.529 0.265 1.057 0.265 0.177 0 0.345-7e-3 0.507-0.022 0.161-0.015 0.316-0.037 0.463-0.066v2.426a7.49 7.49 0 0 1-0.882 0.089 21.67 21.67 0 0 1-0.947 0.022c-0.484 0-0.944-0.034-1.377-0.1a3.233 3.233 0 0 1-1.145-0.386 2.04 2.04 0 0 1-0.782-0.816c-0.191-0.353-0.287-0.816-0.287-1.39v-6.728h-1.894v-2.096h1.894v-3.42h3.129v3.42h2.29zm4.471 0v2.118h0.044a3.907 3.907 0 0 1 1.454-1.754 4.213 4.213 0 0 1 1.036-0.497 3.734 3.734 0 0 1 1.145-0.176c0.206 0 0.433 0.037 0.683 0.11v2.912a5.862 5.862 0 0 0-0.528-0.077 5.566 5.566 0 0 0-0.595-0.033c-0.573 0-1.058 0.096-1.454 0.287a2.52 2.52 0 0 0-0.958 0.783 3.143 3.143 0 0 0-0.518 1.158 6.32 6.32 0 0 0-0.154 1.434v5.14h-3.128v-11.4zm5.684-1.765v-2.582h3.128v2.582h-3.127zm3.128 1.765v11.4h-3.127v-11.4h3.128zm1.63 0h3.569l2.005 2.978 1.982-2.978h3.459l-3.745 5.339 4.208 6.067h-3.57l-2.378-3.596-2.38 3.596h-3.502l4.097-6.001zm15.3 20.84v-30.52h-2.194v-0.732h3.035v31.98h-3.035v-0.732z"
|
||||||
|
id="path1" />
|
||||||
|
</g>
|
||||||
|
<metadata
|
||||||
|
id="metadata1">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:title>Matrix (protocol) logo</dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.9 KiB |
41
assets/img/icons/matrix.svg
Normal file
41
assets/img/icons/matrix.svg
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
fill="#000000"
|
||||||
|
width="800px"
|
||||||
|
height="800px"
|
||||||
|
viewBox="0 0 32 32"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="matrix.svg"
|
||||||
|
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="0.23142407"
|
||||||
|
inkscape:cx="412.66234"
|
||||||
|
inkscape:cy="557.41825"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1020"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="32"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg1" />
|
||||||
|
<path
|
||||||
|
d="M0.844 0.735v30.531h2.197v0.735h-3.041v-32h3.041v0.735zM10.235 10.412v1.547h0.041c0.412-0.595 0.912-1.047 1.489-1.371 0.579-0.323 1.251-0.484 2-0.484 0.719 0 1.38 0.141 1.975 0.417 0.599 0.281 1.047 0.776 1.359 1.479 0.339-0.5 0.803-0.943 1.38-1.323 0.579-0.38 1.267-0.573 2.063-0.573 0.604 0 1.161 0.073 1.677 0.224 0.521 0.145 0.959 0.38 1.328 0.703 0.365 0.329 0.651 0.751 0.86 1.272 0.203 0.52 0.307 1.151 0.307 1.891v7.635h-3.129v-6.468c0-0.381-0.016-0.745-0.048-1.084-0.020-0.307-0.099-0.604-0.239-0.88-0.131-0.251-0.333-0.459-0.584-0.593-0.255-0.152-0.609-0.224-1.047-0.224-0.443 0-0.797 0.083-1.068 0.249-0.265 0.167-0.489 0.396-0.64 0.667-0.161 0.287-0.265 0.604-0.308 0.927-0.052 0.349-0.077 0.699-0.083 1.048v6.359h-3.131v-6.401c0-0.339-0.005-0.672-0.025-1-0.011-0.317-0.073-0.624-0.193-0.916-0.104-0.281-0.301-0.516-0.552-0.672-0.255-0.167-0.636-0.255-1.136-0.255-0.151 0-0.348 0.031-0.588 0.099-0.24 0.067-0.479 0.192-0.703 0.375-0.229 0.188-0.428 0.453-0.589 0.797-0.161 0.343-0.239 0.796-0.239 1.359v6.62h-3.131v-11.421zM31.156 31.265v-30.531h-2.197v-0.735h3.041v32h-3.041v-0.735z"
|
||||||
|
id="path1"
|
||||||
|
style="fill-opacity:1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
125
assets/sfw.ejs
Normal file
125
assets/sfw.ejs
Normal 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>
|
||||||
@@ -449,6 +449,7 @@ const showExpand = [
|
|||||||
'bust',
|
'bust',
|
||||||
'cup',
|
'cup',
|
||||||
'eyes',
|
'eyes',
|
||||||
|
'ethnicity',
|
||||||
'hairColor',
|
'hairColor',
|
||||||
'hasPiercings',
|
'hasPiercings',
|
||||||
'hasTattoos',
|
'hasTattoos',
|
||||||
@@ -628,7 +629,9 @@ const socials = props.actor.socials.map((social) => ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bio-value {
|
.bio-value {
|
||||||
|
display: inline-block;
|
||||||
margin: 0 0 0 2rem;
|
margin: 0 0 0 2rem;
|
||||||
|
max-width: 20rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -851,7 +854,7 @@ const socials = props.actor.socials.map((social) => ({
|
|||||||
display: none;
|
display: none;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 10;
|
||||||
bottom: -.25rem;
|
bottom: -.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -957,6 +960,10 @@ const socials = props.actor.socials.map((social) => ({
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bio-value {
|
||||||
|
max-width: initial;
|
||||||
|
}
|
||||||
|
|
||||||
.expanded .bio-value {
|
.expanded .bio-value {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -18,27 +29,40 @@
|
|||||||
:href="campaign.url || campaign.affiliate?.url"
|
:href="campaign.url || campaign.affiliate?.url"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="campaign"
|
class="campaign"
|
||||||
|
:style="{ 'background-image': backdrop && `url(${bannerSrc})` }"
|
||||||
|
:class="{ backdrop }"
|
||||||
data-umami-event="campaign-click"
|
data-umami-event="campaign-click"
|
||||||
:data-umami-event-campaign-id="`${campaign.entity.slug}-${campaign.id}`"
|
:data-umami-event-campaign-id="`${campaign.entity.slug}-${campaign.id}`"
|
||||||
>
|
>
|
||||||
<img
|
<div class="campaign-overlay">
|
||||||
:src="bannerSrc"
|
<img
|
||||||
:width="campaign.banner.width"
|
:src="bannerSrc"
|
||||||
:height="campaign.banner.height"
|
:width="campaign.banner.width"
|
||||||
class="campaign-banner"
|
:height="campaign.banner.height"
|
||||||
>
|
class="campaign-banner"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</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,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
backdrop: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// console.log(props.campaign?.banner);
|
// console.log(props.campaign);
|
||||||
|
|
||||||
const bannerSrc = (() => {
|
const bannerSrc = (() => {
|
||||||
if (props.campaign.banner) {
|
if (props.campaign.banner) {
|
||||||
@@ -60,7 +84,10 @@ const bannerSrc = (() => {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
/* align-items: center; */
|
border-radius: .25rem;
|
||||||
|
overflow: hidden;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.frame {
|
.frame {
|
||||||
@@ -68,11 +95,45 @@ const bannerSrc = (() => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.campaign-overlay {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
box-sizing: border-box;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.campaign-banner {
|
.campaign-banner {
|
||||||
width: auto;
|
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.backdrop {
|
||||||
|
.campaign-overlay {
|
||||||
|
padding: 4px; /* margin for drop shadow */
|
||||||
|
backdrop-filter: blur(1rem) saturate(70%) brightness(125%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .campaign-overlay {
|
||||||
|
backdrop-filter: blur(1rem) saturate(70%) brightness(75%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.campaign-banner {
|
||||||
|
filter: drop-shadow(0 0 2px var(--shadow-weak-20));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.restricted {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: .5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: .5rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,6 +2,14 @@
|
|||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<span class="footer-segment">© traxxx</span>
|
<span class="footer-segment">© traxxx</span>
|
||||||
|
|
||||||
|
<a
|
||||||
|
v-if="env.links.matrix"
|
||||||
|
:href="env.links.matrix"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="footer-segment footer-link nolink matrix"
|
||||||
|
><Icon icon="matrix-full" /></a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
v-if="env.links.discord"
|
v-if="env.links.discord"
|
||||||
:href="env.links.discord"
|
:href="env.links.discord"
|
||||||
@@ -56,7 +64,8 @@ const { env } = pageContext;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.discord .icon {
|
.discord .icon,
|
||||||
|
.matrix .icon {
|
||||||
height: 1.25rem;
|
height: 1.25rem;
|
||||||
width: 3.5rem;
|
width: 3.5rem;
|
||||||
fill: var(--glass-strong-10);
|
fill: var(--glass-strong-10);
|
||||||
|
|||||||
@@ -46,6 +46,13 @@
|
|||||||
@play="playing = true; paused = false;"
|
@play="playing = true; paused = false;"
|
||||||
@pause="playing = false; paused = true;"
|
@pause="playing = false; paused = true;"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
v-if="(release.trailer || release.teaser).isRestricted"
|
||||||
|
v-tooltip="'Restricted video'"
|
||||||
|
icon="blocked"
|
||||||
|
class="restricted"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -71,9 +78,9 @@
|
|||||||
<div
|
<div
|
||||||
v-for="photo in [
|
v-for="photo in [
|
||||||
...(coversInAlbum ? release.covers : []),
|
...(coversInAlbum ? release.covers : []),
|
||||||
|
...(release.chapters?.map((chapter) => chapter.poster).filter(Boolean) || []),
|
||||||
...release.photos,
|
...release.photos,
|
||||||
...release.caps,
|
...release.caps,
|
||||||
...(release.chapters?.map((chapter) => chapter.poster).filter(Boolean) || []),
|
|
||||||
...(release.teaser?.mime.type === 'image' ? [release.poster] : []),
|
...(release.teaser?.mime.type === 'image' ? [release.poster] : []),
|
||||||
]"
|
]"
|
||||||
:key="`photo-${photo.id}`"
|
:key="`photo-${photo.id}`"
|
||||||
@@ -149,6 +156,10 @@ const coversInAlbum = props.release.covers?.length > 0 && props.release.trailer;
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark .banner {
|
||||||
|
backdrop-filter: brightness(70%) blur(1rem);
|
||||||
|
}
|
||||||
|
|
||||||
.poster-container {
|
.poster-container {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-right: .5rem;
|
margin-right: .5rem;
|
||||||
@@ -188,6 +199,15 @@ const coversInAlbum = props.release.covers?.length > 0 && props.release.trailer;
|
|||||||
width: calc(21/9 * 16rem);
|
width: calc(21/9 * 16rem);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
aspect-ratio: 16/9;
|
aspect-ratio: 16/9;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restricted {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
padding: .5rem;
|
||||||
|
fill: var(--highlight-weak-10);
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.player) {
|
:deep(.player) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<Campaign
|
<Campaign
|
||||||
v-if="campaigns?.pagination"
|
v-if="campaigns?.pagination"
|
||||||
:campaign="campaigns.pagination"
|
:campaign="campaigns.pagination"
|
||||||
|
class="campaign-pagination"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -301,6 +302,10 @@ function getPath(page) {
|
|||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.campaign-pagination) .campaign-banner {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
@media(--small-10) {
|
@media(--small-10) {
|
||||||
.campaign {
|
.campaign {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
@@ -52,6 +52,13 @@
|
|||||||
v-tooltip="'Duration'"
|
v-tooltip="'Duration'"
|
||||||
class="chapter-duration"
|
class="chapter-duration"
|
||||||
><Icon icon="stopwatch" />{{ formatDuration(chapter.duration) }}</span>
|
><Icon icon="stopwatch" />{{ formatDuration(chapter.duration) }}</span>
|
||||||
|
|
||||||
|
<time
|
||||||
|
v-if="chapter.date"
|
||||||
|
v-tooltip="formatDate(chapter.date, 'yyyy-MM-dd hh:mm')"
|
||||||
|
:datetime="chapter.date"
|
||||||
|
class="chapter-date"
|
||||||
|
>{{ formatDate(chapter.date, 'MMM d') }}</time>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div class="chapter-info">
|
<div class="chapter-info">
|
||||||
@@ -87,7 +94,7 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
import getPath from '#/src/get-path.js';
|
import getPath from '#/src/get-path.js';
|
||||||
import { formatDuration } from '#/utils/format.js';
|
import { formatDuration, formatDate } from '#/utils/format.js';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
chapters: {
|
chapters: {
|
||||||
@@ -138,9 +145,9 @@ const timeline = computed(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 .5rem;
|
padding: 0 .75rem;
|
||||||
border-radius: 0 0 .25rem .25rem;
|
border-radius: 0 0 .25rem .25rem;
|
||||||
margin: 0 0 .5rem 0;
|
margin: 0 0 .75rem 0;
|
||||||
color: var(--text-light);
|
color: var(--text-light);
|
||||||
background: var(--grey-dark-40);
|
background: var(--grey-dark-40);
|
||||||
font-size: .8rem;
|
font-size: .8rem;
|
||||||
@@ -164,7 +171,7 @@ const timeline = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chapter-info {
|
.chapter-info {
|
||||||
padding: 0 .5rem;
|
padding: 0 .75rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,6 +188,7 @@ const timeline = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chapter-description {
|
.chapter-description {
|
||||||
|
text-align: justify;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
user: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@
|
|||||||
<Campaign
|
<Campaign
|
||||||
v-if="campaigns?.meta"
|
v-if="campaigns?.meta"
|
||||||
:campaign="campaigns.meta"
|
:campaign="campaigns.meta"
|
||||||
|
class="campaign-meta"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="views">
|
<div class="views">
|
||||||
@@ -155,6 +156,7 @@
|
|||||||
<Campaign
|
<Campaign
|
||||||
v-if="campaigns?.scope"
|
v-if="campaigns?.scope"
|
||||||
:campaign="campaigns.scope"
|
:campaign="campaigns.scope"
|
||||||
|
class="campaign-scope"
|
||||||
/>
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -165,7 +167,10 @@
|
|||||||
v-if="item === 'campaign' && sceneCampaign"
|
v-if="item === 'campaign' && sceneCampaign"
|
||||||
:key="`campaign-${item.id}`"
|
:key="`campaign-${item.id}`"
|
||||||
>
|
>
|
||||||
<Campaign :campaign="sceneCampaign" />
|
<Campaign
|
||||||
|
:campaign="sceneCampaign"
|
||||||
|
:backdrop="true"
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li
|
<li
|
||||||
@@ -431,11 +436,11 @@ function setView(newView) {
|
|||||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
padding: .5rem 1rem 1rem 1rem;
|
padding: .5rem 1rem 1rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.campaign) .campaign-banner {
|
:deep(.campaign-meta) .campaign-banner,
|
||||||
border-radius: .25rem;
|
:deep(.campaign-scope) .campaign-banner {
|
||||||
box-shadow: 0 0 3px var(--shadow-weak-20);
|
width: auto;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.scopes {
|
.scopes {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
|
|
||||||
<Meta
|
<Meta
|
||||||
:scene="scene"
|
:scene="scene"
|
||||||
|
:user="user"
|
||||||
class="meta-full"
|
class="meta-full"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -129,7 +130,7 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
const pageContext = inject('pageContext');
|
||||||
const user = pageContext.user;
|
const { user } = pageContext;
|
||||||
const pageStash = pageContext.pageProps.stash;
|
const pageStash = pageContext.pageProps.stash;
|
||||||
const currentStash = pageStash || pageContext.assets?.primaryStash;
|
const currentStash = pageStash || pageContext.assets?.primaryStash;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="serie-tile">
|
<div class="serie-tile">
|
||||||
<a
|
<a
|
||||||
|
v-if="serie.poster"
|
||||||
:href="`/serie/${serie.id}/${serie.slug}`"
|
:href="`/serie/${serie.id}/${serie.slug}`"
|
||||||
class="poster-container"
|
class="poster-container"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="serie.poster"
|
|
||||||
:src="getPath(serie.poster, 'thumbnail')"
|
:src="getPath(serie.poster, 'thumbnail')"
|
||||||
:style="{ 'background-image': `url(${getPath(serie.poster, 'lazy')})` }"
|
:style="{ 'background-image': `url(${getPath(serie.poster, 'lazy')})` }"
|
||||||
class="poster"
|
class="poster"
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const cookies = Cookies.withConverter({
|
|||||||
const tags = {
|
const tags = {
|
||||||
anal: 'anal',
|
anal: 'anal',
|
||||||
'anal-prolapse': 'anal prolapse',
|
'anal-prolapse': 'anal prolapse',
|
||||||
|
'extreme-insertion': 'extreme insertion (oversized dildos)',
|
||||||
pissing: 'pissing',
|
pissing: 'pissing',
|
||||||
gay: 'gay',
|
gay: 'gay',
|
||||||
transsexual: 'transsexual',
|
transsexual: 'transsexual',
|
||||||
|
|||||||
@@ -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', { local: true })"
|
||||||
:style="{ 'background-image': `url(/${photo.lazy})` }"
|
:style="{ 'background-image': `url(${getPath(photo, 'lazy', { local: true })})` }"
|
||||||
: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,
|
||||||
|
|||||||
@@ -58,11 +58,101 @@ 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,
|
||||||
usernameLength: [2, 24],
|
usernameLength: [2, 24],
|
||||||
usernamePattern: /^[a-zA-Z0-9_-]+$/,
|
usernamePattern: /^[a-zA-Z0-9_-]+$/,
|
||||||
|
captcha: {
|
||||||
|
enabled: false,
|
||||||
|
siteKey: '10000000-ffff-ffff-ffff-000000000001',
|
||||||
|
secretKey: '0x0000000000000000000000000000000000000000',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
bans: {
|
bans: {
|
||||||
defaultExpiry: 60 * 24 * 3, // in minutes, 3 days
|
defaultExpiry: 60 * 24 * 3, // in minutes, 3 days
|
||||||
@@ -84,6 +174,7 @@ module.exports = {
|
|||||||
links: {
|
links: {
|
||||||
content: 'mailto:content@traxxx.me',
|
content: 'mailto:content@traxxx.me',
|
||||||
discord: 'https://discord.gg/gY6fnq6jJV',
|
discord: 'https://discord.gg/gY6fnq6jJV',
|
||||||
|
matrix: 'https://matrix.to/#/#traxxx:matrix.unknown.name',
|
||||||
},
|
},
|
||||||
stashes: {
|
stashes: {
|
||||||
nameLength: [2, 24],
|
nameLength: [2, 24],
|
||||||
@@ -96,8 +187,9 @@ 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
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ module.exports = {
|
|||||||
apps: [
|
apps: [
|
||||||
{
|
{
|
||||||
name: 'traxxx',
|
name: 'traxxx',
|
||||||
script: 'npm',
|
// script: 'npm',
|
||||||
args: 'run server:prod',
|
// args: 'run server:prod',
|
||||||
|
script: './src/app.js',
|
||||||
exec_mode: 'cluster',
|
exec_mode: 'cluster',
|
||||||
instances: 2,
|
instances: 2,
|
||||||
restart_delay: 3000,
|
restart_delay: 3000,
|
||||||
|
|||||||
242
package-lock.json
generated
242
package-lock.json
generated
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "traxxx-web",
|
"name": "traxxx-web",
|
||||||
"version": "0.42.23",
|
"version": "0.46.16",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"version": "0.42.23",
|
"version": "0.46.16",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@brillout/json-serializer": "^0.5.8",
|
"@brillout/json-serializer": "^0.5.8",
|
||||||
"@dicebear/collection": "^7.0.5",
|
"@dicebear/collection": "^7.0.5",
|
||||||
@@ -13,6 +13,8 @@
|
|||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@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",
|
||||||
|
"@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",
|
||||||
@@ -28,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",
|
||||||
@@ -38,6 +41,7 @@
|
|||||||
"graphql": "^16.9.0",
|
"graphql": "^16.9.0",
|
||||||
"graphql-parse-resolve-info": "^4.13.0",
|
"graphql-parse-resolve-info": "^4.13.0",
|
||||||
"graphql-scalars": "^1.24.2",
|
"graphql-scalars": "^1.24.2",
|
||||||
|
"hcaptcha": "^0.2.0",
|
||||||
"ip-cidr": "^4.0.0",
|
"ip-cidr": "^4.0.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"knex": "^3.1.0",
|
"knex": "^3.1.0",
|
||||||
@@ -50,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",
|
||||||
@@ -3023,6 +3028,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@hcaptcha/vue3-hcaptcha": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@hcaptcha/vue3-hcaptcha/-/vue3-hcaptcha-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-IEonS6JiYdU7uy6aeib8cYtMO4nj8utwStbA9bWHyYbOvOvhpkV+AW8vfSKh6SntYxqle/TRwhv+kU9p92CfsA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^3.2.19"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.11.13",
|
"version": "0.11.13",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
|
||||||
@@ -3210,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",
|
||||||
@@ -4513,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",
|
||||||
@@ -5563,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",
|
||||||
@@ -6776,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",
|
||||||
@@ -7319,6 +7391,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hcaptcha": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/hcaptcha/-/hcaptcha-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-x25z3RoEa9oqfyuQsgk2olc+LCNVDAJaGKUP1qFhpAybB6qjqOf4qB2y1E3LJpXDvM229JWEywc6iWnzWvGjNw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/html-encoding-sniffer": {
|
"node_modules/html-encoding-sniffer": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
|
||||||
@@ -8025,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",
|
||||||
@@ -8490,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",
|
||||||
@@ -8633,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",
|
||||||
@@ -8971,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",
|
||||||
@@ -10714,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",
|
||||||
@@ -13761,6 +13898,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@hcaptcha/vue3-hcaptcha": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@hcaptcha/vue3-hcaptcha/-/vue3-hcaptcha-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-IEonS6JiYdU7uy6aeib8cYtMO4nj8utwStbA9bWHyYbOvOvhpkV+AW8vfSKh6SntYxqle/TRwhv+kU9p92CfsA==",
|
||||||
|
"requires": {
|
||||||
|
"vue": "^3.2.19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@humanwhocodes/config-array": {
|
"@humanwhocodes/config-array": {
|
||||||
"version": "0.11.13",
|
"version": "0.11.13",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
|
||||||
@@ -13892,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",
|
||||||
@@ -14731,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",
|
||||||
@@ -15490,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",
|
||||||
@@ -16406,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",
|
||||||
@@ -16771,6 +16958,11 @@
|
|||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"hcaptcha": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/hcaptcha/-/hcaptcha-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-x25z3RoEa9oqfyuQsgk2olc+LCNVDAJaGKUP1qFhpAybB6qjqOf4qB2y1E3LJpXDvM229JWEywc6iWnzWvGjNw=="
|
||||||
|
},
|
||||||
"html-encoding-sniffer": {
|
"html-encoding-sniffer": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
|
||||||
@@ -17244,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",
|
||||||
@@ -17604,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",
|
||||||
@@ -17705,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",
|
||||||
@@ -17956,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",
|
||||||
@@ -19194,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",
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -13,6 +13,8 @@
|
|||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@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",
|
||||||
|
"@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",
|
||||||
@@ -28,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",
|
||||||
@@ -38,6 +41,7 @@
|
|||||||
"graphql": "^16.9.0",
|
"graphql": "^16.9.0",
|
||||||
"graphql-parse-resolve-info": "^4.13.0",
|
"graphql-parse-resolve-info": "^4.13.0",
|
||||||
"graphql-scalars": "^1.24.2",
|
"graphql-scalars": "^1.24.2",
|
||||||
|
"hcaptcha": "^0.2.0",
|
||||||
"ip-cidr": "^4.0.0",
|
"ip-cidr": "^4.0.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"knex": "^3.1.0",
|
"knex": "^3.1.0",
|
||||||
@@ -50,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",
|
||||||
@@ -87,7 +92,7 @@
|
|||||||
"overrides": {
|
"overrides": {
|
||||||
"vite": "$vite"
|
"vite": "$vite"
|
||||||
},
|
},
|
||||||
"version": "0.42.23",
|
"version": "0.46.16",
|
||||||
"imports": {
|
"imports": {
|
||||||
"#/*": "./*.js"
|
"#/*": "./*.js"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,16 @@
|
|||||||
class="row"
|
class="row"
|
||||||
>
|
>
|
||||||
<div class="item-header">
|
<div class="item-header">
|
||||||
<div class="key">{{ item.label || item.key }}</div>
|
<div class="key">
|
||||||
|
{{ item.label || item.key }}
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
v-if="item.note"
|
||||||
|
v-tooltip="item.note"
|
||||||
|
icon="info2"
|
||||||
|
class="item-note"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="item-actions noselect">
|
<div class="item-actions noselect">
|
||||||
<Icon
|
<Icon
|
||||||
@@ -443,6 +452,7 @@ const fields = computed(() => [
|
|||||||
{
|
{
|
||||||
key: 'augmentation',
|
key: 'augmentation',
|
||||||
type: 'augmentation',
|
type: 'augmentation',
|
||||||
|
note: 'Provide explicit evidence, such as social media posts, visible scarring, or before/after. Avoid "it\'s obvious".',
|
||||||
value: {
|
value: {
|
||||||
naturalBoobs: actor.value.naturalBoobs,
|
naturalBoobs: actor.value.naturalBoobs,
|
||||||
boobsVolume: actor.value.boobsVolume,
|
boobsVolume: actor.value.boobsVolume,
|
||||||
@@ -503,6 +513,7 @@ const fields = computed(() => [
|
|||||||
{
|
{
|
||||||
key: 'piercings',
|
key: 'piercings',
|
||||||
type: 'has',
|
type: 'has',
|
||||||
|
note: 'Excludes earrings',
|
||||||
value: {
|
value: {
|
||||||
has: actor.value.hasPiercings,
|
has: actor.value.hasPiercings,
|
||||||
description: actor.value.piercings,
|
description: actor.value.piercings,
|
||||||
@@ -685,10 +696,22 @@ async function submit() {
|
|||||||
|
|
||||||
.key {
|
.key {
|
||||||
width: 10rem;
|
width: 10rem;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item-note{
|
||||||
|
fill: var(--glass);
|
||||||
|
padding: .5rem .75rem;
|
||||||
|
cursor: help;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
fill: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="button button-submit">Log in</button>
|
<button
|
||||||
|
class="button button-submit"
|
||||||
|
:disabled="submitted"
|
||||||
|
>Log in</button>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
v-if="allowSignup"
|
v-if="allowSignup"
|
||||||
@@ -94,11 +97,13 @@ const allowSignup = pageContext.env.allowSignup;
|
|||||||
const username = ref('');
|
const username = ref('');
|
||||||
const password = ref('');
|
const password = ref('');
|
||||||
|
|
||||||
|
const submitted = ref(false);
|
||||||
const errorMsg = ref(null);
|
const errorMsg = ref(null);
|
||||||
const userInput = ref(null);
|
const userInput = ref(null);
|
||||||
const showPassword = ref(false);
|
const showPassword = ref(false);
|
||||||
|
|
||||||
async function login() {
|
async function login() {
|
||||||
|
submitted.value = true;
|
||||||
errorMsg.value = null;
|
errorMsg.value = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -111,6 +116,8 @@ async function login() {
|
|||||||
navigate(pageContext.urlParsed.search.r ? decodeURIComponent(pageContext.urlParsed.search.r) : `/user/${loginUser.username}`, null, { redirect: true });
|
navigate(pageContext.urlParsed.search.r ? decodeURIComponent(pageContext.urlParsed.search.r) : `/user/${loginUser.username}`, null, { redirect: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorMsg.value = error.message;
|
errorMsg.value = error.message;
|
||||||
|
} finally {
|
||||||
|
submitted.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="button button-submit">Sign up</button>
|
<VueHCaptcha
|
||||||
|
v-if="env.captcha.enabled"
|
||||||
|
:sitekey="env.captcha.siteKey"
|
||||||
|
class="captcha"
|
||||||
|
@verify="(verification) => captcha = verification"
|
||||||
|
@expired="captcha = null"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="button button-submit"
|
||||||
|
:disabled="submitted"
|
||||||
|
>Sign up</button>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="/login"
|
href="/login"
|
||||||
@@ -124,12 +135,13 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, inject } from 'vue';
|
import { ref, onMounted, inject } from 'vue';
|
||||||
|
import VueHCaptcha from '@hcaptcha/vue3-hcaptcha';
|
||||||
|
|
||||||
import { post } from '#/src/api.js';
|
import { post } from '#/src/api.js';
|
||||||
import navigate from '#/src/navigate.js';
|
import navigate from '#/src/navigate.js';
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
const pageContext = inject('pageContext');
|
||||||
const user = pageContext.user;
|
const { user, env } = pageContext;
|
||||||
|
|
||||||
const username = ref('');
|
const username = ref('');
|
||||||
const email = ref('');
|
const email = ref('');
|
||||||
@@ -137,28 +149,39 @@ const password = ref('');
|
|||||||
const passwordConfirm = ref('');
|
const passwordConfirm = ref('');
|
||||||
|
|
||||||
const errorMsg = ref(null);
|
const errorMsg = ref(null);
|
||||||
|
const submitted = ref(false);
|
||||||
const userInput = ref(null);
|
const userInput = ref(null);
|
||||||
const showPassword = ref(false);
|
const showPassword = ref(false);
|
||||||
|
const captcha = ref(null);
|
||||||
|
|
||||||
async function signup() {
|
async function signup() {
|
||||||
errorMsg.value = null;
|
errorMsg.value = null;
|
||||||
|
submitted.value = true;
|
||||||
|
|
||||||
if (password.value !== passwordConfirm.value) {
|
if (password.value !== passwordConfirm.value) {
|
||||||
errorMsg.value = 'Passwords do not match';
|
errorMsg.value = 'Passwords do not match';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (env.captcha.enabled && !captcha.value) {
|
||||||
|
errorMsg.value = 'Please complete the CAPTCHA';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newUser = await post('/users', {
|
const newUser = await post('/users', {
|
||||||
username: username.value,
|
username: username.value,
|
||||||
email: email.value,
|
email: email.value,
|
||||||
password: password.value,
|
password: password.value,
|
||||||
redirect: pageContext.urlParsed.search.r,
|
redirect: pageContext.urlParsed.search.r,
|
||||||
|
captcha: captcha.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
navigate(`/user/${newUser.username}`, null, { redirect: true });
|
navigate(`/user/${newUser.username}`, null, { redirect: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorMsg.value = error.message;
|
errorMsg.value = error.message;
|
||||||
|
} finally {
|
||||||
|
submitted.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,6 +252,16 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.captcha {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-submit {
|
||||||
|
margin-top: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
background: var(--error);
|
background: var(--error);
|
||||||
color: var(--text-light);
|
color: var(--text-light);
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ const popularNetworks = [
|
|||||||
'elegantangel',
|
'elegantangel',
|
||||||
'evilangel',
|
'evilangel',
|
||||||
'fakehub',
|
'fakehub',
|
||||||
|
'hentaied',
|
||||||
'hookuphotshot',
|
'hookuphotshot',
|
||||||
'hussiepass',
|
'hussiepass',
|
||||||
'julesjordan',
|
'julesjordan',
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -43,6 +43,14 @@
|
|||||||
>{{ entity.name }}</h2>
|
>{{ entity.name }}</h2>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
v-if="user?.abilities.some((ability) => ability.plainUrls)"
|
||||||
|
:href="entity.url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="plainurl"
|
||||||
|
><Icon icon="link" /></a>
|
||||||
|
|
||||||
<Heart
|
<Heart
|
||||||
domain="entities"
|
domain="entities"
|
||||||
:item="entity"
|
:item="entity"
|
||||||
@@ -154,7 +162,7 @@ import Movies from '#/components/movies/movies.vue';
|
|||||||
import Domains from '#/components/domains/domains.vue';
|
import Domains from '#/components/domains/domains.vue';
|
||||||
import Heart from '#/components/stashes/heart.vue';
|
import Heart from '#/components/stashes/heart.vue';
|
||||||
|
|
||||||
const { pageProps, routeParams } = inject('pageContext');
|
const { pageProps, routeParams, user } = inject('pageContext');
|
||||||
const { entity } = pageProps;
|
const { entity } = pageProps;
|
||||||
|
|
||||||
const children = ref(null);
|
const children = ref(null);
|
||||||
@@ -163,13 +171,7 @@ const expanded = ref(false);
|
|||||||
const scrollable = computed(() => children.value?.scrollWidth > children.value?.clientWidth);
|
const scrollable = computed(() => children.value?.scrollWidth > children.value?.clientWidth);
|
||||||
const domain = routeParams.domain;
|
const domain = routeParams.domain;
|
||||||
|
|
||||||
const entityUrl = (() => {
|
const entityUrl = entity.affiliateUrl || entity.url || null;
|
||||||
if (!entity.url) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return entity.affiliateUrl || entity.url;
|
|
||||||
})();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -317,6 +319,25 @@ const entityUrl = (() => {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.plainurl {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 .25rem;
|
||||||
|
margin-top: -.25rem;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 1rem;
|
||||||
|
height: auto;
|
||||||
|
padding: .5rem;
|
||||||
|
fill: var(--highlight);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .icon {
|
||||||
|
fill: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media(--small-20) {
|
@media(--small-20) {
|
||||||
.logo {
|
.logo {
|
||||||
padding: .5rem 1rem;
|
padding: .5rem 1rem;
|
||||||
|
|||||||
@@ -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),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
:href="scene.watchUrl"
|
:href="user?.abilities?.some((ability) => ability.plainUrls) ? scene.url : scene.watchUrl"
|
||||||
:title="scene.date ? formatDate(scene.date.toISOString(), 'y-MM-dd hh:mm') : `Release date unknown, added ${formatDate(scene.createdAt, 'y-MM-dd')}`"
|
:title="scene.date ? formatDate(scene.date.toISOString(), 'y-MM-dd hh:mm') : `Release date unknown, added ${formatDate(scene.createdAt, 'y-MM-dd')}`"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="date nolink"
|
class="date nolink"
|
||||||
@@ -249,12 +249,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Chapters
|
|
||||||
v-if="scene.chapters.length > 0"
|
|
||||||
:chapters="scene.chapters"
|
|
||||||
class="section"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="scene.description"
|
v-if="scene.description"
|
||||||
class="section"
|
class="section"
|
||||||
@@ -264,6 +258,18 @@
|
|||||||
<p class="description">{{ scene.description }}</p>
|
<p class="description">{{ scene.description }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section
|
||||||
|
v-if="scene.chapters.length > 0"
|
||||||
|
class="section"
|
||||||
|
>
|
||||||
|
<h3 class="heading">Chapters</h3>
|
||||||
|
|
||||||
|
<Chapters
|
||||||
|
:chapters="scene.chapters"
|
||||||
|
class="section"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="campaigns?.scene"
|
v-if="campaigns?.scene"
|
||||||
class="section"
|
class="section"
|
||||||
@@ -343,6 +349,43 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="user && scene.fingerprints.length > 0"
|
||||||
|
class="section fingerprints"
|
||||||
|
>
|
||||||
|
<h3 class="heading">Fingerprints</h3>
|
||||||
|
|
||||||
|
<div class="fingerprints-container">
|
||||||
|
<table class="fingerprints-table">
|
||||||
|
<thead class="fingerprints-head">
|
||||||
|
<tr class="fingerprints-header">
|
||||||
|
<th class="fingerprints-heading">Hash</th>
|
||||||
|
<th class="fingerprints-heading">Type</th>
|
||||||
|
<th class="fingerprints-heading">Duration</th>
|
||||||
|
<th class="fingerprints-heading">Submissions</th>
|
||||||
|
<th class="fingerprints-heading">Source</th>
|
||||||
|
<th class="fingerprints-heading">First added</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody class="fingerprints-body">
|
||||||
|
<tr
|
||||||
|
v-for="fingerprint in scene.fingerprints"
|
||||||
|
:key="`fingerprint-${fingerprint.hash}`"
|
||||||
|
class="fingerprint"
|
||||||
|
>
|
||||||
|
<td class="fingerprint-field fingerprint-hash">{{ fingerprint.hash }}</td>
|
||||||
|
<td class="fingerprint-field fingerprint-type">{{ fingerprint.type.toUpperCase() }}</td>
|
||||||
|
<td class="fingerprint-field fingerprint-duration">{{ formatDuration(fingerprint.duration) }}</td>
|
||||||
|
<td class="fingerprint-field fingerprint-submission">{{ fingerprint.submissions }}</td>
|
||||||
|
<td class="fingerprint-field fingerprint-source">{{ fingerprint.source || 'traxxx' }}</td>
|
||||||
|
<td class="fingerprint-field fingerprint-date">{{ formatDate(fingerprint.createdAt, 'yyyy-MM-dd') }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="scene-actions section"
|
class="scene-actions section"
|
||||||
>
|
>
|
||||||
@@ -740,6 +783,55 @@ function copySummary() {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fingerprints {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fingerprints-container {
|
||||||
|
max-height: 10rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
resize: vertical;
|
||||||
|
|
||||||
|
&[style*="height"] {
|
||||||
|
max-height: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fingerprints-table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fingerprints-head {
|
||||||
|
background: var(--background-base-10);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fingerprints-heading {
|
||||||
|
color: var(--glass);
|
||||||
|
font-weight: normal;
|
||||||
|
padding: .25rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fingerprint {
|
||||||
|
&:nth-child(2n + 1) {
|
||||||
|
background: var(--glass-weak-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--glass-weak-40);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fingerprint-field {
|
||||||
|
padding: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fingerprint-hash {
|
||||||
|
user-select: all;
|
||||||
|
}
|
||||||
|
|
||||||
@media(--compact) {
|
@media(--compact) {
|
||||||
.content {
|
.content {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -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([
|
||||||
|
|||||||
@@ -66,7 +66,16 @@
|
|||||||
class="row"
|
class="row"
|
||||||
>
|
>
|
||||||
<div class="item-header">
|
<div class="item-header">
|
||||||
<div class="key">{{ item.label || item.key }}</div>
|
<div class="key">
|
||||||
|
{{ item.label || item.key }}
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
v-if="item.note"
|
||||||
|
v-tooltip="item.note"
|
||||||
|
icon="info2"
|
||||||
|
class="item-note"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="item-actions">
|
<div class="item-actions">
|
||||||
<Icon
|
<Icon
|
||||||
@@ -262,11 +271,13 @@ const fields = computed(() => [
|
|||||||
key: 'title',
|
key: 'title',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
value: scene.value.title,
|
value: scene.value.title,
|
||||||
|
note: 'Do not correct language errors unless source was updated.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'description',
|
key: 'description',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
value: scene.value.description,
|
value: scene.value.description,
|
||||||
|
note: 'Do not correct language errors unless source was updated.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'date',
|
key: 'date',
|
||||||
@@ -408,6 +419,8 @@ async function submit() {
|
|||||||
|
|
||||||
.key {
|
.key {
|
||||||
width: 8rem;
|
width: 8rem;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
@@ -473,6 +486,16 @@ async function submit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item-note{
|
||||||
|
fill: var(--glass);
|
||||||
|
padding: .5rem .75rem;
|
||||||
|
cursor: help;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
fill: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.editor-footer {
|
.editor-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -16,22 +16,24 @@ export async function onBeforeRender(pageContext) {
|
|||||||
}), {
|
}), {
|
||||||
page: Number(pageContext.routeParams.page) || 1,
|
page: Number(pageContext.routeParams.page) || 1,
|
||||||
limit: Number(pageContext.urlParsed.search.limit) || 15,
|
limit: Number(pageContext.urlParsed.search.limit) || 15,
|
||||||
}, pageContext.user),
|
}, pageContext.user, { restriction: pageContext.restriction }),
|
||||||
fetchActors(curateActorsQuery(pageContext.urlQuery), {
|
fetchActors(curateActorsQuery(pageContext.urlQuery), {
|
||||||
page: Number(pageContext.routeParams.page) || 1,
|
page: Number(pageContext.routeParams.page) || 1,
|
||||||
limit: Number(pageContext.urlParsed.search.limit) || 10,
|
limit: Number(pageContext.urlParsed.search.limit) || 10,
|
||||||
order: ['results', 'desc'],
|
order: ['results', 'desc'],
|
||||||
}, pageContext.user),
|
}, pageContext.user, { restriction: pageContext.restriction }),
|
||||||
fetchMovies(await curateMoviesQuery({
|
fetchMovies(await curateMoviesQuery({
|
||||||
...pageContext.urlQuery,
|
...pageContext.urlQuery,
|
||||||
scope: pageContext.routeParams.scope || 'results',
|
scope: pageContext.routeParams.scope || 'results',
|
||||||
}), {
|
}), {
|
||||||
page: Number(pageContext.routeParams.page) || 1,
|
page: Number(pageContext.routeParams.page) || 1,
|
||||||
limit: Number(pageContext.urlParsed.search.limit) || 5,
|
limit: Number(pageContext.urlParsed.search.limit) || 5,
|
||||||
}, pageContext.user),
|
}, pageContext.user, { restriction: pageContext.restriction }),
|
||||||
fetchEntities({
|
fetchEntities({
|
||||||
query: pageContext.urlParsed.search.q,
|
query: pageContext.urlParsed.search.q,
|
||||||
limit: 5,
|
limit: 5,
|
||||||
|
}, {
|
||||||
|
restriction: pageContext.restriction,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -103,9 +103,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<time
|
<time
|
||||||
:datetime="serie.date.toISOString()"
|
:datetime="serie.effectiveDate.toISOString()"
|
||||||
class="date ellipsis"
|
class="date ellipsis compact-hide"
|
||||||
>{{ formatDate(serie.date, 'MMMM d, y') }}</time>
|
:class="{ nodate: !serie.date }"
|
||||||
|
>{{ formatDate(serie.effectiveDate, {
|
||||||
|
month: 'MMMM y',
|
||||||
|
year: 'y',
|
||||||
|
}[serie.datePrecision] || 'MMMM d, y') }}</time>
|
||||||
|
|
||||||
|
<time
|
||||||
|
:datetime="serie.effectiveDate.toISOString()"
|
||||||
|
class="date ellipsis compact-show"
|
||||||
|
:class="{ nodate: !serie.date }"
|
||||||
|
>{{ formatDate(serie.effectiveDate, {
|
||||||
|
month: 'MMM y',
|
||||||
|
year: 'y',
|
||||||
|
}[serie.datePrecision] || 'MMM d, y') }}</time>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header">
|
<div class="header">
|
||||||
@@ -408,6 +421,11 @@ const scenes = pageContext.pageProps.scenes;
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodate {
|
||||||
|
color: var(--highlight);
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
.info,
|
.info,
|
||||||
.header {
|
.header {
|
||||||
border-top: none;
|
border-top: none;
|
||||||
@@ -537,6 +555,10 @@ const scenes = pageContext.pageProps.scenes;
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.compact-show {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
@media(--small) {
|
@media(--small) {
|
||||||
.content {
|
.content {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -621,5 +643,13 @@ const scenes = pageContext.pageProps.scenes;
|
|||||||
.actors {
|
.actors {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(6.5rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(6.5rem, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.compact-show {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -75,8 +75,8 @@
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="tag.poster"
|
v-if="tag.poster"
|
||||||
:src="`/${tag.poster.thumbnail}`"
|
:src="getPath(tag.poster, 'thumbnail', { local: true })"
|
||||||
:style="{ 'background-image': `url(/${tag.poster.lazy})` }"
|
:style="{ 'background-image': `url(${getPath(tag.poster, 'lazy', { local: true })})` }"
|
||||||
: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;
|
||||||
|
|||||||
@@ -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: [
|
||||||
@@ -117,7 +118,7 @@ const tagSlugs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function searchTags(pageContext) {
|
async function searchTags(pageContext) {
|
||||||
const tags = await fetchTags({ query: pageContext.urlParsed.search.q });
|
const tags = await fetchTags({ query: pageContext.urlParsed.search.q }, { restriction: pageContext.restriction });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pageContext: {
|
pageContext: {
|
||||||
@@ -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),
|
||||||
]));
|
]));
|
||||||
|
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -9,5 +9,6 @@ export default {
|
|||||||
'assets',
|
'assets',
|
||||||
'campaigns',
|
'campaigns',
|
||||||
'meta',
|
'meta',
|
||||||
|
'restriction',
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
|
import format from 'template-format';
|
||||||
|
|
||||||
function getWatchUrl(scene) {
|
function getWatchUrl(scene) {
|
||||||
if (scene.url) {
|
try {
|
||||||
return scene.url;
|
if (scene.url) {
|
||||||
}
|
return new URL(scene.url).href;
|
||||||
|
}
|
||||||
|
|
||||||
if (scene.channel && (scene.channel.isIndependent || scene.channel.type === 'network')) {
|
if (scene.channel && (scene.channel.isIndependent || scene.channel.type === 'network')) {
|
||||||
return scene.channel.url;
|
return new URL(scene.channel.url).href;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scene.network) {
|
if (scene.network) {
|
||||||
return scene.network.url;
|
return new URL(scene.network.url).href;
|
||||||
|
}
|
||||||
|
} catch (_error) {
|
||||||
|
// invalid URL
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -21,16 +27,19 @@ export function getAffiliateSceneUrl(scene) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!scene.affiliate) {
|
if (!scene.affiliate || scene.affiliate.parameters.scene === false) {
|
||||||
return watchUrl;
|
return watchUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scene.affiliate.url?.includes('/track')
|
if (scene.affiliate.parameters.dynamicScene) {
|
||||||
&& scene.affiliate.parameters.scene !== false
|
const scenePath = new URL(watchUrl).pathname;
|
||||||
&& (!scene.channel.isIndependent || scene.channel.id === scene.affiliate.entityId)) { // standard NATS redirect
|
|
||||||
const { pathname, search } = new URL(watchUrl);
|
|
||||||
|
|
||||||
return `${scene.affiliate.url}${pathname.replace(/^\/trial/, '')}${search}`; // replace needed for Jules Jordan, verify behavior on other sites
|
return format(scene.affiliate.parameters.dynamicScene, {
|
||||||
|
scenePath: scene.affiliate.parameters.prefixSlash === false
|
||||||
|
? scenePath.replace(/^\//, '')
|
||||||
|
: scenePath,
|
||||||
|
entryId: scene.entryId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scene.affiliate.parameters.query) { // used by e.g. Bang
|
if (scene.affiliate.parameters.query) { // used by e.g. Bang
|
||||||
@@ -42,41 +51,98 @@ export function getAffiliateSceneUrl(scene) {
|
|||||||
return `${watchUrl}?${newParams.toString()}`;
|
return `${watchUrl}?${newParams.toString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const affiliateUrl = scene.affiliate.parameters.replaceScene?.hostname === new URL(watchUrl).hostname
|
||||||
|
? scene.affiliate.parameters.replaceScene.url
|
||||||
|
: scene.affiliate.url;
|
||||||
|
|
||||||
|
if (!affiliateUrl) {
|
||||||
|
return watchUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NATS deep URL
|
||||||
|
if (affiliateUrl.includes('/track')
|
||||||
|
&& scene.affiliate.parameters.scene !== false
|
||||||
|
&& (!scene.channel.isIndependent || scene.channel.id === scene.affiliate.entityId)) {
|
||||||
|
const { pathname, search } = new URL(watchUrl);
|
||||||
|
|
||||||
|
return `${affiliateUrl}${pathname.replace(/^\/trial/, '')}${search}`; // replace needed for Jules Jordan, verify behavior on other sites
|
||||||
|
}
|
||||||
|
|
||||||
|
const affiliateUrlComponents = new URL(affiliateUrl);
|
||||||
|
|
||||||
|
// NetFame / GammaE deep URL
|
||||||
|
if (affiliateUrlComponents.searchParams.has('pa') && affiliateUrlComponents.searchParams.has('ar')) {
|
||||||
|
affiliateUrlComponents.searchParams.set('pa', 'clip');
|
||||||
|
affiliateUrlComponents.searchParams.set('ar', scene.entryId);
|
||||||
|
|
||||||
|
return affiliateUrlComponents.href;
|
||||||
|
}
|
||||||
|
|
||||||
return watchUrl;
|
return watchUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAffiliateEntityUrl(entity) {
|
function getEntityUrl(entity) {
|
||||||
if (!entity.affiliate) {
|
try {
|
||||||
return entity.url;
|
return new URL(entity.url || entity.parent?.url).href;
|
||||||
|
} catch (_error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAffiliateEntityUrl(entity, affiliate) {
|
||||||
|
const entityUrl = getEntityUrl(entity);
|
||||||
|
const entityAffiliate = affiliate || entity.affiliate;
|
||||||
|
|
||||||
|
if (!entityUrl) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entity.id === entity.affiliate.entityId) {
|
if (!entityAffiliate) {
|
||||||
return entity.affiliate.url;
|
return entityUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
const affiliateUrl = entityAffiliate.parameters?.replaceEntity?.hostname === new URL(entityUrl).hostname
|
||||||
|
? entityAffiliate.parameters.replaceEntity.url
|
||||||
|
: entityAffiliate.url;
|
||||||
|
|
||||||
|
if (affiliateUrl && (entity.id === entityAffiliate.entityId || entityUrl === entity.parent?.url)) {
|
||||||
|
return affiliateUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entityAffiliate.parameters?.query) {
|
||||||
|
const newParams = new URLSearchParams({
|
||||||
|
...Object.fromEntries(new URL(entityUrl).searchParams),
|
||||||
|
...Object.fromEntries(new URLSearchParams(entityAffiliate.parameters.query)),
|
||||||
|
});
|
||||||
|
|
||||||
|
return `${entityUrl}?${newParams.toString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entity.type === 'network' || entity.isIndependent) {
|
if (entity.type === 'network' || entity.isIndependent) {
|
||||||
return entity.url;
|
return entityUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (new URL(entity.url).hostname !== new URL(entity.affiliate.url).hostname) {
|
// channel has its own domain
|
||||||
return entity.url;
|
if (new URL(entityUrl).pathname === '/' && entityUrl !== entity.parent?.url) {
|
||||||
|
return entityUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entity.affiliate.url?.includes('/track')
|
if (entityAffiliate.parameters.dynamicEntity) {
|
||||||
&& entity.affiliate.parameters.channel !== false) {
|
const entityPath = new URL(entityUrl).pathname;
|
||||||
const { pathname, search } = new URL(entity.url);
|
|
||||||
|
|
||||||
return `${entity.affiliate.url}${pathname.replace(/^\/trial/, '')}${search}`; // replace needed for Jules Jordan, verify behavior on other sites
|
return format(entityAffiliate.parameters.dynamicEntity, {
|
||||||
}
|
entityPath: entityAffiliate.parameters.prefixSlash
|
||||||
|
? entityPath
|
||||||
if (entity.affiliate.parameters.query) { // used by e.g. Bang
|
: entityPath.replace(/^\//, ''),
|
||||||
const newParams = new URLSearchParams({
|
|
||||||
...Object.fromEntries(new URL(entity.url).searchParams),
|
|
||||||
...Object.fromEntries(new URLSearchParams(entity.affiliate.parameters.query)),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return `${entity.url}?${newParams.toString()}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return entity.url;
|
if (affiliateUrl?.includes('/track')
|
||||||
|
&& entityAffiliate.parameters.channel !== false) {
|
||||||
|
const { pathname, search } = new URL(entityUrl);
|
||||||
|
|
||||||
|
return `${affiliateUrl}${pathname.replace(/^\/trial/, '')}${search}`; // replace needed for Jules Jordan, verify behavior on other sites
|
||||||
|
}
|
||||||
|
|
||||||
|
return entityUrl;
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/argv.js
10
src/argv.js
@@ -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;
|
||||||
|
|||||||
12
src/auth.js
12
src/auth.js
@@ -5,6 +5,7 @@ import fs from 'fs/promises';
|
|||||||
import { createAvatar } from '@dicebear/core';
|
import { createAvatar } from '@dicebear/core';
|
||||||
import { shapes } from '@dicebear/collection';
|
import { shapes } from '@dicebear/collection';
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { verify } from 'hcaptcha';
|
||||||
|
|
||||||
import { knexOwner as knex } from './knex.js';
|
import { knexOwner as knex } from './knex.js';
|
||||||
import redis from './redis.js';
|
import redis from './redis.js';
|
||||||
@@ -105,6 +106,15 @@ export async function signup(credentials, userIp) {
|
|||||||
throw new HttpError('Password must be 3 characters or longer', 400);
|
throw new HttpError('Password must be 3 characters or longer', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.auth.captcha.enabled) {
|
||||||
|
const captchaVerification = await verify(config.auth.captcha.secretKey, credentials.captcha);
|
||||||
|
|
||||||
|
if (!captchaVerification.success) {
|
||||||
|
logger.warn(`Invalid sign-up CAPTCHA from '${curatedUsername}' (${credentials.email}, ${userIp})`);
|
||||||
|
throw new HttpError('Invalid CAPTCHA', 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const existingUser = await knex('users')
|
const existingUser = await knex('users')
|
||||||
.where(knex.raw('lower(username)'), curatedUsername.toLowerCase())
|
.where(knex.raw('lower(username)'), curatedUsername.toLowerCase())
|
||||||
.orWhere(knex.raw('lower(email)'), credentials.email.toLowerCase())
|
.orWhere(knex.raw('lower(email)'), credentials.email.toLowerCase())
|
||||||
@@ -134,7 +144,7 @@ export async function signup(credentials, userIp) {
|
|||||||
primary: true,
|
primary: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.verbose(`Signup from '${curatedUsername}' (${userId}, ${credentials.email}, ${userIp})`);
|
logger.info(`Signup from '${curatedUsername}' (${userId}, ${credentials.email}, ${userIp})`);
|
||||||
|
|
||||||
await generateAvatar({
|
await generateAvatar({
|
||||||
id: userId,
|
id: userId,
|
||||||
|
|||||||
@@ -4,18 +4,41 @@ import { knexOwner as knex } from './knex.js';
|
|||||||
import { curateEntity } from './entities.js';
|
import { curateEntity } from './entities.js';
|
||||||
import redis from './redis.js';
|
import redis from './redis.js';
|
||||||
import initLogger from './logger.js';
|
import initLogger from './logger.js';
|
||||||
|
import { getAffiliateEntityUrl } from './affiliates.js';
|
||||||
|
|
||||||
const logger = initLogger();
|
const logger = initLogger();
|
||||||
|
|
||||||
|
function getCampaignUrl(campaign, entity) {
|
||||||
|
if (!campaign) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (campaign.url) {
|
||||||
|
return campaign.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (campaign.affiliate?.url) {
|
||||||
|
return campaign.affiliate.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (campaign.entity) {
|
||||||
|
// resolve e.g. parameter tracking
|
||||||
|
return getAffiliateEntityUrl(entity, campaign.affiliate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function curateCampaign(campaign) {
|
function curateCampaign(campaign) {
|
||||||
if (!campaign) {
|
if (!campaign) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const entity = campaign.entity && curateEntity({ ...campaign.entity, parent: campaign.parent_entity });
|
||||||
|
|
||||||
|
const curatedCampaign = {
|
||||||
id: campaign.id,
|
id: campaign.id,
|
||||||
url: campaign.url,
|
entity,
|
||||||
entity: campaign.entity && curateEntity({ ...campaign.entity, parent: campaign.parent_entity }),
|
|
||||||
banner: campaign.banner && {
|
banner: campaign.banner && {
|
||||||
id: campaign.banner.id,
|
id: campaign.banner.id,
|
||||||
type: campaign.banner.type,
|
type: campaign.banner.type,
|
||||||
@@ -31,6 +54,10 @@ function curateCampaign(campaign) {
|
|||||||
parameters: campaign.affiliate.parameters,
|
parameters: campaign.affiliate.parameters,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
curatedCampaign.url = getCampaignUrl(campaign, entity);
|
||||||
|
|
||||||
|
return curatedCampaign;
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectRandomCampaign(primaryCampaigns, entityCampaigns, preferredCampaigns) {
|
function selectRandomCampaign(primaryCampaigns, entityCampaigns, preferredCampaigns) {
|
||||||
@@ -55,18 +82,27 @@ export async function getRandomCampaign(options = {}, context = {}, pass = 0) {
|
|||||||
|
|
||||||
const validCampaigns = campaigns.filter((campaign) => {
|
const validCampaigns = campaigns.filter((campaign) => {
|
||||||
if (options.minRatio && (!campaign.banner || campaign.banner.ratio < options.minRatio)) {
|
if (options.minRatio && (!campaign.banner || campaign.banner.ratio < options.minRatio)) {
|
||||||
|
// too small
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.maxRatio && (!campaign.banner || campaign.banner.ratio > options.maxRatio)) {
|
if (options.maxRatio && (!campaign.banner || campaign.banner.ratio > options.maxRatio)) {
|
||||||
|
// too big
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.entityIds && !options.entityIds.some((entityId) => campaign.entity.id === entityId || campaign.entity.parent?.id === entityId)) {
|
if (options.entityIds && !options.entityIds.some((entityId) => campaign.entity.id === entityId || campaign.entity.parent?.id === entityId)) {
|
||||||
|
// this is an entity page, this campaign does not belong to this entity
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (campaign.affiliate?.parameters?.global === false && !options.entityIds) {
|
||||||
|
// this campaign should only show on entity page
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.tagFilter && campaign.banner && campaign.banner.tags.some((tag) => context.tagFilter.includes(tag) && !options.tagSlugs?.includes(tag))) {
|
if (context.tagFilter && campaign.banner && campaign.banner.tags.some((tag) => context.tagFilter.includes(tag) && !options.tagSlugs?.includes(tag))) {
|
||||||
|
// wrong tag
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
55
src/censor.js
Normal file
55
src/censor.js
Normal 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;
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
@@ -39,12 +46,12 @@ export function curateEntity(entity, context) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
curatedEntity.affiliateUrl = getAffiliateEntityUrl(curatedEntity, curatedEntity);
|
curatedEntity.affiliateUrl = getAffiliateEntityUrl(curatedEntity);
|
||||||
|
|
||||||
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),
|
||||||
|
|||||||
@@ -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}`;
|
||||||
|
|||||||
11
src/knex.js
11
src/knex.js
@@ -1,15 +1,6 @@
|
|||||||
import config from 'config';
|
import config from 'config';
|
||||||
import knex from 'knex';
|
import knex from 'knex';
|
||||||
|
|
||||||
export const knexQuery = knex({
|
|
||||||
client: 'pg',
|
|
||||||
connection: config.database.query,
|
|
||||||
pool: config.database.pool,
|
|
||||||
// performance overhead, don't use asyncStackTraces in production
|
|
||||||
asyncStackTraces: process.env.NODE_ENV === 'development',
|
|
||||||
// debug: process.env.NODE_ENV === 'development',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const knexOwner = knex({
|
export const knexOwner = knex({
|
||||||
client: 'pg',
|
client: 'pg',
|
||||||
connection: config.database.owner,
|
connection: config.database.owner,
|
||||||
@@ -19,6 +10,8 @@ export const knexOwner = knex({
|
|||||||
// debug: process.env.NODE_ENV === 'development',
|
// debug: process.env.NODE_ENV === 'development',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const knexQuery = knexOwner; // legacy
|
||||||
|
|
||||||
export const knexManticore = knex({
|
export const knexManticore = knex({
|
||||||
client: 'mysql',
|
client: 'mysql',
|
||||||
connection: {
|
connection: {
|
||||||
|
|||||||
@@ -30,5 +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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
115
src/scenes.js
115
src/scenes.js
@@ -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,32 +16,34 @@ 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) {
|
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,
|
||||||
date: rawScene.date,
|
date: rawScene.date,
|
||||||
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,
|
||||||
@@ -48,7 +51,7 @@ function curateScene(rawScene, 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.network_has_logo,
|
hasLogo: assets.channel.network_has_logo,
|
||||||
} : null,
|
} : null,
|
||||||
@@ -77,15 +80,19 @@ function curateScene(rawScene, 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),
|
||||||
priority: tag.priority,
|
priority: tag.priority,
|
||||||
})),
|
})),
|
||||||
chapters: assets.chapters.map((chapter) => ({
|
chapters: assets.chapters.map((chapter) => ({
|
||||||
id: chapter.id,
|
id: chapter.id,
|
||||||
title: chapter.title,
|
title: chapter.title,
|
||||||
|
description: chapter.description,
|
||||||
time: chapter.time,
|
time: chapter.time,
|
||||||
|
date: chapter.date,
|
||||||
duration: chapter.duration,
|
duration: chapter.duration,
|
||||||
poster: curateMedia(chapter.chapter_poster),
|
poster: context.restriction
|
||||||
|
? null
|
||||||
|
: curateMedia(chapter.chapter_poster, { type: '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,
|
||||||
@@ -97,26 +104,43 @@ function curateScene(rawScene, assets) {
|
|||||||
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' }),
|
||||||
trailer: curateMedia(assets.trailer, { type: 'trailer' }),
|
|
||||||
teaser: curateMedia(assets.teaser, { type: 'teaser' }),
|
|
||||||
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)) || [],
|
stashes: assets.stashes?.map((stash) => curateStash(stash)) || [],
|
||||||
|
fingerprints: assets.fingerprints?.map((fingerprint) => ({
|
||||||
|
hash: fingerprint.hash,
|
||||||
|
type: fingerprint.type,
|
||||||
|
duration: fingerprint.duration,
|
||||||
|
source: fingerprint.source,
|
||||||
|
submissions: fingerprint.source_submissions,
|
||||||
|
createdAt: fingerprint.created_at,
|
||||||
|
})) || [],
|
||||||
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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isVideoRestricted = config.media.videoRestrictions.includes(curatedScene.channel.slug) || config.media.videoRestrictions.includes(`_${curatedScene.network?.slug}`);
|
||||||
|
|
||||||
|
if (!isVideoRestricted || reqUser?.abilities?.some((ability) => ability.trailerAccess)) {
|
||||||
|
curatedScene.trailer = curateMedia(assets.trailer, { type: 'trailer', isRestricted: isVideoRestricted });
|
||||||
|
curatedScene.teaser = curateMedia(assets.teaser, { type: 'teaser', isRestricted: isVideoRestricted });
|
||||||
|
}
|
||||||
|
|
||||||
curatedScene.watchUrl = getAffiliateSceneUrl(curatedScene);
|
curatedScene.watchUrl = getAffiliateSceneUrl(curatedScene);
|
||||||
|
|
||||||
return curatedScene;
|
return curatedScene;
|
||||||
@@ -138,8 +162,9 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
|||||||
caps,
|
caps,
|
||||||
trailers,
|
trailers,
|
||||||
teasers,
|
teasers,
|
||||||
|
fingerprints,
|
||||||
stashes,
|
stashes,
|
||||||
lastBatch: { id: lastBatchId },
|
lastBatch,
|
||||||
} = await promiseProps({
|
} = await promiseProps({
|
||||||
scenes: knex('releases').whereIn('releases.id', sceneIds),
|
scenes: knex('releases').whereIn('releases.id', sceneIds),
|
||||||
channels: knex('releases')
|
channels: knex('releases')
|
||||||
@@ -174,14 +199,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'),
|
||||||
@@ -219,17 +247,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) {
|
||||||
@@ -237,9 +271,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) {
|
||||||
@@ -259,6 +296,20 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
|||||||
.whereIn('release_id', sceneIds)
|
.whereIn('release_id', sceneIds)
|
||||||
.leftJoin('media', 'media.id', 'releases_teasers.media_id');
|
.leftJoin('media', 'media.id', 'releases_teasers.media_id');
|
||||||
}) : [],
|
}) : [],
|
||||||
|
fingerprints: context.includeAssets ? knex.transaction(async (trx) => {
|
||||||
|
if (reqUser) {
|
||||||
|
await trx.select(knex.raw('set_config(\'user.id\', :userId, true)', { userId: reqUser.id }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return trx('releases_fingerprints')
|
||||||
|
.select('scene_id', 'hash', 'type', 'duration', 'source', 'source_submissions', knex.raw('min(coalesce(source_created_at, created_at)) as created_at'))
|
||||||
|
.whereIn('scene_id', sceneIds)
|
||||||
|
.orderBy([
|
||||||
|
{ column: 'source_submissions', order: 'desc' },
|
||||||
|
{ column: knex.raw('min(coalesce(source_created_at, created_at))'), order: 'desc' },
|
||||||
|
])
|
||||||
|
.groupBy(['scene_id', 'hash', 'type', 'duration', 'source', 'source_submissions']);
|
||||||
|
}) : [],
|
||||||
lastBatch: knex('batches')
|
lastBatch: knex('batches')
|
||||||
.select('id')
|
.select('id')
|
||||||
.where('showcased', true)
|
.where('showcased', true)
|
||||||
@@ -299,6 +350,7 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
|||||||
const sceneCaps = caps.filter((cap) => cap.release_id === sceneId);
|
const sceneCaps = caps.filter((cap) => cap.release_id === sceneId);
|
||||||
const sceneTrailers = trailers.find((trailer) => trailer.release_id === sceneId);
|
const sceneTrailers = trailers.find((trailer) => trailer.release_id === sceneId);
|
||||||
const sceneTeasers = teasers.find((teaser) => teaser.release_id === sceneId);
|
const sceneTeasers = teasers.find((teaser) => teaser.release_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);
|
||||||
|
|
||||||
@@ -316,10 +368,11 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
|||||||
caps: sceneCaps,
|
caps: sceneCaps,
|
||||||
trailer: sceneTrailers,
|
trailer: sceneTrailers,
|
||||||
teaser: sceneTeasers,
|
teaser: sceneTeasers,
|
||||||
|
fingerprints: sceneFingerprints,
|
||||||
stashes: sceneStashes,
|
stashes: sceneStashes,
|
||||||
actorStashes: sceneActorStashes,
|
actorStashes: sceneActorStashes,
|
||||||
lastBatchId,
|
lastBatchId: lastBatch?.id,
|
||||||
});
|
}, reqUser, context);
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,9 +505,11 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
|||||||
builder.where('scenes.is_showcased', filters.isShowcased);
|
builder.where('scenes.is_showcased', filters.isShowcased);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
if (filters.isShowcased) {
|
if (filters.isShowcased) {
|
||||||
builder.where('scenes.date', '>', 0);
|
builder.where('scenes.date', '>', 0);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
if (options.dedupe) {
|
if (options.dedupe) {
|
||||||
builder.where('scenes.dupe_index', '<', 2);
|
builder.where('scenes.dupe_index', '<', 2);
|
||||||
@@ -512,7 +567,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -570,14 +625,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 }));
|
||||||
@@ -590,16 +649,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 {
|
||||||
|
|||||||
27
src/tags.js
27
src/tags.js
@@ -1,17 +1,18 @@
|
|||||||
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';
|
||||||
|
|
||||||
const logger = initLogger();
|
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)) || [],
|
||||||
@@ -23,7 +24,7 @@ function curateTag(tag, context) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchTags(options = {}) {
|
export async function fetchTags(options = {}, context = {}) {
|
||||||
const query = options.query?.trim();
|
const query = options.query?.trim();
|
||||||
|
|
||||||
const [tags, posters] = await Promise.all([
|
const [tags, posters] = await Promise.all([
|
||||||
@@ -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]));
|
||||||
@@ -66,10 +70,10 @@ export async function fetchTags(options = {}) {
|
|||||||
return tags.map((tagEntry) => curateTag({
|
return tags.map((tagEntry) => curateTag({
|
||||||
...tagEntry,
|
...tagEntry,
|
||||||
poster: postersByTagId[tagEntry.id],
|
poster: postersByTagId[tagEntry.id],
|
||||||
}));
|
}, context));
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
|
|||||||
18
src/users.js
18
src/users.js
@@ -29,6 +29,7 @@ export function curateUser(user, _assets = {}) {
|
|||||||
isIdentityVerified: user.identity_verified,
|
isIdentityVerified: user.identity_verified,
|
||||||
avatar: `/media/avatars/${user.id}_${user.username}.png`,
|
avatar: `/media/avatars/${user.id}_${user.username}.png`,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
|
abilities: [...user.role_abilities || [], ...user.abilities || []],
|
||||||
createdAt: user.created_at,
|
createdAt: user.created_at,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -61,23 +62,6 @@ export async function fetchUser(userId, options = {}, _reqUser) {
|
|||||||
throw new HttpError(`User '${userId}' not found`, 404);
|
throw new HttpError(`User '${userId}' not found`, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
const [stashes, templates] = await Promise.all([
|
|
||||||
knex('stashes')
|
|
||||||
.select('stashes.*', 'stashes_meta.*')
|
|
||||||
.leftJoin('stashes_meta', 'stashes_meta.stash_id', 'stashes.id')
|
|
||||||
.where('user_id', user.id)
|
|
||||||
.modify((builder) => {
|
|
||||||
if (reqUser?.id !== user.id && !options.includeStashes) {
|
|
||||||
builder.where('public', true);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
options.includeTemplates
|
|
||||||
? knex('users_templates').where('user_id', user.id)
|
|
||||||
: null,
|
|
||||||
]);
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (options.raw) {
|
if (options.raw) {
|
||||||
// return { user, stashes, templates };
|
// return { user, stashes, templates };
|
||||||
return { user };
|
return { user };
|
||||||
|
|||||||
@@ -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('.')
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export default async function mainHandler(req, res, next) {
|
|||||||
username: req.user.username,
|
username: req.user.username,
|
||||||
email: req.user.email,
|
email: req.user.email,
|
||||||
role: req.user.role,
|
role: req.user.role,
|
||||||
|
abilities: req.user.abilities,
|
||||||
avatar: req.user.avatar,
|
avatar: req.user.avatar,
|
||||||
},
|
},
|
||||||
assets: req.user ? {
|
assets: req.user ? {
|
||||||
@@ -45,7 +46,12 @@ export default async function mainHandler(req, res, next) {
|
|||||||
psa: config.psa,
|
psa: config.psa,
|
||||||
links: config.links,
|
links: config.links,
|
||||||
socials,
|
socials,
|
||||||
|
captcha: {
|
||||||
|
enabled: config.auth.captcha.enabled,
|
||||||
|
siteKey: config.auth.captcha.siteKey,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
restriction: req.restriction,
|
||||||
meta: {
|
meta: {
|
||||||
now: new Date().toISOString(),
|
now: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export async function fetchMoviesApi(req, res) {
|
|||||||
} = await fetchMovies(await curateMoviesQuery(req.query), {
|
} = await fetchMovies(await curateMoviesQuery(req.query), {
|
||||||
page: Number(req.query.page) || 1,
|
page: Number(req.query.page) || 1,
|
||||||
limit: Number(req.query.limit) || 30,
|
limit: Number(req.query.limit) || 30,
|
||||||
}, req.user);
|
}, req.user, { restriction: req.restriction });
|
||||||
|
|
||||||
res.send(stringify({
|
res.send(stringify({
|
||||||
movies,
|
movies,
|
||||||
@@ -47,7 +47,7 @@ export async function fetchMoviesApi(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchMovieApi(req, res) {
|
export async function fetchMovieApi(req, res) {
|
||||||
const [movie] = await fetchMoviesById([Number(req.params.movieId)], { reqUser: req.user });
|
const [movie] = await fetchMoviesById([Number(req.params.movieId)], { reqUser: req.user }, { restriction: req.restriction });
|
||||||
|
|
||||||
if (!movie) {
|
if (!movie) {
|
||||||
throw new HttpError(`No movie with ID ${req.params.movieId} found`, 404);
|
throw new HttpError(`No movie with ID ${req.params.movieId} found`, 404);
|
||||||
@@ -137,7 +137,7 @@ export async function fetchMoviesGraphql(query, req) {
|
|||||||
page: query.page || 1,
|
page: query.page || 1,
|
||||||
limit: query.limit || 30,
|
limit: query.limit || 30,
|
||||||
aggregate: false,
|
aggregate: false,
|
||||||
}, req.user);
|
}, req.user, { restriction: req.restriction });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
nodes: movies,
|
nodes: movies,
|
||||||
|
|||||||
80
src/web/restrictions.js
Normal file
80
src/web/restrictions.js
Normal 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;
|
||||||
|
}
|
||||||
@@ -68,7 +68,9 @@ async function fetchScenesApi(req, res) {
|
|||||||
}), {
|
}), {
|
||||||
page: Number(req.query.page) || 1,
|
page: Number(req.query.page) || 1,
|
||||||
limit: Number(req.query.limit) || 30,
|
limit: Number(req.query.limit) || 30,
|
||||||
}, req.user);
|
}, req.user, {
|
||||||
|
restriction: req.restriction,
|
||||||
|
});
|
||||||
|
|
||||||
res.send(stringify({
|
res.send(stringify({
|
||||||
scenes,
|
scenes,
|
||||||
@@ -250,7 +252,7 @@ export async function fetchScenesGraphql(query, req) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchSceneApi(req, res) {
|
async function fetchSceneApi(req, res) {
|
||||||
const [scene] = await fetchScenesById([Number(req.params.sceneId)], { reqUser: req.user });
|
const [scene] = await fetchScenesById([Number(req.params.sceneId)], { reqUser: req.user }, { restriction: req.restriction });
|
||||||
|
|
||||||
if (!scene) {
|
if (!scene) {
|
||||||
throw new HttpError(`No scene with ID ${req.params.sceneId} found`, 404);
|
throw new HttpError(`No scene with ID ${req.params.sceneId} found`, 404);
|
||||||
@@ -263,6 +265,7 @@ export async function fetchScenesByIdGraphql(query, req) {
|
|||||||
const scenes = await fetchScenesById([].concat(query.id, query.ids).filter(Boolean), {
|
const scenes = await fetchScenesById([].concat(query.id, query.ids).filter(Boolean), {
|
||||||
reqUser: req.user,
|
reqUser: req.user,
|
||||||
includePartOf: true,
|
includePartOf: true,
|
||||||
|
restriction: req.restriction,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (query.ids) {
|
if (query.ids) {
|
||||||
|
|||||||
@@ -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');
|
||||||
@@ -175,7 +180,9 @@ export default async function initServer() {
|
|||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
||||||
const port = process.env.PORT || config.web.port || 3000;
|
const port = process.env.PORT || config.web.port || 3000;
|
||||||
app.listen(port);
|
// const port = Math.round(Math.random() * 10000);
|
||||||
|
|
||||||
logger.info(`Server running at http://localhost:${port}`);
|
app.listen(port, config.web.host);
|
||||||
|
|
||||||
|
logger.info(`Server running at http://${config.web.host}:${port}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { fetchTags } from '../tags.js';
|
|||||||
export async function fetchTagsApi(req, res) {
|
export async function fetchTagsApi(req, res) {
|
||||||
const tags = await fetchTags({
|
const tags = await fetchTags({
|
||||||
query: req.query.query,
|
query: req.query.query,
|
||||||
|
}, {
|
||||||
|
restriction: req.restriction,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.send(tags);
|
res.send(tags);
|
||||||
|
|||||||
2
static
2
static
Submodule static updated: d1ce4d1258...bb5b4f01b4
Reference in New Issue
Block a user