refactor: cf worker version

This commit is contained in:
dsrkafuu 2022-03-13 15:21:27 +08:00
parent c3cf12769c
commit 7f9acbb07e
90 changed files with 2033 additions and 2598 deletions

13
.eslintrc.json Normal file
View File

@ -0,0 +1,13 @@
{
"env": {
"browser": true,
"node": true,
"es2021": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {}
}

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto

55
.gitignore vendored
View File

@ -1,26 +1,39 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# identity
wrangler.toml
/.history
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
# output
dist/
# logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
yarn.lock*
lerna-debug.log*
# diagnostic reports
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# runtime data
pids
*.pid
*.seed
*.pid.lock
# dependency directories
package-lock.json
.npm/
.pnpm-store/
node_modules/
jspm_packages/
web_modules/
# system
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

13
.prettierrc Normal file
View File

@ -0,0 +1,13 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"quoteProps": "as-needed",
"jsxSingleQuote": true,
"trailingComma": "es5",
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always"
}

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020 journey-ad
Copyright (c) 2020 journey-ad, 2022 DSRKafuU
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

133
Readme.md
View File

@ -1,81 +1,84 @@
# Moe-counter
# Moe Counter CF
多种风格可选的萌萌计数器
Fork of Moe Counter for fast global access powered by Cloudflare Workers.
![Moe-counter](https://count.getloli.com/get/@Moe-counter.github)
<details>
<summary>More theme</summary>
##### asoul
![asoul](https://count.getloli.com/get/@demo?theme=asoul)
##### moebooru
![moebooru](https://count.getloli.com/get/@demo?theme=moebooru)
##### rule34
![Rule34](https://count.getloli.com/get/@demo?theme=rule34)
##### gelbooru
![Gelbooru](https://count.getloli.com/get/@demo?theme=gelbooru)</details>
[Source Project](https://github.com/journey-ad/Moe-counter) | [Cloudflare Workers](https://workers.cloudflare.com/) | [Workers KV](https://www.cloudflare.com/products/workers-kv/)
## Demo
[https://count.getloli.com](https://count.getloli.com)
![Gelbooru](https://count.dsrkafuu.net/dsrkafuu:demo?theme=gelbooru)
<details>
<summary>More Themes</summary>
**A-SOUL**
![A-SOUL](https://count.dsrkafuu.net/dsrkafuu:demo?theme=asoul)
**Moebooru**
![Moebooru](https://count.dsrkafuu.net/dsrkafuu:demo?theme=moebooru)
##### Rule 34
![Rule 34](https://count.dsrkafuu.net/dsrkafuu:demo?theme=rule34)
##### Gelbooru
![Gelbooru](https://count.dsrkafuu.net/dsrkafuu:demo?theme=gelbooru)
</details>
## Usage
### Install
#### Run on Repl.it
- Open the url [https://repl.it/@journeyad/Moe-counter](https://repl.it/@journeyad/Moe-counter)
- Just hit the **Fork** button
- And hit the **Run** button
#### Deploying on your own server
```shell
$ git clone https://github.com/journey-ad/Moe-counter.git
$ cd Moe-counter
$ yarn install
$ yarn start
```
### Confignation
`config.yml`
```yaml
app:
port: 3000
db:
type: mongodb # sqlite or mongodb
```
If you use mongodb, you need to specify the environment variable `DB_URL`
```shell
# eg:
export DB_URL=mongodb+srv://account:passwd@***.***.***.mongodb.net/db_count
```
repl.it can use `.env` file, [documentation](https://docs.repl.it/repls/secret-keys)
**Public Counter**
```
DB_URL="mongodb+srv://account:passwd@***.***.***.mongodb.net/db_count"
https://count.dsrkafuu.net/<id>
https://count.dsrkafuu.net/<id>?theme=<theme>&length=<length>
https://count.dsrkafuu.net/dsrkafuu:demo?theme=gelbooru
```
1. `<id>`: A string between 1-256 chars (`0-9a-zA-Z!@#$%^&*_-`) starting with a letter (`a-zA-Z`)
2. `<theme>`: `asoul`, `gelbooru`, `moebooru`, `rule34` (and two other themes, default is `gelbooru`)
3. `<length>`: Number between 1-10 (default: 7)
Recommend to use `user:usage` like string as ID for better management.
**API Endpoints**
```
GET https://count.dsrkafuu.net/api/<id>
DELETE https://count.dsrkafuu.net/api/<id>
```
DELETE is not enabled by default, create a issue if you need to use it in public counter.
**HTML and Markdown**
```
<img src="https://count.dsrkafuu.net/<id>" alt="<id>" />
![<id>](https://count.dsrkafuu.net/<id>)
```
## Self-hosting
1. Create a Cloudflare Workers worker
2. Create a Cloudflare Workers KV store
3. Create your own `wrangler.toml` based on the example
4. Build the worker and publish it using `wrangler publish`
## Credits
* [repl.it](https://repl.it/)
* [A-SOUL](https://www.asoulworld.com/) <sup>(非官方导航站)</sup>
* [moebooru](https://github.com/moebooru/moebooru)
* rule34.xxx NSFW
* gelbooru.com NSFW
* [Icons8](https://icons8.com/icons/set/star)
- [A-SOUL](https://space.bilibili.com/703007996)
- [Moebooru](https://github.com/moebooru/moebooru)
- [Rule 34 (**NSFW**)](https://rule34.xxx/)
- [Gelbooru (**NSFW**)](https://gelbooru.com/)
## License
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fjourney-ad%2FMoe-counter.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fjourney-ad%2FMoe-counter?ref=badge_large)
This project and all contributors shall not be responsible for any dispute or loss caused by using this project.
This project is released under the `MIT` License, for more information read the [License](https://github.com/dsrkafuu/moe-counter/blob/master/LICENSE).
**Copyright (c) 2020 journey-ad, 2022 DSRKafuU**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -1,5 +0,0 @@
@media screen and (max-width: 900px) {
iframe {
display: none;
}
}

View File

@ -1,5 +0,0 @@
app:
port: 3000
db:
type: mongodb # sqlite or mongodb

View File

@ -1,17 +0,0 @@
'use strict'
const config = require('config-yml')
let db
switch(config.db.type){
case 'mongodb':
db = require('./mongodb')
break;
case 'sqlite':
default:
db = require('./sqlite')
break;
}
module.exports = db

View File

@ -1,39 +0,0 @@
'use strict'
const mongoose = require('mongoose')
const schema = require('./schema')
// the default mongodb url (local server)
const mongodbURL = process.env.DB_URL || 'mongodb://127.0.0.1:27017'
mongoose.connect(mongodbURL, {
useNewUrlParser: true,
useUnifiedTopology: true,
useFindAndModify: false
})
const Count = mongoose.connection.model('Count', schema)
function getNum(name) {
return Count
.findOne({ name }, '-_id -__v')
.exec()
}
function getAll() {
return Count
.find({ }, '-_id -__v')
.exec()
}
function setNum(name, num) {
return Count
.findOneAndUpdate({ name }, { name, num }, { upsert: true })
.exec()
}
module.exports = {
getNum,
getAll,
setNum
}

View File

@ -1,8 +0,0 @@
'use strict'
const mongoose = require('mongoose');
module.exports = new mongoose.Schema({
name: { type: String, required: true },
num: { type: Number, required: true }
}, { collection: 'tb_count', versionKey: false });

View File

@ -1,60 +0,0 @@
'use strict'
const path = require('path')
const sqlite3 = require('sqlite3')
const db = new sqlite3.Database(path.resolve(__dirname, '../count.db'))
db.run(`CREATE TABLE IF NOT EXISTS tb_count (
id INTEGER PRIMARY KEY AUTOINCREMENT
NOT NULL
UNIQUE,
name VARCHAR (32) NOT NULL
UNIQUE,
num BIGINT NOT NULL
DEFAULT (0)
);`)
function getNum(name) {
return new Promise((resolve, reject) => {
db.get('SELECT `name`, `num` from tb_count WHERE `name` = ?', name, (err, row) => {
if (err) reject(err)
resolve(row || { name, num: 0 })
})
})
}
function getAll(name) {
return new Promise((resolve, reject) => {
db.get('SELECT * from tb_count', (err, row) => {
if (err) reject(err)
resolve(row)
})
})
}
function setNum(name, num) {
return new Promise((resolve, reject) => {
db.run(`INSERT INTO tb_count(\`name\`, \`num\`)
VALUES($name, $num)
ON CONFLICT(name) DO
UPDATE SET \`num\` = $num;`
, {
$name: name,
$num: num
}
, (err, row) => {
if (err) reject(err)
resolve(row)
})
})
}
module.exports = {
getNum,
getAll,
setNum
}

View File

@ -1,89 +0,0 @@
'use strict'
const fs = require('fs')
const config = require('config-yml')
const express = require('express')
const compression = require('compression')
const db = require('./db')
const themify = require('./utils/themify')
const PLACES = 7
const app = express()
app.use(express.static('assets'))
app.use(compression())
app.set('view engine', 'pug')
app.get('/', (req, res) => {
res.render('index')
});
// get the image
app.get('/get/@:name', async (req, res) => {
const { name } = req.params
const { theme = 'moebooru' } = req.query
let length = PLACES
// 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'
})
length = 10
}
// Send the generated SVG as the result
const renderSvg = themify.getCountImage({ count: data.num, theme, length })
res.send(renderSvg)
console.log(data, `theme: ${theme}`)
})
// JSON record
app.get('/record/@:name', async (req, res) => {
const { name } = req.params
const data = await getCountByName(name)
res.json(data)
})
app.get('/heart-beat', (req, res) => {
res.set({
'cache-control': 'max-age=0, no-cache, no-store, must-revalidate'
})
res.send('alive')
console.log('heart-beat')
});
const listener = app.listen(config.app.port || 3000, () => {
console.log('Your app is listening on port ' + listener.address().port)
})
async function getCountByName(name) {
const defaultCount = { name, num: 0 }
if (name === 'demo') return { name, num: '0123456789' }
try {
const counter = await db.getNum(name) || defaultCount
const num = counter.num + 1
db.setNum(counter.name, num)
return counter
} catch (error) {
console.log("get count by name is error: ", error)
return defaultCount
}
}

2091
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,25 @@
{
"name": "kawaii-counter",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "journey-ad",
"name": "moe-counter-cf",
"version": "2.0.0",
"description": "Fork of Moe Counter for fast global access powered by Cloudflare Workers.",
"author": "DSRKafuU <dsrkafuu@outlook.com> (https://dsrkafuu.net)",
"license": "MIT",
"main": "./dist/worker.js",
"scripts": {
"lint": "eslint \"./src\" && prettier --write",
"build": "webpack",
"dev": "wrangler dev --host count.dsrkafuu.net",
"publish": "wrangler publish"
},
"dependencies": {
"compression": "^1.7.4",
"config-yml": "^0.10.3",
"express": "^4.17.1",
"image-size": "^0.8.3",
"mime-types": "^2.1.27",
"mongoose": "^5.9.28",
"pug": "^3.0.0",
"sqlite3": "^5.0.0"
"itty-router": "~2.5.2"
},
"devDependencies": {
"@cloudflare/wrangler": "~1.19.8",
"clean-webpack-plugin": "~4.0.0",
"eslint": "~8.10.0",
"prettier": "~2.5.1",
"webpack": "~5.70.0",
"webpack-cli": "~4.9.2"
}
}

1502
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

26
src/index.js Normal file
View File

@ -0,0 +1,26 @@
import { Router } from 'itty-router';
import { genErrorResponse, ResError } from './response';
import * as index from './routes/index';
import * as favicon from './routes/favicon';
import * as image from './routes/image';
import * as api from './routes/api';
const router = Router();
// routes
router.get('/', index.get);
router.get('/favicon.ico', favicon.get);
router.get('/:id', image.get);
router.get('/api/:id', api.get);
// router.delete('/api/:id', api.del);
// 404
router.all('*', async () => {
throw new ResError(404, 'Route Not Found');
});
addEventListener('fetch', (event) => {
event.respondWith(
router.handle(event.request, event).catch((e) => genErrorResponse(e))
);
});

42
src/response.js Normal file
View File

@ -0,0 +1,42 @@
/**
* custom api error
*/
export class ResError extends Error {
/**
* @param {number} status
* @param {string} message
*/
constructor(statusCode, message) {
super(message);
this.statusCode = statusCode;
this.message = message;
}
}
/**
* @param {any} e
*/
export function genErrorResponse(e) {
const headers = {
'Content-Type': 'text/plain',
'Cache-Control': 'no-cache',
};
if (e instanceof ResError) {
return new Response(e.message, { status: e.statusCode, headers });
} else {
console.error(e.name + ':', e.message);
return new Response('Internal Server Error', { status: 500, headers });
}
}
/**
* @param {string} html
*/
export function genHTMLResponse(html) {
return new Response(html, {
headers: {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'max-age=0, private, must-revalidate',
},
});
}

31
src/routes/api.js Normal file
View File

@ -0,0 +1,31 @@
/* global KV */
import { validateID } from '../utils';
export async function get(req) {
const id = validateID(req.params.id);
// get times from KV
const data = ((await KV.get(id)) || '|').split('|');
const count = Number.parseInt(data[0]) || 0;
const update = Number.parseInt(data[1]) || null;
return new Response(JSON.stringify({ count, update }), {
headers: {
'Content-Type': 'application/json; charset=utf-8',
'Cache-Control': 'no-cache',
},
});
}
export async function del(req) {
const id = validateID(req.params.id);
await KV.delete(id);
return new Response(null, {
status: 204,
headers: {
'Cache-Control': 'no-cache',
},
});
}

18
src/routes/favicon.js Normal file
View File

@ -0,0 +1,18 @@
export async function get(req, event) {
const cacheKey = new Request(new URL(req.url).toString(), req);
const cache = caches.default;
let usingCache = true;
let resCache = await cache.match(cacheKey);
if (!resCache) {
usingCache = false;
resCache = await fetch(
'https://cdn.jsdelivr.net/gh/dsrkafuu/dsr-assets@3.0.0/favicon/favicon.ico'
);
event.waitUntil(cache.put(cacheKey, resCache.clone()));
}
const res = new Response(resCache.body, resCache);
res.headers.set('X-Custom-Cache', usingCache ? 'HIT' : 'MISS');
return new Response(res.body, res);
}

49
src/routes/image.js Normal file
View File

@ -0,0 +1,49 @@
/* global KV */
import themes from '../../themes';
import { validateID, minify } from '../utils';
function genImage(count, theme, length) {
const nums = count.toString().padStart(length, '0').split('');
const { width, height, images } = themes[theme];
let x = 0; // x axis
const parts = nums.reduce((pre, cur) => {
const uri = images[cur];
const image = `<image x="${x}" y="0" width="${width}" height="${height}" xlink:href="${uri}"/>`;
x += width;
return pre + image;
}, '');
const svg = `
<?xml version="1.0" encoding="UTF-8"?>
<svg width="${x}" height="${height}" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Moe Count</title>
<g>${parts}</g>
</svg>
`;
return minify(svg);
}
export async function get(req, event) {
const id = validateID(req.params.id);
let { theme, length } = req.query;
if (!themes[theme]) {
theme = 'gelbooru';
}
if (!length || length <= 0 || length > 10) {
length = 7;
}
// get times from KV
const data = ((await KV.get(id)) || '|').split('|');
const count = (Number.parseInt(data[0]) || 0) + 1;
const image = genImage(count, theme, length);
// set time asynchronously (no await)
event.waitUntil(KV.put(id, `${count}|${Date.now()}`));
return new Response(image, {
headers: {
'Content-Type': 'image/svg+xml; charset=utf-8',
'Cache-Control': 'no-cache',
},
});
}

10
src/routes/index.js Normal file
View File

@ -0,0 +1,10 @@
import index from '../views/index.html';
export async function get() {
return new Response(index, {
headers: {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'max-age=0, private, must-revalidate',
},
});
}

15
src/utils.js Normal file
View File

@ -0,0 +1,15 @@
import { ResError } from './response';
export function minify(str) {
if (typeof str !== 'string') {
return str;
}
return str.replace(/ *\n */g, '').trim();
}
export function validateID(id) {
if (!/^[a-z][0-9a-z!@#$%^&*_-]{0,255}$/i.test(id)) {
throw new ResError(400, 'Invalid Counter ID');
}
return id;
}

49
src/views/index.html Normal file
View File

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Moe Counter CF</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/dsr-design@2.5.1/css/colors.css"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/dsr-design@2.5.1/css/reset.css"
/>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?display=swap&family=Inter:wght@400;500"
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="https://cdn.jsdelivr.net/gh/dsrkafuu/dsr-assets@3.0.0/favicon/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="https://cdn.jsdelivr.net/gh/dsrkafuu/dsr-assets@3.0.0/favicon/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="https://cdn.jsdelivr.net/gh/dsrkafuu/dsr-assets@3.0.0/favicon/favicon-16x16.png"
/>
<link
rel="shortcut icon"
href="https://cdn.jsdelivr.net/gh/dsrkafuu/dsr-assets@3.0.0/favicon/favicon.ico"
/>
</head>
<body>
<header>HEADER</header>
<main>MAIN</main>
<footer>Copyright &copy; 2022 DSRKafuU</footer>
</body>
</html>

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

98
themes/index.js Normal file
View File

@ -0,0 +1,98 @@
import as_0 from './asoul/0.gif';
import as_1 from './asoul/1.gif';
import as_2 from './asoul/2.gif';
import as_3 from './asoul/3.gif';
import as_4 from './asoul/4.gif';
import as_5 from './asoul/5.gif';
import as_6 from './asoul/6.gif';
import as_7 from './asoul/7.gif';
import as_8 from './asoul/8.gif';
import as_9 from './asoul/9.gif';
import gb_0 from './gelbooru/0.gif';
import gb_1 from './gelbooru/1.gif';
import gb_2 from './gelbooru/2.gif';
import gb_3 from './gelbooru/3.gif';
import gb_4 from './gelbooru/4.gif';
import gb_5 from './gelbooru/5.gif';
import gb_6 from './gelbooru/6.gif';
import gb_7 from './gelbooru/7.gif';
import gb_8 from './gelbooru/8.gif';
import gb_9 from './gelbooru/9.gif';
import gh_0 from './gelbooru-h/0.png';
import gh_1 from './gelbooru-h/1.png';
import gh_2 from './gelbooru-h/2.png';
import gh_3 from './gelbooru-h/3.png';
import gh_4 from './gelbooru-h/4.png';
import gh_5 from './gelbooru-h/5.png';
import gh_6 from './gelbooru-h/6.png';
import gh_7 from './gelbooru-h/7.png';
import gh_8 from './gelbooru-h/8.png';
import gh_9 from './gelbooru-h/9.png';
import mb_0 from './moebooru/0.gif';
import mb_1 from './moebooru/1.gif';
import mb_2 from './moebooru/2.gif';
import mb_3 from './moebooru/3.gif';
import mb_4 from './moebooru/4.gif';
import mb_5 from './moebooru/5.gif';
import mb_6 from './moebooru/6.gif';
import mb_7 from './moebooru/7.gif';
import mb_8 from './moebooru/8.gif';
import mb_9 from './moebooru/9.gif';
import mh_0 from './moebooru-h/0.png';
import mh_1 from './moebooru-h/1.png';
import mh_2 from './moebooru-h/2.png';
import mh_3 from './moebooru-h/3.png';
import mh_4 from './moebooru-h/4.png';
import mh_5 from './moebooru-h/5.png';
import mh_6 from './moebooru-h/6.png';
import mh_7 from './moebooru-h/7.png';
import mh_8 from './moebooru-h/8.png';
import mh_9 from './moebooru-h/9.png';
import ru_0 from './rule34/0.gif';
import ru_1 from './rule34/1.gif';
import ru_2 from './rule34/2.gif';
import ru_3 from './rule34/3.gif';
import ru_4 from './rule34/4.gif';
import ru_5 from './rule34/5.gif';
import ru_6 from './rule34/6.gif';
import ru_7 from './rule34/7.gif';
import ru_8 from './rule34/8.gif';
import ru_9 from './rule34/9.gif';
export default {
asoul: {
width: 45,
height: 100,
images: [as_0, as_1, as_2, as_3, as_4, as_5, as_6, as_7, as_8, as_9],
},
gelbooru: {
width: 68,
height: 150,
images: [gb_0, gb_1, gb_2, gb_3, gb_4, gb_5, gb_6, gb_7, gb_8, gb_9],
},
'gelbooru-h': {
width: 68,
height: 150,
images: [gh_0, gh_1, gh_2, gh_3, gh_4, gh_5, gh_6, gh_7, gh_8, gh_9],
},
moebooru: {
width: 45,
height: 100,
images: [mb_0, mb_1, mb_2, mb_3, mb_4, mb_5, mb_6, mb_7, mb_8, mb_9],
},
'moebooru-h': {
width: 45,
height: 100,
images: [mh_0, mh_1, mh_2, mh_3, mh_4, mh_5, mh_6, mh_7, mh_8, mh_9],
},
rule34: {
width: 45,
height: 100,
images: [ru_0, ru_1, ru_2, ru_3, ru_4, ru_5, ru_6, ru_7, ru_8, ru_9],
},
};

View File

Before

Width:  |  Height:  |  Size: 680 B

After

Width:  |  Height:  |  Size: 680 B

View File

Before

Width:  |  Height:  |  Size: 769 B

After

Width:  |  Height:  |  Size: 769 B

View File

Before

Width:  |  Height:  |  Size: 855 B

After

Width:  |  Height:  |  Size: 855 B

View File

Before

Width:  |  Height:  |  Size: 775 B

After

Width:  |  Height:  |  Size: 775 B

View File

Before

Width:  |  Height:  |  Size: 819 B

After

Width:  |  Height:  |  Size: 819 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 795 B

After

Width:  |  Height:  |  Size: 795 B

View File

Before

Width:  |  Height:  |  Size: 1012 B

After

Width:  |  Height:  |  Size: 1012 B

View File

Before

Width:  |  Height:  |  Size: 913 B

After

Width:  |  Height:  |  Size: 913 B

View File

Before

Width:  |  Height:  |  Size: 826 B

After

Width:  |  Height:  |  Size: 826 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 922 B

After

Width:  |  Height:  |  Size: 922 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 974 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -1,67 +0,0 @@
'use strict'
const fs = require('fs')
const path = require('path')
const mimeType = require('mime-types')
const sizeOf = require('image-size')
const themePath = path.resolve(__dirname, '../assets/theme')
const themeList = {}
fs.readdirSync(themePath).forEach(theme => {
if(!(theme in themeList)) themeList[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 { width, height } = sizeOf(imgPath)
themeList[theme][name] = {
width,
height,
data: convertToDatauri(imgPath)
}
})
})
function convertToDatauri(path){
const mime = mimeType.lookup(path)
const base64 = fs.readFileSync(path).toString('base64')
return `data:${mime};base64,${base64}`
}
function getCountImage({ count, theme='moebooru', length=7 }) {
if(!(theme in themeList)) theme = 'moebooru'
// This is not the greatest way for generating an SVG but it'll do for now
const countArray = count.toString().padStart(length, '0').split('')
let x = 0, y = 0
const parts = countArray.reduce((acc, next, index) => {
const { width, height, data } = themeList[theme][next]
const image = `${acc}
<image x="${x}" y="0" width="${width}" height="${height}" xlink:href="${data}" />`
x += width
if(height > y) y = height
return image
}, '')
return `<?xml version="1.0" encoding="UTF-8"?>
<svg width="${x}" height="${y}" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Moe Count</title>
<g>
${parts}
</g>
</svg>
`
}
module.exports = {
getCountImage
}

View File

@ -1,112 +0,0 @@
html
head
title='Moe Counter!'
meta(name='viewport', content='width=device-width, initial-scale=1')
link(rel='icon', type='image/png', href='favicon.png')
link(rel='stylesheet', href='https://cdn.jsdelivr.net/gh/kognise/water.css@latest/dist/light.min.css')
link(rel='stylesheet', href='style.css')
<!-- Global site tag (gtag.js) - Google Analytics -->
script(async, src='https://www.googletagmanager.com/gtag/js?id=G-2RLWN5JXRL')
script.
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-2RLWN5JXRL');
function _evt_push(type, category, label){
gtag('event', type, {
'event_category' : category,
'event_label' : label
});
}
body
h3 How to use:
h5 SVG address
code https://count.getloli.com/get/@:name
h5 Img tag
code &lt;img src="https://count.getloli.com/get/@:name" alt=":name" />
h5 Markdown
code ![:name](https://count.getloli.com/get/@:name)
h3 eg:
<img src="https://count.getloli.com/get/@index" alt="Moe Count!" />
i Data can access by anyone, please
| <span style="color: #ff4500;"> DO NOT</span>
| enter personal information
details
summary(style='display: inline-block;', onclick='_evt_push("click", "normal", "more_theme")')
h3(style='display: inline-block; cursor: pointer;') More theme
p(style='margin: 0;') Just use the query parameters <code>theme</code>, like this: <code>https://count.getloli.com/get/@:name?theme=moebooru</code>
h5 asoul
img(src='https://count.getloli.com/get/@demo?theme=asoul', alt='A-SOUL')
h5 moebooru
img(src='https://count.getloli.com/get/@demo?theme=moebooru', alt='Moebooru')
h5 moebooru-h
img(src='https://count.getloli.com/get/@demo?theme=moebooru-h', alt='Moebooru-Hentai')
h5 rule34
img(src='https://count.getloli.com/get/@demo?theme=rule34', alt='Rule34')
h5 gelbooru
img(src='https://count.getloli.com/get/@demo?theme=gelbooru', alt='Gelbooru')
h5 gelbooru-h
img(src='https://count.getloli.com/get/@demo?theme=gelbooru-h', alt='Gelbooru-Hentai')
h3 Credits
ul
li
a(href='https://repl.it/', target='_blank', rel='nofollow') repl.it
li
a(href='https://www.asoulworld.com/', target='_blank', title='A-SOUL导航站(非官方)') 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/icons/set/star', target='_blank', rel='nofollow') Icons8
h3 Tool
.tool
code https://count.getloli.com/get/@
input#name(type='text', placeholder=':name', style='display: inline-block; width: 80px; height: 1.4em; line-height: 1.4em; margin: 0 4px; vertical-align: middle;')
code ?theme=
select#theme(style='display: inline-block; height: 1.6em; line-height: 1.6em; font-size: 14px; margin: 0 4px; padding: 0 4px; vertical-align: middle;')
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
button#get(style='margin: 10px 0;', onclick='_evt_push("click", "normal", "get_counter")') Get
img#result(style='display: block;')
script.
var btn = document.getElementById('get'),
img = document.getElementById('result')
btn.addEventListener('click', function() {
var name = document.getElementById('name'),
themeEl = document.getElementById('theme')
var text = name.value ? name.value.trim() : ''
var theme = themeEl.value || 'moebooru'
if(!text) {
alert('Please input counter name.')
return
}
img.src = 'https://count.getloli.com/get/@' + text + '?theme=' + theme
})
iframe(src="https://chat.getloli.com/room/@Moe-counter?title=%E8%90%8C%E8%90%8C%E8%AE%A1%E6%95%B0%E5%99%A8%E7%9A%84%E7%95%99%E8%A8%80%E6%9D%BF", scrolling="no", frameborder="0", height="70%", width="26%", style="position: fixed;top: 2%;right: 5%;")
p.copy
a(href='https://github.com/journey-ad/Moe-counter', target='_blank', onclick='_evt_push("click", "normal", "go_github")') source code

28
webpack.config.js Normal file
View File

@ -0,0 +1,28 @@
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: 'worker.js',
path: path.resolve(__dirname, './dist'),
},
devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.html?$/i,
type: 'asset/source',
},
{
test: /\.(gif|png)$/i,
type: 'asset/inline',
},
],
},
optimization: {
minimize: false,
},
plugins: [new CleanWebpackPlugin()],
};

15
wrangler.example.toml Normal file
View File

@ -0,0 +1,15 @@
name = "moe-counter-cf"
type = "javascript"
account_id = "<CF_ACCOUNT_ID>"
zone_id = "<CF_ZONE_ID>"
workers_dev = false
route = "<YOUR_DOMAIN>/*"
compatibility_date = "2022-03-12"
kv_namespaces = [
{ binding = "KV", id = "<CF_KV_ID>", preview_id = "<CF_PREVIEW_KV_ID>" },
]
[build]
command = "pnpm run build"
[build.upload]
format = "service-worker"