1
0
mirror of https://github.com/journey-ad/Moe-Counter.git synced 2024-11-30 18:24:28 +01:00

feat: Add params validate

This commit is contained in:
roffy3051 2024-10-20 08:29:58 +08:00
parent cc1881cc4f
commit 77cac2c504
10 changed files with 316 additions and 93 deletions

View File

@ -84,4 +84,4 @@ Glitch can use `.env` file, [documentation](https://help.glitch.com/hc/en-us/art
## License
[MIT License](./LICENSE)
[MIT License](./LICENSE), excluding all themes

BIN
assets/img/back-to-top.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -34,6 +34,69 @@ h2, h3, h4, h5 {
margin-bottom: .6em;
}
.back-to-top {
position: fixed;
z-index: 2;
right: -108px;
bottom: 0;
width: 108px;
height: 150px;
background: url('./img/back-to-top.png?v=1') no-repeat 0 0; /* artwork from https://www.pixiv.net/artworks/83996495 */
background-size: 108px 450px;
opacity: 0.6;
transition: opacity 0.3s, right 0.8s;
cursor: pointer;
}
.back-to-top:hover {
background-position: 0 -150px;
opacity: 1;
}
.back-to-top.load {
right: 0;
}
.back-to-top.ani-leave {
background-position: 0 -150px;
animation: ani-leave 390ms ease-in-out;
animation-fill-mode: forwards;
}
.back-to-top.leaved {
pointer-events: none;
background: none;
transition: none;
}
.back-to-top.ending {
pointer-events: none;
}
.back-to-top.ending::after {
opacity: 1;
transition-delay: 0.35s;
}
.back-to-top::after {
content: '';
position: fixed;
z-index: 2;
right: 0;
bottom: 0;
width: 108px;
height: 150px;
background: url('./img/back-to-top.png?v=1') no-repeat 0 0;
background-size: 108px 450px;
background-position: 0 -300px;
transition: opacity 0.3s;
opacity: 0;
pointer-events: none;
}
@keyframes ani-leave {
0% {
transform: translateX(0);
}
100% {
transform: translateX(108px);
}
}
@media screen and (max-width: 900px) {
iframe {
display: none;

104
index.js
View File

@ -3,9 +3,12 @@
const config = require("config-yml");
const express = require("express");
const compression = require("compression");
const { z } = require("zod");
const db = require("./db");
const themify = require("./utils/themify");
const { themeList, getCountImage } = require("./utils/themify");
const { ZodValid } = require("./utils/zod");
const { randomArray } = require("./utils");
const app = express();
@ -15,56 +18,65 @@ app.set("view engine", "pug");
app.get('/', (req, res) => {
const site = config.app.site || `${req.protocol}://${req.get('host')}`
res.render('index', { site })
res.render('index', {
site,
themeList,
})
});
// get the image
app.get(["/@:name", "/get/@:name"], async (req, res) => {
const { name } = req.params;
const { theme = "moebooru", padding = 7, pixelated = '1', darkmode = 'auto' } = req.query;
const isPixelated = pixelated === '1';
app.get(["/@:name", "/get/@:name"],
ZodValid({
params: z.object({
name: z.string().max(32),
}),
query: z.object({
theme: z.string().default("moebooru"),
padding: z.coerce.number().min(0).max(32).default(7),
offset: z.coerce.number().min(-500).max(500).default(0),
scale: z.coerce.number().min(0.1).max(2).default(1),
pixelated: z.enum(["0", "1"]).default("1"),
darkmode: z.enum(["0", "1", "auto"]).default("auto")
})
}),
async (req, res) => {
const { name } = req.params;
let { theme = "moebooru", ...rest } = req.query;
if (name.length > 32) {
res.status(400).send("name too long");
return;
// This helps with GitHub's image cache
res.set({
"content-type": "image/svg+xml",
"cache-control": "max-age=0, no-cache, no-store, must-revalidate",
});
const data = await getCountByName(name);
if (name === "demo") {
res.set("cache-control", "max-age=31536000");
}
if (theme === "random") {
theme = randomArray(Object.keys(themeList));
}
// Send the generated SVG as the result
const renderSvg = getCountImage({
count: data.num,
theme,
...rest
});
res.send(renderSvg);
console.log(
data,
`theme: ${theme}`,
`ip: ${req.headers['x-forwarded-for'] || req.connection.remoteAddress}`,
`ref: ${req.get("Referrer") || null}`,
`ua: ${req.get("User-Agent") || null}`
);
}
if (padding > 32) {
res.status(400).send("padding too long");
return;
}
// This helps with GitHub's image cache
res.set({
"content-type": "image/svg+xml",
"cache-control": "max-age=0, no-cache, no-store, must-revalidate",
});
const data = await getCountByName(name);
if (name === "demo") {
res.set("cache-control", "max-age=31536000");
}
// Send the generated SVG as the result
const renderSvg = themify.getCountImage({
count: data.num,
theme,
padding,
darkmode,
pixelated: isPixelated
});
res.send(renderSvg);
console.log(
data,
`theme: ${theme}`,
`ip: ${req.headers['x-forwarded-for'] || req.connection.remoteAddress}`,
`ref: ${req.get("Referrer") || null}`,
`ua: ${req.get("User-Agent") || null}`
);
});
);
// JSON record
app.get("/record/@:name", async (req, res) => {

View File

@ -18,6 +18,10 @@
"image-size": "^0.8.3",
"mime-types": "^2.1.27",
"mongoose": "^5.9.28",
"pug": "^3.0.0"
"pug": "^3.0.0",
"zod": "^3.23.8"
},
"engines": {
"node": "16.x"
}
}

7
pnpm-lock.yaml generated
View File

@ -29,6 +29,9 @@ dependencies:
pug:
specifier: ^3.0.0
version: 3.0.3
zod:
specifier: ^3.23.8
version: 3.23.8
packages:
@ -1665,3 +1668,7 @@ packages:
y18n: 3.2.2
yargs-parser: 2.4.1
dev: false
/zod@3.23.8:
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
dev: false

5
utils/index.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
randomArray: (arr) => {
return arr[Math.floor(Math.random() * arr.length)]
},
}

View File

@ -14,10 +14,10 @@ fs.readdirSync(themePath).forEach(theme => {
const imgList = fs.readdirSync(path.resolve(themePath, theme))
imgList.forEach(img => {
const imgPath = path.resolve(themePath, theme, img)
const name = path.parse(img).name
const num = path.parse(img).name
const { width, height } = sizeOf(imgPath)
themeList[theme][name] = {
themeList[theme][num] = {
width,
height,
data: convertToDatauri(imgPath)
@ -32,8 +32,12 @@ function convertToDatauri(path) {
return `data:${mime};base64,${base64}`
}
function getCountImage({ count, theme = 'moebooru', padding = 7, pixelated = true, darkmode = 'auto' }) {
function getCountImage(params) {
let { count, theme = 'moebooru', padding = 7, offset = 0, scale = 1, pixelated = '1', darkmode = 'auto' } = params
if (!(theme in themeList)) theme = 'moebooru'
padding = parseInt(padding, 10)
offset = parseInt(offset, 10)
// This is not the greatest way for generating an SVG but it'll do for now
const countArray = count.toString().padStart(padding, '0').split('')
@ -42,7 +46,9 @@ function getCountImage({ count, theme = 'moebooru', padding = 7, pixelated = tru
let x = 0, y = 0
const defs = uniqueChar.reduce((ret, cur) => {
const { width, height, data } = themeList[theme][cur]
let { width, height, data } = themeList[theme][cur]
width *= scale
height *= scale
if (height > y) y = height
@ -53,19 +59,23 @@ function getCountImage({ count, theme = 'moebooru', padding = 7, pixelated = tru
}, '')
const parts = countArray.reduce((ret, cur) => {
const { width } = themeList[theme][cur]
let { width } = themeList[theme][cur]
width *= scale
const image = `${ret}
<use x="${x}" xlink:href="#${cur}" />`
x += width
x += width + offset
return image
}, '')
// Fix the last image offset
x -= offset
const style = `
svg {
${pixelated ? 'image-rendering: pixelated;' : ''}
${pixelated === '1' ? 'image-rendering: pixelated;' : ''}
${darkmode === '1' ? 'filter: brightness(.6);' : ''}
}
${darkmode === 'auto' ? `@media (prefers-color-scheme: dark) { svg { filter: brightness(.6); } }` : ''}
@ -85,5 +95,6 @@ function getCountImage({ count, theme = 'moebooru', padding = 7, pixelated = tru
}
module.exports = {
themeList,
getCountImage
}

50
utils/zod.js Normal file
View File

@ -0,0 +1,50 @@
function parseError(error) {
const err = JSON.parse(error)[0];
return {
code: 400,
message: `The field \`${err.path[0]}\` is invalid. ${err.message}`,
}
}
module.exports = {
ZodValid: ({ headers, params, query, body }) => {
const handler = (req, res, next) => {
if (headers) {
const result = headers.safeParse(req.headers);
if (!result.success) {
res.status(400).send(parseError(result.error));
return;
}
}
if (params) {
const result = params.safeParse(req.params);
if (!result.success) {
res.status(400).send(parseError(result.error));
return;
}
}
if (query) {
const result = query.safeParse(req.query);
if (!result.success) {
res.status(400).send(parseError(result.error));
return;
}
}
if (body) {
const result = body.safeParse(req.body);
if (!result.success) {
res.status(400).send(parseError(result.error));
return;
}
}
next();
}
return handler
}
}

View File

@ -35,7 +35,7 @@ html
h3 How to use
p Set a unique id for your counter, replace
code :name
| in the url, that's all.
| in the url, That's it!
h5 SVG address
code #{site}/@:name
@ -47,24 +47,15 @@ html
code ![:name](#{site}/@:name)
h5 e.g.
<img src="#{site}/@index" alt="Moe Count!" />
<img src="#{site}/@index" alt="Moe Counter!" />
details#themes
details#themes(style='margin-top: 2em;')
summary#more_theme(onclick='_evt_push("click", "normal", "more_theme")')
h3(style='display: inline-block; margin: 0; cursor: pointer;') More theme✨
p(style='margin: 0;') Just use the query parameters <code>theme</code>, like this: <code>#{site}/@:name?theme=moebooru</code>
h5 asoul
<img src="#{site}/@demo?theme=asoul" alt="A-SOUL" />
h5 moebooru
<img src="#{site}/@demo?theme=moebooru" alt="Moebooru" />
h5 moebooru-h
<img src="#{site}/@demo?theme=moebooru-h" alt="Moebooru-Hentai" />
h5 rule34
<img src="#{site}/@demo?theme=rule34" alt="Rule34" />
h5 gelbooru
<img src="#{site}/@demo?theme=gelbooru" alt="Gelbooru" />
h5 gelbooru-h
<img src="#{site}/@demo?theme=gelbooru-h" alt="Gelbooru-Hentai" />
each theme in Object.keys(themeList)
h5 #{theme}
<img src="#{site}/@demo?theme=#{theme}" alt="#{theme}" />
h3 Credits
ul
@ -74,14 +65,13 @@ html
a(href='https://space.bilibili.com/703007996', target='_blank', title='A-SOUL_Official') A-SOUL
li
a(href='https://github.com/moebooru/moebooru', target='_blank', rel='nofollow') moebooru
li
a(href='javascript:alert("!!! NSFW LINK !!!\\nPlease enter the url manually")') rule34.xxx
| NSFW
li
a(href='javascript:alert("!!! NSFW LINK !!!\\nPlease enter the url manually")') gelbooru.com
| NSFW
li
a(href='https://icons8.com/icon/80355/star', target='_blank', rel='nofollow') Icons8
span
i And all booru site...
h3 Tool
.tool
@ -89,37 +79,58 @@ html
thead
tr
th Param
th Description
th Value
tbody
tr
td
code name
td Unique counter name
td
input#name(type='text', placeholder=':name')
tr
td
code theme
td Select a counter image theme, default is
code moebooru
td
select#theme
option(value='asoul') asoul
option(value='moebooru') moebooru
option(value='moebooru-h') moebooru-h
option(value='rule34') rule34
option(value='gelbooru') gelbooru
option(value='gelbooru-h') gelbooru-h
tr
td
code pixelated
td
input#pixelated(type='checkbox', checked, style='margin: .5rem .75rem;')
option(value="random", selected) * random
each theme in Object.keys(themeList)
<option value="#{theme}">#{theme}</option>
tr
td
code padding
td Set the minimum length, between 1-32, default is
code 7
td
input#padding(type='number', value='7', min='1', max='32', step='1', oninput='this.value = this.value.replace(/[^0-9]/g, "")')
tr
td
code offset
td Set the offset pixel value, between -500-500, default is
code 0
td
input#offset(type='number', value='0', min='-500', max='500', step='1', oninput='this.value = this.value.replace(/[^0-9|\-]/g, "")')
tr
td
code scale
td Set the image scale, between 0.1-2, default is
code 1
td
input#scale(type='number', value='1', min='0.1', max='2', step='0.1', oninput='this.value = this.value.replace(/[^0-9|\.]/g, "")')
tr
td
code pixelated
td Enable pixelated mode, Enum 0/1, default is
code 1
td
input#pixelated(type='checkbox', checked, style='margin: .5rem .75rem;')
tr
td
code darkmode
td Enable dark mode, Enum 0/1/auto, default is
code auto
td
select#darkmode(name="darkmode")
option(value="auto", selected) auto
@ -132,7 +143,13 @@ html
code#code(style='visibility: hidden; display: inline-block; margin-bottom: 1em;')
img#result(style='display: block;')
script.
p(style='margin-top: 2em;')
a(href='https://github.com/journey-ad/Moe-Counter', target='_blank', onclick='_evt_push("click", "normal", "go_github")') source code
div.back-to-top
script.
(function () {
var btn = document.getElementById('get'),
img = document.getElementById('result'),
code = document.getElementById('code')
@ -140,15 +157,13 @@ html
btn.addEventListener('click', throttle(function() {
var $name = document.getElementById('name'),
$theme = document.getElementById('theme'),
$pixelated = document.getElementById('pixelated'),
$padding = document.getElementById('padding'),
$offset = document.getElementById('offset'),
$scale = document.getElementById('scale'),
$pixelated = document.getElementById('pixelated'),
$darkmode = document.getElementById('darkmode')
var name = $name.value ? $name.value.trim() : ''
var theme = $theme.value || 'moebooru'
var pixelated = $pixelated.checked ? '1' : '0'
var padding = $padding.value || '7'
var darkmode = $darkmode.value || 'auto'
if(!name) {
alert('Please input counter name.')
@ -157,7 +172,19 @@ html
party.confetti(this, { count: party.variation.range(20, 40) });
img.src = `#{site}/@${name}?theme=${theme}&pixelated=${pixelated}&padding=${padding}&darkmode=${darkmode}`
var params = {
name: $name.value ? $name.value.trim() : '',
theme: $theme.value || 'moebooru',
padding: $padding.value || '7',
offset: $offset.value || '0',
scale: $scale.value || '1',
pixelated: $pixelated.checked ? '1' : '0',
darkmode: $darkmode.value || 'auto',
}
var query = new URLSearchParams(params).toString()
img.src = `#{site}/@${name}?${query}`
code.textContent = img.src
code.style.visibility = 'visible'
@ -220,6 +247,50 @@ html
}
}
}
})();
p(style='margin-top: 2em;')
a(href='https://github.com/journey-ad/Moe-Counter', target='_blank', onclick='_evt_push("click", "normal", "go_github")') source code
script.
(function () {
var isShow = false, lock = false;
var btn = document.querySelector('.back-to-top');
window.addEventListener('scroll', function () {
if (lock) return;
if (document.body.scrollTop >= 1000) {
if (!isShow) btn.classList.add('load');
isShow = true;
} else {
if (isShow) {
btn.classList.remove('load');
isShow = false;
}
}
});
btn.addEventListener('click', function () {
lock = true;
btn.classList.add('ani-leave');
window.scrollTo({ top: 0, behavior: 'smooth' });
setTimeout(function () {
btn.classList.remove('ani-leave');
btn.classList.add('leaved');
}, 390);
setTimeout(function () {
btn.classList.add('ending');
}, 120);
setTimeout(function () {
btn.classList.remove('load');
}, 1500);
setTimeout(function () {
lock = false;
isShow = false;
btn.classList.remove('leaved', 'ending');
}, 2000);
});
})();