Compare commits
104 Commits
ref/struct
...
main
Author | SHA1 | Date | |
---|---|---|---|
92c3c8eefe | |||
497ea031d8 | |||
39ca394a56 | |||
0bd1771f35 | |||
328c73c209 | |||
5ac952ec13 | |||
2cf5923b28 | |||
75973c72ee | |||
b40d24721c | |||
0a6ecc6314 | |||
b4a0320e3e | |||
8cf9395354 | |||
1e8d28e194 | |||
c0340677a1 | |||
54a2de0a11 | |||
3abd6666c0 | |||
44e13724fc | |||
dd7e2242a0 | |||
503a93a09f | |||
507f2c331e | |||
a1a4abff46 | |||
c6b7736dac | |||
fa5ab258da | |||
9dd06fa7bc | |||
bb7f846305 | |||
7f9563a2a6 | |||
d0d9c21aba | |||
44bc99dd9d | |||
2c83b79881 | |||
1a20d5afe0 | |||
ae338f88ee | |||
96903dec2b | |||
58b4e2613c | |||
2b0497c83a | |||
3bc72720d1 | |||
557a013b42 | |||
16cfae8bad | |||
cbd46d4030 | |||
6b93a781b7 | |||
fe2fd4fe36 | |||
dd70543594 | |||
1ff71ab241 | |||
cf7a285f57 | |||
79a37d927a | |||
f003e77d52 | |||
4addadb035 | |||
23917b2976 | |||
6d946f74df | |||
c5ba673069 | |||
fa5ccce83f | |||
7786d66dbb | |||
b18b45078f | |||
1633e56b1e | |||
a063f2401b | |||
44f68993a0 | |||
980dd542ee | |||
c82a95d0bc | |||
137c19d74e | |||
5fb1355346 | |||
8456bb7485 | |||
01f5e57864 | |||
bf00918c00 | |||
44f4ee5b01 | |||
cd8fe502ba | |||
95681dcbf3 | |||
59f09ca5eb | |||
d686b6a369 | |||
d8c74a609a | |||
9d5c7cc47d | |||
7528dcdf81 | |||
811a8261b3 | |||
d0aa27b2ad | |||
280825cf67 | |||
e658135a81 | |||
4632bd5906 | |||
43b52dee0b | |||
8900ac7ec7 | |||
2772849933 | |||
784939074a | |||
94dd662c40 | |||
1ebc0d0c1b | |||
728a74f4d3 | |||
0c8a459d92 | |||
d7b6792d05 | |||
50bcb48bd6 | |||
fe386d2b02 | |||
e72b3008d1 | |||
a31f702499 | |||
5a112aeaee | |||
d675187f68 | |||
10de53b773 | |||
f97e42e7d0 | |||
be0ff294be | |||
d74ff02a3f | |||
92e00d033d | |||
6d1698fcb6 | |||
7d8361589c | |||
66b89eb562 | |||
d994e67036 | |||
ee40764fdc | |||
175ec047cf | |||
358dd1ee5e | |||
d9c8253019 | |||
1a86831e90 |
96
.dockerignore
Normal file
96
.dockerignore
Normal file
@ -0,0 +1,96 @@
|
||||
built/*
|
||||
tests/cases/rwc/*
|
||||
tests/cases/perf/*
|
||||
!tests/cases/webharness/compilerToString.js
|
||||
test-args.txt
|
||||
~*.docx
|
||||
\#*\#
|
||||
.\#*
|
||||
tests/baselines/local/*
|
||||
tests/baselines/local.old/*
|
||||
tests/services/baselines/local/*
|
||||
tests/baselines/prototyping/local/*
|
||||
tests/baselines/rwc/*
|
||||
tests/baselines/reference/projectOutput/*
|
||||
tests/baselines/local/projectOutput/*
|
||||
tests/baselines/reference/testresults.tap
|
||||
tests/baselines/symlinks/*
|
||||
tests/services/baselines/prototyping/local/*
|
||||
tests/services/browser/typescriptServices.js
|
||||
src/harness/*.js
|
||||
src/compiler/diagnosticInformationMap.generated.ts
|
||||
src/compiler/diagnosticMessages.generated.json
|
||||
src/parser/diagnosticInformationMap.generated.ts
|
||||
src/parser/diagnosticMessages.generated.json
|
||||
rwc-report.html
|
||||
*.swp
|
||||
build.json
|
||||
*.actual
|
||||
tests/webTestServer.js
|
||||
tests/webTestServer.js.map
|
||||
tests/webhost/*.d.ts
|
||||
tests/webhost/webtsc.js
|
||||
tests/cases/**/*.js
|
||||
tests/cases/**/*.js.map
|
||||
*.config
|
||||
scripts/eslint/built/
|
||||
scripts/debug.bat
|
||||
scripts/run.bat
|
||||
scripts/**/*.js
|
||||
scripts/**/*.js.map
|
||||
coverage/
|
||||
internal/
|
||||
**/.DS_Store
|
||||
.settings
|
||||
**/.vs
|
||||
**/.vscode/*
|
||||
!**/.vscode/tasks.json
|
||||
!**/.vscode/settings.template.json
|
||||
!**/.vscode/launch.template.json
|
||||
!**/.vscode/extensions.json
|
||||
!tests/cases/projects/projectOption/**/node_modules
|
||||
!tests/cases/projects/NodeModulesSearch/**/*
|
||||
!tests/baselines/reference/project/nodeModules*/**/*
|
||||
yarn.lock
|
||||
yarn-error.log
|
||||
.parallelperf.*
|
||||
tests/baselines/reference/dt
|
||||
.failed-tests
|
||||
TEST-results.xml
|
||||
package-lock.json
|
||||
.eslintcache
|
||||
*v8.log
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# npm dependencies
|
||||
node_modules/
|
||||
|
||||
# project specific
|
||||
logs/
|
||||
__pycache__
|
||||
ml/filter/runs
|
||||
ml/pred/runs
|
||||
ml/pred/checkpoints
|
||||
ml/pred/observed
|
||||
ml/data/
|
||||
ml/filter/checkpoints
|
||||
scripts
|
||||
model/
|
||||
|
||||
|
||||
.astro
|
||||
|
||||
# Database
|
||||
*.dump
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
data/
|
||||
|
||||
docker-compose.yml
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.woff2 filter=lfs diff=lfs merge=lfs -text
|
83
.gitignore
vendored
83
.gitignore
vendored
@ -1,79 +1,18 @@
|
||||
built/*
|
||||
tests/cases/rwc/*
|
||||
tests/cases/perf/*
|
||||
!tests/cases/webharness/compilerToString.js
|
||||
test-args.txt
|
||||
~*.docx
|
||||
\#*\#
|
||||
.\#*
|
||||
tests/baselines/local/*
|
||||
tests/baselines/local.old/*
|
||||
tests/services/baselines/local/*
|
||||
tests/baselines/prototyping/local/*
|
||||
tests/baselines/rwc/*
|
||||
tests/baselines/reference/projectOutput/*
|
||||
tests/baselines/local/projectOutput/*
|
||||
tests/baselines/reference/testresults.tap
|
||||
tests/baselines/symlinks/*
|
||||
tests/services/baselines/prototyping/local/*
|
||||
tests/services/browser/typescriptServices.js
|
||||
src/harness/*.js
|
||||
src/compiler/diagnosticInformationMap.generated.ts
|
||||
src/compiler/diagnosticMessages.generated.json
|
||||
src/parser/diagnosticInformationMap.generated.ts
|
||||
src/parser/diagnosticMessages.generated.json
|
||||
rwc-report.html
|
||||
*.swp
|
||||
build.json
|
||||
*.actual
|
||||
tests/webTestServer.js
|
||||
tests/webTestServer.js.map
|
||||
tests/webhost/*.d.ts
|
||||
tests/webhost/webtsc.js
|
||||
tests/cases/**/*.js
|
||||
tests/cases/**/*.js.map
|
||||
*.config
|
||||
scripts/eslint/built/
|
||||
scripts/debug.bat
|
||||
scripts/run.bat
|
||||
scripts/**/*.js
|
||||
scripts/**/*.js.map
|
||||
coverage/
|
||||
internal/
|
||||
**/.DS_Store
|
||||
.settings
|
||||
**/.vs
|
||||
**/.vscode/*
|
||||
!**/.vscode/tasks.json
|
||||
!**/.vscode/settings.template.json
|
||||
!**/.vscode/launch.template.json
|
||||
!**/.vscode/extensions.json
|
||||
!tests/cases/projects/projectOption/**/node_modules
|
||||
!tests/cases/projects/NodeModulesSearch/**/*
|
||||
!tests/baselines/reference/project/nodeModules*/**/*
|
||||
yarn.lock
|
||||
yarn-error.log
|
||||
.parallelperf.*
|
||||
tests/baselines/reference/dt
|
||||
.failed-tests
|
||||
TEST-results.xml
|
||||
package-lock.json
|
||||
|
||||
.eslintcache
|
||||
*v8.log
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
.env.*
|
||||
|
||||
# Fresh build directory
|
||||
_fresh/
|
||||
# npm dependencies
|
||||
node_modules/
|
||||
|
||||
|
||||
# project specific
|
||||
logs/
|
||||
__pycache__
|
||||
@ -85,3 +24,21 @@ ml/data/
|
||||
ml/filter/checkpoints
|
||||
scripts
|
||||
model/
|
||||
|
||||
.astro
|
||||
|
||||
# Database
|
||||
*.dump
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
data/
|
||||
redis/
|
||||
|
||||
# Build
|
||||
dist/
|
||||
build/
|
||||
|
||||
docker-compose.yml
|
||||
|
||||
ucaptcha-config.yaml
|
3
.idea/.gitignore
vendored
3
.idea/.gitignore
vendored
@ -6,4 +6,5 @@
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
dataSources.xml
|
||||
dataSources.xml
|
||||
MarsCodeWorkspaceAppSettings.xml
|
6
.idea/bun.xml
Normal file
6
.idea/bun.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="BunSettings">
|
||||
<option name="bunPath" value="$USER_HOME$/.bun/bin/bun" />
|
||||
</component>
|
||||
</project>
|
55
.idea/codeStyles/Project.xml
Normal file
55
.idea/codeStyles/Project.xml
Normal file
@ -0,0 +1,55 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<option name="LINE_SEPARATOR" value=" " />
|
||||
<HTMLCodeStyleSettings>
|
||||
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
||||
</HTMLCodeStyleSettings>
|
||||
<JSCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</JSCodeStyleSettings>
|
||||
<TypeScriptCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</TypeScriptCodeStyleSettings>
|
||||
<VueCodeStyleSettings>
|
||||
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
|
||||
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
|
||||
</VueCodeStyleSettings>
|
||||
<codeStyleSettings language="HTML">
|
||||
<option name="SOFT_MARGINS" value="120" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JavaScript">
|
||||
<option name="SOFT_MARGINS" value="120" />
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="TypeScript">
|
||||
<option name="SOFT_MARGINS" value="120" />
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="Vue">
|
||||
<option name="SOFT_MARGINS" value="120" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="4" />
|
||||
<option name="TAB_SIZE" value="4" />
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
@ -19,6 +19,17 @@
|
||||
<excludeFolder url="file://$MODULE_DIR$/.zed" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/frontend/.astro" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/scripts" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.astro" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ml/pred/checkpoints" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ml/pred/observed" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ml/pred/runs" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/backend/logs" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/core/net/logs" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/crawler/logs" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/data" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/redis" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ml" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/src" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
|
@ -2,5 +2,6 @@
|
||||
<project version="4">
|
||||
<component name="DenoSettings">
|
||||
<option name="denoInit" value="{ "enable": true, "lint": true, "unstable": true, "importMap": "import_map.json", "config": "deno.json", "fmt": { "useTabs": true, "lineWidth": 120, "indentWidth": 4, "semiColons": true, "proseWrap": "always" } }" />
|
||||
<option name="useDenoValue" value="DISABLE" />
|
||||
</component>
|
||||
</project>
|
@ -15,11 +15,22 @@
|
||||
<scope name="Astro" level="INFORMATION" enabled="false" editorAttributes="INFORMATION_ATTRIBUTES" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GrazieInspection" enabled="false" level="GRAMMAR_ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="HtmlUnknownAttribute" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="myValues">
|
||||
<value>
|
||||
<list size="1">
|
||||
<item index="0" class="java.lang.String" itemvalue="autocorrect" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
<option name="myCustomValuesEnabled" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="LanguageDetectionInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
|
||||
<option name="processCode" value="true" />
|
||||
<option name="processLiterals" value="true" />
|
||||
<option name="processComments" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="TsLint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
8
.prettierrc
Normal file
8
.prettierrc
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "none",
|
||||
"singleQuote": false,
|
||||
"printWidth": 120,
|
||||
"endOfLine": "lf"
|
||||
}
|
@ -3,4 +3,9 @@ data
|
||||
*.svg
|
||||
*.txt
|
||||
*.md
|
||||
*config*
|
||||
*config*
|
||||
Inter.css
|
||||
MiSans.css
|
||||
*.yaml
|
||||
*.yml
|
||||
*.mdx
|
||||
|
6
.vscode/extensions.json
vendored
6
.vscode/extensions.json
vendored
@ -1,6 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"denoland.vscode-deno",
|
||||
"bradlc.vscode-tailwindcss"
|
||||
]
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
// Folder-specific settings
|
||||
//
|
||||
// For a full list of overridable settings, and general information on folder-specific settings,
|
||||
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
|
||||
{
|
||||
"lsp": {
|
||||
"deno": {
|
||||
"settings": {
|
||||
"deno": {
|
||||
"enable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"languages": {
|
||||
"TypeScript": {
|
||||
"language_servers": [
|
||||
"deno",
|
||||
"!typescript-language-server",
|
||||
"!vtsls",
|
||||
"!eslint"
|
||||
],
|
||||
"formatter": "language_server"
|
||||
},
|
||||
"TSX": {
|
||||
"language_servers": [
|
||||
"deno",
|
||||
"!typescript-language-server",
|
||||
"!vtsls",
|
||||
"!eslint"
|
||||
],
|
||||
"formatter": "language_server"
|
||||
}
|
||||
}
|
||||
}
|
23
Dockerfile.backend
Normal file
23
Dockerfile.backend
Normal file
@ -0,0 +1,23 @@
|
||||
FROM oven/bun:1.2.8-debian
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./packages/core ./core
|
||||
|
||||
COPY ./packages/backend/package.json ./packages/backend/bun.lock ./backend/
|
||||
|
||||
RUN apt update && apt install -y curl
|
||||
|
||||
RUN ln -s /bin/uname /usr/bin/uname
|
||||
|
||||
RUN /bin/bash -c "$(curl -fsSL https://aliyuncli.alicdn.com/install.sh)"
|
||||
|
||||
WORKDIR backend
|
||||
|
||||
RUN bun install
|
||||
|
||||
COPY ./packages/backend/ .
|
||||
|
||||
RUN mkdir -p /app/logs
|
||||
|
||||
CMD ["bun", "start"]
|
19
Dockerfile.crawler
Normal file
19
Dockerfile.crawler
Normal file
@ -0,0 +1,19 @@
|
||||
FROM oven/bun:1.2.8-debian
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN bun i
|
||||
|
||||
RUN mkdir -p /app/logs
|
||||
|
||||
RUN apt update && apt install -y curl
|
||||
|
||||
RUN ln -s /bin/uname /usr/bin/uname
|
||||
|
||||
RUN /bin/bash -c "$(curl -fsSL https://aliyuncli.alicdn.com/install.sh)"
|
||||
|
||||
WORKDIR packages/crawler
|
||||
|
||||
CMD ["bun", "all"]
|
23
Dockerfile.frontend
Normal file
23
Dockerfile.frontend
Normal file
@ -0,0 +1,23 @@
|
||||
FROM oven/bun
|
||||
|
||||
ARG BACKEND_URL
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN bun install
|
||||
|
||||
WORKDIR packages/frontend
|
||||
|
||||
RUN bun run build
|
||||
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=4321
|
||||
ENV BACKEND_URL=${BACKEND_URL}
|
||||
|
||||
EXPOSE 4321
|
||||
|
||||
RUN mkdir -p /app/logs
|
||||
|
||||
CMD ["bun", "/app/packages/frontend/dist/server/entry.mjs"]
|
14
Dockerfile.next
Normal file
14
Dockerfile.next
Normal file
@ -0,0 +1,14 @@
|
||||
FROM node:lts-slim AS production
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./packages/next/.next ./.next
|
||||
COPY ./packages/next/public ./public
|
||||
COPY ./packages/next/package.json ./package.json
|
||||
COPY ./packages/next/node_modules ./node_modules
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
||||
EXPOSE 7400
|
||||
|
||||
CMD ["npm", "start"]
|
21
deno.json
21
deno.json
@ -1,21 +0,0 @@
|
||||
{
|
||||
"lock": false,
|
||||
"workspace": ["./packages/crawler", "./packages/frontend", "./packages/backend", "./packages/core"],
|
||||
"nodeModulesDir": "auto",
|
||||
"tasks": {
|
||||
"crawler": "deno task --filter 'crawler' all",
|
||||
"backend": "deno task --filter 'backend' start"
|
||||
},
|
||||
"fmt": {
|
||||
"useTabs": true,
|
||||
"lineWidth": 120,
|
||||
"indentWidth": 4,
|
||||
"semiColons": true,
|
||||
"proseWrap": "always"
|
||||
},
|
||||
"imports": {
|
||||
"@astrojs/node": "npm:@astrojs/node@^9.1.3",
|
||||
"@astrojs/svelte": "npm:@astrojs/svelte@^7.0.8",
|
||||
"date-fns": "npm:date-fns@^4.1.0"
|
||||
}
|
||||
}
|
71
docker-compose.example.yml
Normal file
71
docker-compose.example.yml
Normal file
@ -0,0 +1,71 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:17
|
||||
ports:
|
||||
- "5431:5432"
|
||||
environment:
|
||||
POSTGRES_USER: cvsa
|
||||
POSTGRES_PASSWORD: ""
|
||||
POSTGRES_DB: cvsa_main
|
||||
volumes:
|
||||
- ./data:/var/lib/postgresql/data
|
||||
|
||||
redis:
|
||||
image: redis:latest
|
||||
ports:
|
||||
- "6378:6379"
|
||||
volumes:
|
||||
- ./redis/data:/data
|
||||
- ./redis/redis.conf:/usr/local/etc/redis/redis.conf
|
||||
- ./redis/logs:/logs
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.frontend
|
||||
ports:
|
||||
- "4321:4321"
|
||||
environment:
|
||||
- HOST=0.0.0.0
|
||||
- PORT=4321
|
||||
- DB_HOST=db
|
||||
- DB_NAME=cvsa_main
|
||||
- DB_NAME_CRED=cvsa_cred
|
||||
- DB_USER=cvsa
|
||||
- DB_PORT=5432
|
||||
- DB_PASSWORD=""
|
||||
- LOG_VERBOSE=/app/logs/verbose.log
|
||||
- LOG_WARN=/app/logs/warn.log
|
||||
- LOG_ERR=/app/logs/error.log
|
||||
depends_on:
|
||||
- db
|
||||
volumes:
|
||||
- /path/to/your/logs:/app/logs
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.backend
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
- HOST=0.0.0.0
|
||||
- DB_HOST=db
|
||||
- DB_NAME=cvsa_main
|
||||
- DB_NAME_CRED=cvsa_cred
|
||||
- DB_USER=cvsa
|
||||
- DB_PORT=5432
|
||||
- DB_PASSWORD=""
|
||||
- LOG_VERBOSE=/app/logs/verbose.log
|
||||
- LOG_WARN=/app/logs/warn.log
|
||||
- LOG_ERR=/app/logs/error.log
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
depends_on:
|
||||
- db
|
||||
volumes:
|
||||
- /path/to/your/logs:/app/logs
|
||||
|
||||
volumes:
|
||||
db_data:
|
23
package.json
Normal file
23
package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "cvsa",
|
||||
"version": "3.15.34",
|
||||
"private": false,
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
"packages/frontend",
|
||||
"packages/core",
|
||||
"packages/backend",
|
||||
"packages/crawler"
|
||||
],
|
||||
"dependencies": {
|
||||
"arg": "^5.0.2",
|
||||
"postgres": "^3.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.15",
|
||||
"prettier": "^3.5.3",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.1.2",
|
||||
"vitest-tsconfig-paths": "^3.4.1"
|
||||
}
|
||||
}
|
8
packages/backend/.prettierrc
Normal file
8
packages/backend/.prettierrc
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "none",
|
||||
"singleQuote": false,
|
||||
"printWidth": 120,
|
||||
"endOfLine": "lf"
|
||||
}
|
208
packages/backend/bun.lock
Normal file
208
packages/backend/bun.lock
Normal file
@ -0,0 +1,208 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@rabbit-company/argon2id": "^2.1.0",
|
||||
"hono": "^4.7.8",
|
||||
"hono-rate-limiter": "^0.4.2",
|
||||
"ioredis": "^5.6.1",
|
||||
"postgres": "^3.4.5",
|
||||
"rate-limit-redis": "^4.2.0",
|
||||
"yup": "^1.6.1",
|
||||
"zod": "^3.24.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.11",
|
||||
"prettier": "^3.5.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@ioredis/commands": ["@ioredis/commands@1.2.0", "", {}, "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="],
|
||||
|
||||
"@rabbit-company/argon2id": ["@rabbit-company/argon2id@2.1.0", "", { "peerDependencies": { "typescript": "^5.6.2" } }, "sha512-X/kt89qjmS9+Zh+DYCGcWeTwHa4C8vY8T3EnSma+vWj7spMzAYX4F8vmGUkny9hygpTOeC/yXwAUdJAfJ52H+w=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.11", "", { "dependencies": { "bun-types": "1.2.11" } }, "sha512-ZLbbI91EmmGwlWTRWuV6J19IUiUC5YQ3TCEuSHI3usIP75kuoA8/0PVF+LTrbEnVc8JIhpElWOxv1ocI1fJBbw=="],
|
||||
|
||||
"@types/node": ["@types/node@22.15.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw=="],
|
||||
|
||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
|
||||
"body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-dbkp5Lo8HDrXkLrONm6bk+yiiYQSntvFUzQp0v3pzTAsXk6FtgVMjdQ+lzFNVAmQFUkPQZ3WMZqH5tTo+Dp/IA=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
|
||||
|
||||
"content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="],
|
||||
|
||||
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||
|
||||
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||
|
||||
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
||||
|
||||
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
|
||||
|
||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
||||
"express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
|
||||
|
||||
"express-rate-limit": ["express-rate-limit@7.5.0", "", { "peerDependencies": { "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg=="],
|
||||
|
||||
"finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="],
|
||||
|
||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
|
||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hono": ["hono@4.7.8", "", {}, "sha512-PCibtFdxa7/Ldud9yddl1G81GjYaeMYYTq4ywSaNsYbB1Lug4mwtOMJf2WXykL0pntYwmpRJeOI3NmoDgD+Jxw=="],
|
||||
|
||||
"hono-rate-limiter": ["hono-rate-limiter@0.4.2", "", { "peerDependencies": { "hono": "^4.1.1" } }, "sha512-AAtFqgADyrmbDijcRTT/HJfwqfvhalya2Zo+MgfdrMPas3zSMD8SU03cv+ZsYwRU1swv7zgVt0shwN059yzhjw=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ioredis": ["ioredis@5.6.1", "", { "dependencies": { "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||
|
||||
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
|
||||
|
||||
"lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||
|
||||
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="],
|
||||
|
||||
"postgres": ["postgres@3.4.5", "", {}, "sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg=="],
|
||||
|
||||
"prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="],
|
||||
|
||||
"property-expr": ["property-expr@2.0.6", "", {}, "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA=="],
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
"rate-limit-redis": ["rate-limit-redis@4.2.0", "", { "peerDependencies": { "express-rate-limit": ">= 6" } }, "sha512-wV450NQyKC24NmPosJb2131RoczLdfIJdKCReNwtVpm5998U8SgKrAZrIHaN/NfQgqOHaan8Uq++B4sa5REwjA=="],
|
||||
|
||||
"raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="],
|
||||
|
||||
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
|
||||
|
||||
"redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="],
|
||||
|
||||
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="],
|
||||
|
||||
"serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="],
|
||||
|
||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||
|
||||
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||
|
||||
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||
|
||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
|
||||
|
||||
"statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
|
||||
|
||||
"tiny-case": ["tiny-case@1.0.3", "", {}, "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="],
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"toposort": ["toposort@2.0.2", "", {}, "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="],
|
||||
|
||||
"type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="],
|
||||
|
||||
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"yup": ["yup@1.6.1", "", { "dependencies": { "property-expr": "^2.0.5", "tiny-case": "^1.0.3", "toposort": "^2.0.2", "type-fest": "^2.19.0" } }, "sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA=="],
|
||||
|
||||
"zod": ["zod@3.24.3", "", {}, "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg=="],
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
import { type Client, Pool } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
|
||||
import { postgresConfig, postgresConfigCred } from "@core/db/pgConfig.ts";
|
||||
import { createMiddleware } from "hono/factory";
|
||||
|
||||
const pool = new Pool(postgresConfig, 4);
|
||||
const poolCred = new Pool(postgresConfigCred, 2);
|
||||
|
||||
export const db = pool;
|
||||
export const dbCred = poolCred;
|
||||
|
||||
export const dbMiddleware = createMiddleware(async (c, next) => {
|
||||
const connection = await pool.connect();
|
||||
c.set("db", connection);
|
||||
await next();
|
||||
connection.release();
|
||||
});
|
||||
|
||||
export const dbCredMiddleware = createMiddleware(async (c, next) => {
|
||||
const connection = await poolCred.connect();
|
||||
c.set("dbCred", connection);
|
||||
await next();
|
||||
connection.release();
|
||||
});
|
||||
|
||||
declare module "hono" {
|
||||
interface ContextVariableMap {
|
||||
db: Client;
|
||||
dbCred: Client;
|
||||
}
|
||||
}
|
13
packages/backend/db/latestSnapshots.ts
Normal file
13
packages/backend/db/latestSnapshots.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { sql } from "@core/db/dbNew";
|
||||
import type { LatestSnapshotType } from "@core/db/schema.d.ts";
|
||||
|
||||
export async function getVideosInViewsRange(minViews: number, maxViews: number) {
|
||||
return sql<LatestSnapshotType[]>`
|
||||
SELECT *
|
||||
FROM latest_video_snapshot
|
||||
WHERE views >= ${minViews}
|
||||
AND views <= ${maxViews}
|
||||
ORDER BY views DESC
|
||||
LIMIT 5000
|
||||
`;
|
||||
}
|
58
packages/backend/db/snapshots.ts
Normal file
58
packages/backend/db/snapshots.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { sql } from "@core/db/dbNew";
|
||||
import type { VideoSnapshotType } from "@core/db/schema.d.ts";
|
||||
|
||||
export async function getVideoSnapshots(
|
||||
aid: number,
|
||||
limit: number,
|
||||
pageOrOffset: number,
|
||||
reverse: boolean,
|
||||
mode: "page" | "offset" = "page"
|
||||
) {
|
||||
const offset = mode === "page" ? (pageOrOffset - 1) * limit : pageOrOffset;
|
||||
if (reverse) {
|
||||
return sql<VideoSnapshotType[]>`
|
||||
SELECT *
|
||||
FROM video_snapshot
|
||||
WHERE aid = ${aid}
|
||||
ORDER BY created_at
|
||||
LIMIT ${limit} OFFSET ${offset}
|
||||
`;
|
||||
} else {
|
||||
return sql<VideoSnapshotType[]>`
|
||||
SELECT *
|
||||
FROM video_snapshot
|
||||
WHERE aid = ${aid}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ${limit} OFFSET ${offset}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getVideoSnapshotsByBV(
|
||||
bv: string,
|
||||
limit: number,
|
||||
pageOrOffset: number,
|
||||
reverse: boolean,
|
||||
mode: "page" | "offset" = "page"
|
||||
) {
|
||||
const offset = mode === "page" ? (pageOrOffset - 1) * limit : pageOrOffset;
|
||||
if (reverse) {
|
||||
return sql<VideoSnapshotType[]>`
|
||||
SELECT vs.*
|
||||
FROM video_snapshot vs
|
||||
JOIN bilibili_metadata bm ON vs.aid = bm.aid
|
||||
WHERE bm.bvid = ${bv}
|
||||
ORDER BY vs.created_at
|
||||
LIMIT ${limit} OFFSET ${offset}
|
||||
`;
|
||||
} else {
|
||||
return sql<VideoSnapshotType[]>`
|
||||
SELECT vs.*
|
||||
FROM video_snapshot vs
|
||||
JOIN bilibili_metadata bm ON vs.aid = bm.aid
|
||||
WHERE bm.bvid = ${bv}
|
||||
ORDER BY vs.created_at DESC
|
||||
LIMIT ${limit} OFFSET ${offset}
|
||||
`;
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
{
|
||||
"name": "@cvsa/backend",
|
||||
"imports": {
|
||||
"@rabbit-company/argon2id": "jsr:@rabbit-company/argon2id@^2.1.0",
|
||||
"hono": "jsr:@hono/hono@^4.7.5",
|
||||
"zod": "npm:zod",
|
||||
"yup": "npm:yup",
|
||||
"@core/": "../core/",
|
||||
"log/": "../core/log/",
|
||||
"@crawler/net/videoInfo": "../crawler/net/getVideoInfo.ts",
|
||||
"ioredis": "npm:ioredis"
|
||||
},
|
||||
"tasks": {
|
||||
"dev": "deno serve --env-file=.env --allow-env --allow-net --allow-read --allow-write --allow-run --watch main.ts",
|
||||
"start": "deno serve --env-file=.env --allow-env --allow-net --allow-read --allow-write --allow-run --host 127.0.0.1 main.ts"
|
||||
},
|
||||
"compilerOptions": {
|
||||
"jsx": "precompile",
|
||||
"jsxImportSource": "hono/jsx"
|
||||
},
|
||||
"exports": "./main.ts"
|
||||
}
|
62
packages/backend/lib/auth/captchaDifficulty.ts
Normal file
62
packages/backend/lib/auth/captchaDifficulty.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { Psql } from "@core/db/psql";
|
||||
import { SlidingWindow } from "@core/mq/slidingWindow.ts";
|
||||
import { redis } from "@core/db/redis.ts";
|
||||
import { getIdentifier } from "@/middleware/rateLimiters.ts";
|
||||
import { Context } from "hono";
|
||||
|
||||
type seconds = number;
|
||||
|
||||
export interface CaptchaDifficultyConfig {
|
||||
global: boolean;
|
||||
duration: seconds;
|
||||
threshold: number;
|
||||
difficulty: number;
|
||||
}
|
||||
|
||||
export const getCaptchaDifficultyConfigByRoute = async (sql: Psql, route: string): Promise<CaptchaDifficultyConfig[]> => {
|
||||
return sql<CaptchaDifficultyConfig[]>`
|
||||
SELECT duration, threshold, difficulty, global
|
||||
FROM captcha_difficulty_settings
|
||||
WHERE CONCAT(method, '-', path) = ${route}
|
||||
ORDER BY duration
|
||||
`;
|
||||
};
|
||||
|
||||
export const getCaptchaConfigMaxDuration = async (sql: Psql, route: string): Promise<seconds> => {
|
||||
const rows = await sql<{max: number}[]>`
|
||||
SELECT MAX(duration)
|
||||
FROM captcha_difficulty_settings
|
||||
WHERE CONCAT(method, '-', path) = ${route}
|
||||
`;
|
||||
if (rows.length < 1){
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
return rows[0].max;
|
||||
}
|
||||
|
||||
|
||||
export const getCurrentCaptchaDifficulty = async (sql: Psql, c: Context | string): Promise<number | null> => {
|
||||
const isRoute = typeof c === "string";
|
||||
const route = isRoute ? c : `${c.req.method}-${c.req.path}`
|
||||
const configs = await getCaptchaDifficultyConfigByRoute(sql, route);
|
||||
if (configs.length < 1) {
|
||||
return null
|
||||
}
|
||||
else if (configs.length == 1) {
|
||||
return configs[0].difficulty
|
||||
}
|
||||
const maxDuration = configs.reduce((max, config) =>
|
||||
Math.max(max, config.duration), 0);
|
||||
const slidingWindow = new SlidingWindow(redis, maxDuration);
|
||||
for (let i = 1; i < configs.length; i++) {
|
||||
const config = configs[i];
|
||||
const lastConfig = configs[i - 1];
|
||||
const identifier = isRoute ? c : getIdentifier(c, config.global);
|
||||
const count = await slidingWindow.count(`captcha-${identifier}`, config.duration);
|
||||
if (count >= config.threshold) {
|
||||
continue;
|
||||
}
|
||||
return lastConfig.difficulty
|
||||
}
|
||||
return configs[configs.length-1].difficulty;
|
||||
}
|
14
packages/backend/lib/auth/getJWTsecret.ts
Normal file
14
packages/backend/lib/auth/getJWTsecret.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { ErrorResponse } from "src/schema";
|
||||
|
||||
export const getJWTsecret = () => {
|
||||
const secret = process.env["JWT_SECRET"];
|
||||
if (!secret) {
|
||||
const response: ErrorResponse = {
|
||||
message: "JWT_SECRET is not set",
|
||||
code: "SERVER_ERROR",
|
||||
errors: []
|
||||
};
|
||||
return [response, true];
|
||||
}
|
||||
return [secret, null];
|
||||
};
|
103
packages/backend/lib/const/singers.ts
Normal file
103
packages/backend/lib/const/singers.ts
Normal file
@ -0,0 +1,103 @@
|
||||
export const singers = [
|
||||
{
|
||||
name: "洛天依",
|
||||
color: "#66CCFF",
|
||||
birthday: "0712"
|
||||
},
|
||||
{
|
||||
name: "言和",
|
||||
color: "#00FFCC",
|
||||
birthday: "0711"
|
||||
},
|
||||
{
|
||||
name: "乐正绫",
|
||||
color: "#EE0000",
|
||||
birthday: "0412"
|
||||
},
|
||||
{
|
||||
name: "乐正龙牙",
|
||||
color: "#006666",
|
||||
birthday: "1002"
|
||||
},
|
||||
{
|
||||
name: "徵羽摩柯",
|
||||
color: "#0080FF",
|
||||
birthday: "1210"
|
||||
},
|
||||
{
|
||||
name: "墨清弦",
|
||||
color: "#FFFF00",
|
||||
birthday: "0520"
|
||||
},
|
||||
{
|
||||
name: "星尘",
|
||||
color: "#9999FF",
|
||||
birthday: "0812"
|
||||
},
|
||||
{
|
||||
name: "心华",
|
||||
color: "#EE82EE",
|
||||
birthday: "0210"
|
||||
},
|
||||
{
|
||||
name: "海伊",
|
||||
color: "#3399FF",
|
||||
birthday: "0722"
|
||||
},
|
||||
{
|
||||
name: "苍穹",
|
||||
color: "#8BC0B5",
|
||||
birthday: "0520"
|
||||
},
|
||||
{
|
||||
name: "赤羽",
|
||||
color: "#FF4004",
|
||||
birthday: "1126"
|
||||
},
|
||||
{
|
||||
name: "诗岸",
|
||||
color: "#F6BE72",
|
||||
birthday: "0119"
|
||||
},
|
||||
{
|
||||
name: "牧心",
|
||||
color: "#2A2859",
|
||||
birthday: "0807"
|
||||
}
|
||||
];
|
||||
|
||||
export interface Singer {
|
||||
name: string;
|
||||
color?: string;
|
||||
birthday?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export const specialSingers = [
|
||||
{
|
||||
name: "雅音宫羽",
|
||||
message: "你是我最真模样,从来不曾遗忘。"
|
||||
},
|
||||
{
|
||||
name: "初音未来",
|
||||
message: "初始之音,响彻未来!"
|
||||
}
|
||||
];
|
||||
|
||||
export const pickSinger = () => {
|
||||
const index = Math.floor(Math.random() * singers.length);
|
||||
return singers[index];
|
||||
};
|
||||
|
||||
export const pickSpecialSinger = () => {
|
||||
const index = Math.floor(Math.random() * specialSingers.length);
|
||||
return specialSingers[index];
|
||||
};
|
||||
|
||||
export const getSingerForBirthday = (): Singer[] => {
|
||||
const today = new Date();
|
||||
const month = String(today.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(today.getDate()).padStart(2, "0");
|
||||
const datestring = `${month}${day}`;
|
||||
return singers.filter((singer) => singer.birthday === datestring);
|
||||
};
|
@ -1,26 +0,0 @@
|
||||
import { Hono } from "hono";
|
||||
import { dbCredMiddleware, dbMiddleware } from "./database.ts";
|
||||
import { rootHandler } from "./root.ts";
|
||||
import { getSnapshotsHanlder } from "./snapshots.ts";
|
||||
import { registerHandler } from "./register.ts";
|
||||
import { videoInfoHandler } from "./videoInfo.ts";
|
||||
|
||||
export const app = new Hono();
|
||||
|
||||
app.use("/video/*", dbMiddleware);
|
||||
app.use("/user", dbCredMiddleware);
|
||||
|
||||
app.get("/", ...rootHandler);
|
||||
|
||||
app.get("/video/:id/snapshots", ...getSnapshotsHanlder);
|
||||
app.post("/user", ...registerHandler);
|
||||
|
||||
app.get("/video/:id/info", ...videoInfoHandler);
|
||||
|
||||
const fetch = app.fetch;
|
||||
|
||||
export default {
|
||||
fetch,
|
||||
} satisfies Deno.ServeDefaultExport;
|
||||
|
||||
export const VERSION = "0.4.2";
|
14
packages/backend/middleware/bodyLimits.ts
Normal file
14
packages/backend/middleware/bodyLimits.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { bodyLimit } from "hono/body-limit";
|
||||
import { ErrorResponse } from "../src/schema";
|
||||
|
||||
export const bodyLimitForPing = bodyLimit({
|
||||
maxSize: 14000,
|
||||
onError: (c) => {
|
||||
const res: ErrorResponse<string> = {
|
||||
message: "Body too large",
|
||||
errors: ["Body should not be larger than 14kB."],
|
||||
code: "BODY_TOO_LARGE"
|
||||
};
|
||||
return c.json(res, 413);
|
||||
}
|
||||
});
|
120
packages/backend/middleware/captcha.ts
Normal file
120
packages/backend/middleware/captcha.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import { Context, Next } from "hono";
|
||||
import { ErrorResponse } from "src/schema";
|
||||
import { SlidingWindow } from "@core/mq/slidingWindow.ts";
|
||||
import { getCaptchaConfigMaxDuration, getCurrentCaptchaDifficulty } from "@/lib/auth/captchaDifficulty.ts";
|
||||
import { sqlCred } from "@core/db/dbNew.ts";
|
||||
import { redis } from "@core/db/redis.ts";
|
||||
import { verify } from "hono/jwt";
|
||||
import { JwtTokenInvalid, JwtTokenExpired } from "hono/utils/jwt/types";
|
||||
import { getJWTsecret } from "@/lib/auth/getJWTsecret.ts";
|
||||
import { lockManager } from "@core/mq/lockManager.ts";
|
||||
import { object, string, number, ValidationError } from "yup";
|
||||
import { getIdentifier } from "@/middleware/rateLimiters.ts";
|
||||
|
||||
const tokenSchema = object({
|
||||
exp: number().integer(),
|
||||
id: string().length(6),
|
||||
difficulty: number().integer().moreThan(0)
|
||||
});
|
||||
|
||||
export const captchaMiddleware = async (c: Context, next: Next) => {
|
||||
const authHeader = c.req.header("Authorization");
|
||||
|
||||
if (!authHeader) {
|
||||
const response: ErrorResponse = {
|
||||
message: "'Authorization' header is missing.",
|
||||
code: "UNAUTHORIZED",
|
||||
errors: []
|
||||
};
|
||||
return c.json<ErrorResponse>(response, 401);
|
||||
}
|
||||
|
||||
const authIsBearer = authHeader.startsWith("Bearer ");
|
||||
if (!authIsBearer || authHeader.length < 8) {
|
||||
const response: ErrorResponse = {
|
||||
message: "'Authorization' header is invalid.",
|
||||
code: "INVALID_HEADER",
|
||||
errors: []
|
||||
};
|
||||
return c.json<ErrorResponse>(response, 400);
|
||||
}
|
||||
|
||||
const [r, err] = getJWTsecret();
|
||||
if (err) {
|
||||
return c.json<ErrorResponse>(r as ErrorResponse, 500);
|
||||
}
|
||||
const jwtSecret = r as string;
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
|
||||
const path = c.req.path;
|
||||
const method = c.req.method;
|
||||
const route = `${method}-${path}`;
|
||||
|
||||
const requiredDifficulty = await getCurrentCaptchaDifficulty(sqlCred, c);
|
||||
|
||||
try {
|
||||
const decodedPayload = await verify(token, jwtSecret);
|
||||
const payload = await tokenSchema.validate(decodedPayload);
|
||||
const difficulty = payload.difficulty;
|
||||
const tokenID = payload.id;
|
||||
const consumed = await lockManager.isLocked(tokenID);
|
||||
if (consumed) {
|
||||
const response: ErrorResponse = {
|
||||
message: "Token has already been used.",
|
||||
code: "INVALID_CREDENTIALS",
|
||||
errors: []
|
||||
};
|
||||
return c.json<ErrorResponse>(response, 401);
|
||||
}
|
||||
if (difficulty < requiredDifficulty) {
|
||||
const response: ErrorResponse = {
|
||||
message: "Token too weak.",
|
||||
code: "UNAUTHORIZED",
|
||||
errors: []
|
||||
};
|
||||
return c.json<ErrorResponse>(response, 401);
|
||||
}
|
||||
const EXPIRE_FIVE_MINUTES = 300;
|
||||
await lockManager.acquireLock(tokenID, EXPIRE_FIVE_MINUTES);
|
||||
} catch (e) {
|
||||
if (e instanceof JwtTokenInvalid) {
|
||||
const response: ErrorResponse = {
|
||||
message: "Failed to verify the token.",
|
||||
code: "INVALID_CREDENTIALS",
|
||||
errors: []
|
||||
};
|
||||
return c.json<ErrorResponse>(response, 400);
|
||||
} else if (e instanceof JwtTokenExpired) {
|
||||
const response: ErrorResponse = {
|
||||
message: "Token expired.",
|
||||
code: "INVALID_CREDENTIALS",
|
||||
errors: []
|
||||
};
|
||||
return c.json<ErrorResponse>(response, 400);
|
||||
} else if (e instanceof ValidationError) {
|
||||
const response: ErrorResponse = {
|
||||
code: "INVALID_QUERY_PARAMS",
|
||||
message: "Invalid query parameters",
|
||||
errors: e.errors
|
||||
};
|
||||
return c.json<ErrorResponse>(response, 400);
|
||||
} else {
|
||||
const response: ErrorResponse = {
|
||||
message: "Unknown error.",
|
||||
code: "UNKNOWN_ERROR",
|
||||
errors: []
|
||||
};
|
||||
return c.json<ErrorResponse>(response, 500);
|
||||
}
|
||||
}
|
||||
const duration = await getCaptchaConfigMaxDuration(sqlCred, route);
|
||||
const window = new SlidingWindow(redis, duration);
|
||||
|
||||
const identifierWithIP = getIdentifier(c, true);
|
||||
const identifier = getIdentifier(c, false);
|
||||
await window.event(`captcha-${identifier}`);
|
||||
await window.event(`captcha-${identifierWithIP}`);
|
||||
|
||||
await next();
|
||||
};
|
6
packages/backend/middleware/contentType.ts
Normal file
6
packages/backend/middleware/contentType.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Context, Next } from "hono";
|
||||
|
||||
export const contentType = async (c: Context, next: Next) => {
|
||||
await next();
|
||||
c.header("Content-Type", "application/json; charset=utf-8");
|
||||
};
|
14
packages/backend/middleware/cors.ts
Normal file
14
packages/backend/middleware/cors.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { cors } from "hono/cors";
|
||||
import { Context, Next } from "hono";
|
||||
|
||||
export const corsMiddleware = async (c: Context, next: Next) => {
|
||||
if (c.req.path.startsWith("/user") || c.req.path.startsWith("/login")) {
|
||||
const corsMiddlewareHandler = cors({
|
||||
origin: c.req.header("Origin"),
|
||||
credentials: true
|
||||
});
|
||||
return corsMiddlewareHandler(c, next);
|
||||
}
|
||||
const corsMiddlewareHandler = cors();
|
||||
return corsMiddlewareHandler(c, next);
|
||||
};
|
160
packages/backend/middleware/logger.ts
Normal file
160
packages/backend/middleware/logger.ts
Normal file
@ -0,0 +1,160 @@
|
||||
// Color constants
|
||||
import { Context, Next } from "hono";
|
||||
import { TimingVariables } from "hono/timing";
|
||||
import { getConnInfo } from "hono/bun";
|
||||
|
||||
const green = "\x1b[97;42m";
|
||||
const white = "\x1b[90;47m";
|
||||
const yellow = "\x1b[90;43m";
|
||||
const red = "\x1b[97;41m";
|
||||
const blue = "\x1b[97;44m";
|
||||
const magenta = "\x1b[97;45m";
|
||||
const cyan = "\x1b[97;46m";
|
||||
const reset = "\x1b[0m";
|
||||
|
||||
let consoleColorMode = "auto";
|
||||
|
||||
function formatCurrentTime() {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, "0"); // Month is 0-indexed
|
||||
const day = String(now.getDate()).padStart(2, "0");
|
||||
const hours = String(now.getHours()).padStart(2, "0");
|
||||
const minutes = String(now.getMinutes()).padStart(2, "0");
|
||||
const seconds = String(now.getSeconds()).padStart(2, "0");
|
||||
const milliseconds = String(now.getMilliseconds()).padStart(3, "0");
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
|
||||
}
|
||||
|
||||
export const DisableConsoleColor = () => {
|
||||
consoleColorMode = "disable";
|
||||
};
|
||||
|
||||
export const ForceConsoleColor = () => {
|
||||
consoleColorMode = "force";
|
||||
};
|
||||
|
||||
const defaultFormatter = (params) => {
|
||||
const latency = params.latency > 60000 ? `${Math.round(params.latency / 1000)}s` : `${params.latency}ms`;
|
||||
|
||||
let statusColor = white;
|
||||
if (params.isOutputColor) {
|
||||
if (params.status >= 100 && params.status < 300) statusColor = green;
|
||||
else if (params.status >= 300 && params.status < 400) statusColor = white;
|
||||
else if (params.status >= 400 && params.status < 500) statusColor = yellow;
|
||||
else statusColor = red;
|
||||
}
|
||||
|
||||
let methodColor = reset;
|
||||
switch (params.method) {
|
||||
case "GET":
|
||||
methodColor = blue;
|
||||
break;
|
||||
case "POST":
|
||||
methodColor = cyan;
|
||||
break;
|
||||
case "PUT":
|
||||
methodColor = yellow;
|
||||
break;
|
||||
case "DELETE":
|
||||
methodColor = red;
|
||||
break;
|
||||
case "PATCH":
|
||||
methodColor = green;
|
||||
break;
|
||||
case "HEAD":
|
||||
methodColor = magenta;
|
||||
break;
|
||||
case "OPTIONS":
|
||||
methodColor = white;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
`${params.timestamp} |${statusColor} ${params.status} ${reset}| ` +
|
||||
`${latency.padStart(7)} | ${params.ip.padStart(16)} |` +
|
||||
`${methodColor} ${params.method.padEnd(6)}${reset} ${params.path}`
|
||||
);
|
||||
};
|
||||
type Ctx = Context;
|
||||
export const logger = (config) => {
|
||||
const { formatter = defaultFormatter, output = console, skipPaths = [], skip = null } = config;
|
||||
|
||||
// Convert skipPaths to Set for faster lookups
|
||||
const skipPathsSet = new Set(skipPaths);
|
||||
|
||||
return async (c: Ctx, next: Next) => {
|
||||
const start = Date.now();
|
||||
const url = new URL(c.req.url);
|
||||
const path = url.pathname;
|
||||
|
||||
// Check if we should skip logging
|
||||
if (skipPathsSet.has(path) || (typeof skip === "function" && skip(c))) {
|
||||
return next();
|
||||
}
|
||||
|
||||
try {
|
||||
await next();
|
||||
} catch (error) {
|
||||
// Handle errors
|
||||
const errorParams = {
|
||||
timestamp: formatCurrentTime(),
|
||||
latency: Date.now() - start,
|
||||
status: 500,
|
||||
ip: getClientIP(c),
|
||||
method: c.req.method,
|
||||
path,
|
||||
error: error.message,
|
||||
isOutputColor: shouldColorize(c)
|
||||
};
|
||||
|
||||
output.error(
|
||||
formatter({
|
||||
...errorParams,
|
||||
errorMessage: error.message
|
||||
})
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
const status = c.res.status;
|
||||
const latency = Date.now() - start;
|
||||
|
||||
const params = {
|
||||
timestamp: formatCurrentTime(),
|
||||
latency,
|
||||
status,
|
||||
ip: getClientIP(c),
|
||||
method: c.req.method,
|
||||
path,
|
||||
bodySize: c.res.headers.get("content-length") || 0,
|
||||
isOutputColor: shouldColorize(c)
|
||||
};
|
||||
|
||||
// Format and output the log
|
||||
const logMessage = formatter(params);
|
||||
|
||||
if (status >= 400 && status < 500) {
|
||||
output.warn?.(logMessage) || output.log(logMessage);
|
||||
} else if (status >= 500) {
|
||||
output.error?.(logMessage) || output.log(logMessage);
|
||||
} else {
|
||||
output.log(logMessage);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
function shouldColorize(c) {
|
||||
if (consoleColorMode === "disable") return false;
|
||||
if (consoleColorMode === "force") return true;
|
||||
|
||||
// In development environment with TTY
|
||||
return process.stdout.isTTY;
|
||||
}
|
||||
|
||||
export function getClientIP(c: Ctx) {
|
||||
const info = getConnInfo(c);
|
||||
return info.remote.address;
|
||||
}
|
18
packages/backend/middleware/preetifyResponse.ts
Normal file
18
packages/backend/middleware/preetifyResponse.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { startTime, endTime } from "hono/timing";
|
||||
import { Context, Next } from "hono";
|
||||
|
||||
export const preetifyResponse = async (c: Context, next: Next) => {
|
||||
await next();
|
||||
const contentType = c.res.headers.get("Content-Type") || "";
|
||||
if (!contentType.includes("application/json")) return;
|
||||
const accept = c.req.header("Accept") || "";
|
||||
const secFetchMode = c.req.header("Sec-Fetch-Mode");
|
||||
const isBrowser = accept.includes("text/html") || secFetchMode === "navigate";
|
||||
if (isBrowser) {
|
||||
const json = await c.res.json();
|
||||
startTime(c, "seralize", "Prettify the response");
|
||||
const prettyJson = JSON.stringify(json, null, 2);
|
||||
endTime(c, "seralize");
|
||||
c.res = new Response(prettyJson, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
|
||||
}
|
||||
};
|
53
packages/backend/middleware/rateLimiters.ts
Normal file
53
packages/backend/middleware/rateLimiters.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import type { BlankEnv } from "hono/types";
|
||||
import { getConnInfo } from "hono/bun";
|
||||
import { Context, Next } from "hono";
|
||||
import { generateRandomId } from "@core/lib/randomID.ts";
|
||||
import { RateLimiter } from "@koshnic/ratelimit";
|
||||
import { ErrorResponse } from "@/src/schema";
|
||||
import { redis } from "@core/db/redis.ts";
|
||||
|
||||
export const getUserIP = (c: Context) => {
|
||||
let ipAddr = null;
|
||||
const info = getConnInfo(c);
|
||||
if (info.remote && info.remote.address) {
|
||||
ipAddr = info.remote.address;
|
||||
}
|
||||
const forwardedFor = c.req.header("X-Forwarded-For");
|
||||
if (forwardedFor) {
|
||||
ipAddr = forwardedFor.split(",")[0];
|
||||
}
|
||||
return ipAddr;
|
||||
};
|
||||
|
||||
export const getIdentifier = (c: Context, includeIP: boolean = true) => {
|
||||
let ipAddr = generateRandomId(6);
|
||||
if (getUserIP(c)) {
|
||||
ipAddr = getUserIP(c);
|
||||
}
|
||||
const path = c.req.path;
|
||||
const method = c.req.method;
|
||||
const ipIdentifier = includeIP ? `@${ipAddr}` : "";
|
||||
return `${method}-${path}${ipIdentifier}`;
|
||||
};
|
||||
|
||||
export const registerRateLimiter = async (c: Context<BlankEnv, "/user", {}>, next: Next) => {
|
||||
const limiter = new RateLimiter(redis);
|
||||
const identifier = getIdentifier(c, true);
|
||||
const { allowed, retryAfter } = await limiter.allow(identifier, {
|
||||
burst: 5,
|
||||
ratePerPeriod: 5,
|
||||
period: 120,
|
||||
cost: 1
|
||||
});
|
||||
|
||||
if (!allowed) {
|
||||
const response: ErrorResponse = {
|
||||
message: `Too many requests, please retry after ${Math.round(retryAfter)} seconds.`,
|
||||
code: "RATE_LIMIT_EXCEEDED",
|
||||
errors: []
|
||||
};
|
||||
return c.json<ErrorResponse>(response, 429);
|
||||
}
|
||||
|
||||
await next();
|
||||
};
|
30
packages/backend/package.json
Normal file
30
packages/backend/package.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@cvsa/backend",
|
||||
"private": false,
|
||||
"version": "0.6.0",
|
||||
"scripts": {
|
||||
"format": "prettier --write .",
|
||||
"dev": "NODE_ENV=development bun run --hot src/main.ts",
|
||||
"start": "NODE_ENV=production bun run src/main.ts",
|
||||
"build": "bun build ./src/main.ts --target bun --outdir ./dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@koshnic/ratelimit": "^1.0.3",
|
||||
"@rabbit-company/argon2id": "^2.1.0",
|
||||
"chalk": "^5.4.1",
|
||||
"hono": "^4.7.8",
|
||||
"hono-rate-limiter": "^0.4.2",
|
||||
"ioredis": "^5.6.1",
|
||||
"limiter": "^3.0.0",
|
||||
"postgres": "^3.4.5",
|
||||
"rate-limit-redis": "^4.2.0",
|
||||
"yup": "^1.6.1",
|
||||
"zod": "^3.24.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.11",
|
||||
"prettier": "^3.5.3"
|
||||
},
|
||||
"main": "./dist/main.js",
|
||||
"types": "./src/types.d.ts"
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
import { createHandlers } from "./utils.ts";
|
||||
import Argon2id from "@rabbit-company/argon2id";
|
||||
import { object, string, ValidationError } from "yup";
|
||||
import type { Context } from "hono";
|
||||
import type { Bindings, BlankEnv, BlankInput } from "hono/types";
|
||||
import type { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
|
||||
|
||||
const RegistrationBodySchema = object({
|
||||
username: string().trim().required("Username is required").max(50, "Username cannot exceed 50 characters"),
|
||||
password: string().required("Password is required"),
|
||||
nickname: string().optional(),
|
||||
});
|
||||
|
||||
type ContextType = Context<BlankEnv & { Bindings: Bindings }, "/user", BlankInput>;
|
||||
|
||||
export const userExists = async (username: string, client: Client) => {
|
||||
const query = `
|
||||
SELECT * FROM users WHERE username = $1
|
||||
`;
|
||||
const result = await client.queryObject(query, [username]);
|
||||
return result.rows.length > 0;
|
||||
};
|
||||
|
||||
export const registerHandler = createHandlers(async (c: ContextType) => {
|
||||
const client = c.get("dbCred");
|
||||
|
||||
try {
|
||||
const body = await RegistrationBodySchema.validate(await c.req.json());
|
||||
const { username, password, nickname } = body;
|
||||
|
||||
if (await userExists(username, client)) {
|
||||
return c.json({
|
||||
message: `User "${username}" already exists.`,
|
||||
}, 400);
|
||||
}
|
||||
|
||||
const hash = await Argon2id.hashEncoded(password);
|
||||
|
||||
const query = `
|
||||
INSERT INTO users (username, password, nickname) VALUES ($1, $2, $3)
|
||||
`;
|
||||
await client.queryObject(query, [username, hash, nickname || null]);
|
||||
|
||||
return c.json({
|
||||
message: `User "${username}" registered successfully.`,
|
||||
}, 201);
|
||||
} catch (e) {
|
||||
if (e instanceof ValidationError) {
|
||||
return c.json({
|
||||
message: "Invalid registration data.",
|
||||
errors: e.errors,
|
||||
}, 400);
|
||||
} else if (e instanceof SyntaxError) {
|
||||
return c.json({
|
||||
message: "Invalid JSON in request body.",
|
||||
}, 400);
|
||||
} else {
|
||||
console.error("Registration error:", e);
|
||||
return c.json({
|
||||
message: "An unexpected error occurred during registration.",
|
||||
error: (e as Error).message,
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
});
|
10
packages/backend/routes/404.ts
Normal file
10
packages/backend/routes/404.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Context } from "hono";
|
||||
|
||||
export const notFoundRoute = (c: Context) => {
|
||||
return c.json(
|
||||
{
|
||||
message: "Not Found"
|
||||
},
|
||||
404
|
||||
);
|
||||
};
|
99
packages/backend/routes/captcha/[id]/result/GET.ts
Normal file
99
packages/backend/routes/captcha/[id]/result/GET.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import { Context } from "hono";
|
||||
import { Bindings, BlankEnv } from "hono/types";
|
||||
import { ErrorResponse } from "src/schema";
|
||||
import { createHandlers } from "src/utils.ts";
|
||||
import { sign } from "hono/jwt";
|
||||
import { generateRandomId } from "@core/lib/randomID.ts";
|
||||
import { getJWTsecret } from "lib/auth/getJWTsecret.ts";
|
||||
|
||||
interface CaptchaResponse {
|
||||
success: boolean;
|
||||
difficulty?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const getChallengeVerificationResult = async (id: string, ans: string) => {
|
||||
const baseURL = process.env["UCAPTCHA_URL"];
|
||||
const url = new URL(baseURL);
|
||||
url.pathname = `/challenge/${id}/validation`;
|
||||
return await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
y: ans
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
export const verifyChallengeHandler = createHandlers(
|
||||
async (c: Context<BlankEnv & { Bindings: Bindings }, "/captcha/:id/result">) => {
|
||||
const id = c.req.param("id");
|
||||
const ans = c.req.query("ans");
|
||||
if (!ans) {
|
||||
const response: ErrorResponse = {
|
||||
message: "Missing required query parameter: ans",
|
||||
code: "INVALID_QUERY_PARAMS",
|
||||
errors: []
|
||||
};
|
||||
return c.json<ErrorResponse>(response, 400);
|
||||
}
|
||||
const res = await getChallengeVerificationResult(id, ans);
|
||||
const data: CaptchaResponse = await res.json();
|
||||
if (data.error && res.status === 404) {
|
||||
const response: ErrorResponse = {
|
||||
message: data.error,
|
||||
code: "ENTITY_NOT_FOUND",
|
||||
i18n: {
|
||||
key: "backend.error.captcha_not_found"
|
||||
},
|
||||
errors: []
|
||||
};
|
||||
return c.json<ErrorResponse>(response, 401);
|
||||
} else if (data.error && res.status === 400) {
|
||||
const response: ErrorResponse = {
|
||||
message: data.error,
|
||||
code: "INVALID_QUERY_PARAMS",
|
||||
errors: []
|
||||
};
|
||||
return c.json<ErrorResponse>(response, 400);
|
||||
} else if (data.error) {
|
||||
const response: ErrorResponse = {
|
||||
message: data.error,
|
||||
code: "UNKNOWN_ERROR",
|
||||
errors: []
|
||||
};
|
||||
return c.json<ErrorResponse>(response, 500);
|
||||
}
|
||||
if (!data.success) {
|
||||
const response: ErrorResponse = {
|
||||
message: "Incorrect answer",
|
||||
code: "INVALID_CREDENTIALS",
|
||||
errors: []
|
||||
};
|
||||
return c.json<ErrorResponse>(response, 401);
|
||||
}
|
||||
|
||||
const [r, err] = getJWTsecret();
|
||||
if (err) {
|
||||
return c.json<ErrorResponse>(r as ErrorResponse, 500);
|
||||
}
|
||||
const jwtSecret = r as string;
|
||||
|
||||
const tokenID = generateRandomId(6);
|
||||
const NOW = Math.floor(Date.now() / 1000);
|
||||
const FIVE_MINUTES_LATER = NOW + 60 * 5;
|
||||
const jwt = await sign(
|
||||
{
|
||||
difficulty: data.difficulty!,
|
||||
id: tokenID,
|
||||
exp: FIVE_MINUTES_LATER
|
||||
},
|
||||
jwtSecret
|
||||
);
|
||||
return c.json({
|
||||
token: jwt
|
||||
});
|
||||
}
|
||||
);
|
44
packages/backend/routes/captcha/difficulty/GET.ts
Normal file
44
packages/backend/routes/captcha/difficulty/GET.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { createHandlers } from "src/utils.ts";
|
||||
import { object, string, ValidationError } from "yup";
|
||||
import { ErrorResponse } from "src/schema";
|
||||
import { getCurrentCaptchaDifficulty } from "@/lib/auth/captchaDifficulty.ts";
|
||||
import { sqlCred } from "@core/db/dbNew.ts";
|
||||
|
||||
const queryParamsSchema = object({
|
||||
route: string().matches(/(?:GET|POST|PUT|PATCH|DELETE)-\/.*/g)
|
||||
});
|
||||
|
||||
export const getCaptchaDifficultyHandler = createHandlers(async (c) => {
|
||||
try {
|
||||
const queryParams = await queryParamsSchema.validate(c.req.query());
|
||||
const { route } = queryParams;
|
||||
const difficulty = await getCurrentCaptchaDifficulty(sqlCred, route);
|
||||
if (!difficulty) {
|
||||
const response: ErrorResponse<unknown> = {
|
||||
code: "ENTITY_NOT_FOUND",
|
||||
message: "No difficulty configs found for this route.",
|
||||
errors: []
|
||||
};
|
||||
return c.json<ErrorResponse<unknown>>(response, 404);
|
||||
}
|
||||
return c.json({
|
||||
difficulty: difficulty
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof ValidationError) {
|
||||
const response: ErrorResponse = {
|
||||
code: "INVALID_QUERY_PARAMS",
|
||||
message: "Invalid query parameters",
|
||||
errors: e.errors
|
||||
};
|
||||
return c.json<ErrorResponse>(response, 400);
|
||||
} else {
|
||||
const response: ErrorResponse<unknown> = {
|
||||
code: "UNKNOWN_ERROR",
|
||||
message: "Unknown error",
|
||||
errors: [e]
|
||||
};
|
||||
return c.json<ErrorResponse<unknown>>(response, 500);
|
||||
}
|
||||
}
|
||||
});
|
2
packages/backend/routes/captcha/index.ts
Normal file
2
packages/backend/routes/captcha/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./session/POST.ts";
|
||||
export * from "./[id]/result/GET.ts";
|
51
packages/backend/routes/captcha/session/POST.ts
Normal file
51
packages/backend/routes/captcha/session/POST.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { createHandlers } from "src/utils.ts";
|
||||
import { getCurrentCaptchaDifficulty } from "@/lib/auth/captchaDifficulty.ts";
|
||||
import { sqlCred } from "@core/db/dbNew.ts";
|
||||
import { object, string, ValidationError } from "yup";
|
||||
import { CaptchaSessionResponse, ErrorResponse } from "@/src/schema";
|
||||
import type { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
|
||||
const bodySchema = object({
|
||||
route: string().matches(/(?:GET|POST|PUT|PATCH|DELETE)-\/.*/g)
|
||||
});
|
||||
|
||||
const createNewChallenge = async (difficulty: number) => {
|
||||
const baseURL = process.env["UCAPTCHA_URL"];
|
||||
const url = new URL(baseURL);
|
||||
url.pathname = "/challenge";
|
||||
return await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
difficulty: difficulty
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
export const createCaptchaSessionHandler = createHandlers(async (c) => {
|
||||
try {
|
||||
const requestBody = await bodySchema.validate(await c.req.json());
|
||||
const { route } = requestBody;
|
||||
const difficuly = await getCurrentCaptchaDifficulty(sqlCred, route);
|
||||
const res = await createNewChallenge(difficuly);
|
||||
return c.json<CaptchaSessionResponse | unknown>(await res.json(), res.status as ContentfulStatusCode);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof ValidationError) {
|
||||
const response: ErrorResponse = {
|
||||
code: "INVALID_QUERY_PARAMS",
|
||||
message: "Invalid query parameters",
|
||||
errors: e.errors
|
||||
};
|
||||
return c.json<ErrorResponse>(response, 400);
|
||||
} else {
|
||||
const response: ErrorResponse<unknown> = {
|
||||
code: "UNKNOWN_ERROR",
|
||||
message: "Unknown error",
|
||||
errors: [e]
|
||||
};
|
||||
return c.json<ErrorResponse<unknown>>(response, 500);
|
||||
}
|
||||
}
|
||||
});
|
@ -1,6 +1,6 @@
|
||||
import { getSingerForBirthday, pickSinger, pickSpecialSinger, type Singer } from "./singers.ts";
|
||||
import { VERSION } from "./main.ts";
|
||||
import { createHandlers } from "./utils.ts";
|
||||
import { getSingerForBirthday, pickSinger, pickSpecialSinger, type Singer } from "lib/const/singers.ts";
|
||||
import { VERSION } from "src/main.ts";
|
||||
import { createHandlers } from "src/utils.ts";
|
||||
|
||||
export const rootHandler = createHandlers((c) => {
|
||||
let singer: Singer | Singer[];
|
||||
@ -17,13 +17,13 @@ export const rootHandler = createHandlers((c) => {
|
||||
singer = pickSinger();
|
||||
}
|
||||
return c.json({
|
||||
"project": {
|
||||
"name": "中V档案馆",
|
||||
"motto": "一起唱吧,心中的歌!",
|
||||
project: {
|
||||
name: "中V档案馆",
|
||||
motto: "一起唱吧,心中的歌!"
|
||||
},
|
||||
"status": 200,
|
||||
"version": VERSION,
|
||||
"time": Date.now(),
|
||||
"singer": singer,
|
||||
status: 200,
|
||||
version: VERSION,
|
||||
time: Date.now(),
|
||||
singer: singer
|
||||
});
|
||||
});
|
105
packages/backend/routes/login/session/POST.ts
Normal file
105
packages/backend/routes/login/session/POST.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { Context } from "hono";
|
||||
import { Bindings, BlankEnv } from "hono/types";
|
||||
import { ErrorResponse, LoginResponse } from "src/schema";
|
||||
import { createHandlers } from "src/utils.ts";
|
||||
import { sqlCred } from "@core/db/dbNew";
|
||||
import { object, string, ValidationError } from "yup";
|
||||
import { setCookie } from "hono/cookie";
|
||||
import Argon2id from "@rabbit-company/argon2id";
|
||||
import { createLoginSession } from "routes/user/POST";
|
||||
import { UserType } from "@core/db/schema";
|
||||
|
||||
const LoginBodySchema = object({
|
||||
username: string().trim().required("Username is required").max(50, "Username cannot exceed 50 characters"),
|
||||
password: string().required("Password is required")
|
||||
});
|
||||
|
||||
export const loginHandler = createHandlers(
|
||||
async (c: Context<BlankEnv & { Bindings: Bindings }, "/user/session/:id">) => {
|
||||
try {
|
||||
const body = await LoginBodySchema.validate(await c.req.json());
|
||||
const { username, password: submittedPassword } = body;
|
||||
|
||||
const result = await sqlCred<UserType[]>`
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE username = ${username}
|
||||
`;
|
||||
|
||||
if (result.length === 0) {
|
||||
const response: ErrorResponse<string> = {
|
||||
message: `User does not exist.`,
|
||||
errors: [`User ${username} does not exist.`],
|
||||
code: "ENTITY_NOT_FOUND"
|
||||
};
|
||||
return c.json<ErrorResponse<string>>(response, 400);
|
||||
}
|
||||
|
||||
const storedPassword = result[0].password;
|
||||
const uid = result[0].id;
|
||||
const nickname = result[0].nickname;
|
||||
const role = result[0].role;
|
||||
|
||||
const passwordAreSame = await Argon2id.verify(storedPassword, submittedPassword);
|
||||
|
||||
if (!passwordAreSame) {
|
||||
const response: ErrorResponse<string> = {
|
||||
message: "Incorrect password.",
|
||||
errors: [],
|
||||
i18n: {
|
||||
key: "backend.error.incorrect_password"
|
||||
},
|
||||
code: "INVALID_CREDENTIALS"
|
||||
};
|
||||
return c.json<ErrorResponse<string>>(response, 401);
|
||||
}
|
||||
|
||||
const sessionID = await createLoginSession(uid, c);
|
||||
|
||||
const response: LoginResponse = {
|
||||
uid: uid,
|
||||
username: username,
|
||||
nickname: nickname,
|
||||
role: role,
|
||||
token: sessionID
|
||||
};
|
||||
|
||||
const A_YEAR = 365 * 86400;
|
||||
const isDev = process.env.NODE_ENV === "development";
|
||||
|
||||
setCookie(c, "session_id", sessionID, {
|
||||
path: "/",
|
||||
maxAge: A_YEAR,
|
||||
domain: process.env.DOMAIN,
|
||||
secure: isDev ? true : true,
|
||||
sameSite: isDev ? "None" : "Lax",
|
||||
httpOnly: true
|
||||
});
|
||||
|
||||
return c.json<LoginResponse>(response, 200);
|
||||
} catch (e) {
|
||||
if (e instanceof ValidationError) {
|
||||
const response: ErrorResponse<string> = {
|
||||
message: "Invalid registration data.",
|
||||
errors: e.errors,
|
||||
code: "INVALID_PAYLOAD"
|
||||
};
|
||||
return c.json<ErrorResponse<string>>(response, 400);
|
||||
} else if (e instanceof SyntaxError) {
|
||||
const response: ErrorResponse<string> = {
|
||||
message: "Invalid JSON payload.",
|
||||
errors: [e.message],
|
||||
code: "INVALID_FORMAT"
|
||||
};
|
||||
return c.json<ErrorResponse<string>>(response, 400);
|
||||
} else {
|
||||
const response: ErrorResponse<string> = {
|
||||
message: "Unknown error.",
|
||||
errors: [(e as Error).message],
|
||||
code: "UNKNOWN_ERROR"
|
||||
};
|
||||
return c.json<ErrorResponse<string>>(response, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
24
packages/backend/routes/ping/index.ts
Normal file
24
packages/backend/routes/ping/index.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { getClientIP } from "middleware/logger.ts";
|
||||
import { createHandlers } from "src/utils.ts";
|
||||
import { VERSION } from "src/main.ts";
|
||||
|
||||
export const pingHandler = createHandlers(async (c) => {
|
||||
const requestHeaders = c.req.raw.headers;
|
||||
return c.json({
|
||||
message: "pong",
|
||||
request: {
|
||||
headers: requestHeaders,
|
||||
ip: getClientIP(c),
|
||||
mode: c.req.raw.mode,
|
||||
method: c.req.method,
|
||||
query: new URL(c.req.url).searchParams,
|
||||
body: await c.req.text(),
|
||||
url: c.req.raw.url
|
||||
},
|
||||
response: {
|
||||
time: new Date().getTime(),
|
||||
status: 200,
|
||||
version: VERSION
|
||||
}
|
||||
});
|
||||
});
|
75
packages/backend/routes/session/[id]/DELETE.ts
Normal file
75
packages/backend/routes/session/[id]/DELETE.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { Context } from "hono";
|
||||
import { Bindings, BlankEnv } from "hono/types";
|
||||
import { ErrorResponse } from "src/schema";
|
||||
import { createHandlers } from "src/utils.ts";
|
||||
import { sqlCred } from "@core/db/dbNew";
|
||||
import { object, string, ValidationError } from "yup";
|
||||
import { setCookie } from "hono/cookie";
|
||||
|
||||
const loginSessionExists = async (sessionID: string) => {
|
||||
const result = await sqlCred`
|
||||
SELECT 1
|
||||
FROM login_sessions
|
||||
WHERE id = ${sessionID}
|
||||
`;
|
||||
return result.length > 0;
|
||||
};
|
||||
|
||||
export const logoutHandler = createHandlers(async (c: Context<BlankEnv & { Bindings: Bindings }, "/session/:id">) => {
|
||||
try {
|
||||
const session_id = c.req.param("id");
|
||||
|
||||
const exists = loginSessionExists(session_id);
|
||||
|
||||
if (!exists) {
|
||||
const response: ErrorResponse<string> = {
|
||||
message: "Cannot found given session_id.",
|
||||
errors: [`Session ${session_id} not found`],
|
||||
code: "ENTITY_NOT_FOUND"
|
||||
};
|
||||
return c.json<ErrorResponse<string>>(response, 404);
|
||||
}
|
||||
|
||||
await sqlCred`
|
||||
UPDATE login_sessions
|
||||
SET deactivated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ${session_id}
|
||||
`;
|
||||
|
||||
const isDev = process.env.NODE_ENV === "development";
|
||||
|
||||
setCookie(c, "session_id", "", {
|
||||
path: "/",
|
||||
maxAge: 0,
|
||||
domain: process.env.DOMAIN,
|
||||
secure: isDev ? true : true,
|
||||
sameSite: isDev ? "None" : "Lax",
|
||||
httpOnly: true
|
||||
});
|
||||
|
||||
return c.body(null, 204);
|
||||
} catch (e) {
|
||||
if (e instanceof ValidationError) {
|
||||
const response: ErrorResponse<string> = {
|
||||
message: "Invalid registration data.",
|
||||
errors: e.errors,
|
||||
code: "INVALID_PAYLOAD"
|
||||
};
|
||||
return c.json<ErrorResponse<string>>(response, 400);
|
||||
} else if (e instanceof SyntaxError) {
|
||||
const response: ErrorResponse<string> = {
|
||||
message: "Invalid JSON payload.",
|
||||
errors: [e.message],
|
||||
code: "INVALID_FORMAT"
|
||||
};
|
||||
return c.json<ErrorResponse<string>>(response, 400);
|
||||
} else {
|
||||
const response: ErrorResponse<string> = {
|
||||
message: "Unknown error.",
|
||||
errors: [(e as Error).message],
|
||||
code: "UNKNOWN_ERROR"
|
||||
};
|
||||
return c.json<ErrorResponse<string>>(response, 500);
|
||||
}
|
||||
}
|
||||
});
|
1
packages/backend/routes/session/index.ts
Normal file
1
packages/backend/routes/session/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./[id]/DELETE";
|
140
packages/backend/routes/user/POST.ts
Normal file
140
packages/backend/routes/user/POST.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import { createHandlers } from "src/utils.ts";
|
||||
import Argon2id from "@rabbit-company/argon2id";
|
||||
import { object, string, ValidationError } from "yup";
|
||||
import type { Context } from "hono";
|
||||
import type { Bindings, BlankEnv, BlankInput } from "hono/types";
|
||||
import { sqlCred } from "@core/db/dbNew.ts";
|
||||
import { ErrorResponse, SignUpResponse } from "src/schema";
|
||||
import { generateRandomId } from "@core/lib/randomID";
|
||||
import { getUserIP } from "@/middleware/rateLimiters";
|
||||
import { setCookie } from "hono/cookie";
|
||||
|
||||
const RegistrationBodySchema = object({
|
||||
username: string().trim().required("Username is required").max(50, "Username cannot exceed 50 characters"),
|
||||
password: string().required("Password is required"),
|
||||
nickname: string().optional()
|
||||
});
|
||||
|
||||
type ContextType = Context<BlankEnv & { Bindings: Bindings }, "/user", BlankInput>;
|
||||
|
||||
export const userExists = async (username: string) => {
|
||||
const result = await sqlCred`
|
||||
SELECT 1
|
||||
FROM users
|
||||
WHERE username = ${username}
|
||||
`;
|
||||
return result.length > 0;
|
||||
};
|
||||
|
||||
export const createLoginSession = async (uid: number, c: Context): Promise<string> => {
|
||||
const ipAddress = getUserIP(c) || null;
|
||||
const userAgent = c.req.header("User-Agent") || null;
|
||||
const id = generateRandomId(24);
|
||||
await sqlCred`
|
||||
INSERT INTO login_sessions (id, uid, expire_at, ip_address, user_agent)
|
||||
VALUES (${id}, ${uid}, CURRENT_TIMESTAMP + INTERVAL '1 year', ${ipAddress}, ${userAgent})
|
||||
`;
|
||||
return id;
|
||||
};
|
||||
|
||||
const getUserIDByName = async (username: string) => {
|
||||
const result = await sqlCred<{ id: number }[]>`
|
||||
SELECT id
|
||||
FROM users
|
||||
WHERE username = ${username}
|
||||
`;
|
||||
if (result.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return result[0].id;
|
||||
};
|
||||
|
||||
export const registerHandler = createHandlers(async (c: ContextType) => {
|
||||
try {
|
||||
const body = await RegistrationBodySchema.validate(await c.req.json());
|
||||
const { username, password, nickname } = body;
|
||||
|
||||
if (await userExists(username)) {
|
||||
const response: ErrorResponse = {
|
||||
message: `User "${username}" already exists.`,
|
||||
code: "ENTITY_EXISTS",
|
||||
errors: [],
|
||||
i18n: {
|
||||
key: "backend.error.user_exists",
|
||||
values: {
|
||||
username: username
|
||||
}
|
||||
}
|
||||
};
|
||||
return c.json<ErrorResponse>(response, 400);
|
||||
}
|
||||
|
||||
const hash = await Argon2id.hashEncoded(password);
|
||||
|
||||
await sqlCred`
|
||||
INSERT INTO users (username, password, nickname)
|
||||
VALUES (${username}, ${hash}, ${nickname ? nickname : null})
|
||||
`;
|
||||
|
||||
const uid = await getUserIDByName(username);
|
||||
|
||||
if (!uid) {
|
||||
const response: ErrorResponse<string> = {
|
||||
message: "Cannot find registered user.",
|
||||
errors: [`Cannot find user ${username} in table 'users'.`],
|
||||
code: "ENTITY_NOT_FOUND",
|
||||
i18n: {
|
||||
key: "backend.error.user_not_found_after_register",
|
||||
values: {
|
||||
username: username
|
||||
}
|
||||
}
|
||||
};
|
||||
return c.json<ErrorResponse<string>>(response, 500);
|
||||
}
|
||||
|
||||
const sessionID = await createLoginSession(uid, c);
|
||||
|
||||
const response: SignUpResponse = {
|
||||
username: username,
|
||||
token: sessionID
|
||||
};
|
||||
|
||||
const A_YEAR = 365 * 86400;
|
||||
const isDev = process.env.NODE_ENV === "development";
|
||||
|
||||
setCookie(c, "session_id", sessionID, {
|
||||
path: "/",
|
||||
maxAge: A_YEAR,
|
||||
domain: process.env.DOMAIN,
|
||||
secure: isDev ? false : true,
|
||||
sameSite: "Lax",
|
||||
httpOnly: true
|
||||
});
|
||||
|
||||
return c.json<SignUpResponse>(response, 201);
|
||||
} catch (e) {
|
||||
if (e instanceof ValidationError) {
|
||||
const response: ErrorResponse<string> = {
|
||||
message: "Invalid registration data.",
|
||||
errors: e.errors,
|
||||
code: "INVALID_PAYLOAD"
|
||||
};
|
||||
return c.json<ErrorResponse<string>>(response, 400);
|
||||
} else if (e instanceof SyntaxError) {
|
||||
const response: ErrorResponse<string> = {
|
||||
message: "Invalid JSON payload.",
|
||||
errors: [e.message],
|
||||
code: "INVALID_FORMAT"
|
||||
};
|
||||
return c.json<ErrorResponse<string>>(response, 400);
|
||||
} else {
|
||||
const response: ErrorResponse<string> = {
|
||||
message: "Unknown error.",
|
||||
errors: [(e as Error).message],
|
||||
code: "UNKNOWN_ERROR"
|
||||
};
|
||||
return c.json<ErrorResponse<string>>(response, 500);
|
||||
}
|
||||
}
|
||||
});
|
2
packages/backend/routes/user/index.ts
Normal file
2
packages/backend/routes/user/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./POST.ts";
|
||||
export * from "./session/[id]/GET.ts";
|
32
packages/backend/routes/user/session/[id]/GET.ts
Normal file
32
packages/backend/routes/user/session/[id]/GET.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { Context } from "hono";
|
||||
import { Bindings, BlankEnv } from "hono/types";
|
||||
import { ErrorResponse } from "src/schema";
|
||||
import { createHandlers } from "src/utils.ts";
|
||||
import { sqlCred } from "@core/db/dbNew";
|
||||
import { UserType } from "@core/db/schema";
|
||||
|
||||
export const getUserByLoginSessionHandler = createHandlers(
|
||||
async (c: Context<BlankEnv & { Bindings: Bindings }, "/user/session/:id">) => {
|
||||
const id = c.req.param("id");
|
||||
const users = await sqlCred<UserType[]>`
|
||||
SELECT u.*
|
||||
FROM users u
|
||||
JOIN login_sessions ls ON u.id = ls.uid
|
||||
WHERE ls.id = ${id};
|
||||
`;
|
||||
if (users.length === 0) {
|
||||
const response: ErrorResponse = {
|
||||
message: "Cannot find user",
|
||||
code: "ENTITY_NOT_FOUND",
|
||||
errors: []
|
||||
};
|
||||
return c.json<ErrorResponse>(response, 404);
|
||||
}
|
||||
const user = users[0];
|
||||
return c.json({
|
||||
username: user.username,
|
||||
nickname: user.nickname,
|
||||
role: user.role
|
||||
});
|
||||
}
|
||||
);
|
@ -1,21 +1,21 @@
|
||||
import logger from "log/logger.ts";
|
||||
import { Redis } from "ioredis";
|
||||
import logger from "@core/log/logger.ts";
|
||||
import { redis } from "@core/db/redis.ts";
|
||||
import { sql } from "@core/db/dbNew.ts";
|
||||
import { number, ValidationError } from "yup";
|
||||
import { createHandlers } from "./utils.ts";
|
||||
import { getVideoInfo, getVideoInfoByBV } from "@crawler/net/videoInfo";
|
||||
import { createHandlers } from "@/src/utils.ts";
|
||||
import { getVideoInfo, getVideoInfoByBV } from "@core/net/getVideoInfo.ts";
|
||||
import { idSchema } from "./snapshots.ts";
|
||||
import { NetSchedulerError } from "@core/net/delegate.ts";
|
||||
import type { Context } from "hono";
|
||||
import type { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
|
||||
import type { BlankEnv, BlankInput } from "hono/types";
|
||||
import type { VideoInfoData } from "@core/net/bilibili.d.ts";
|
||||
import { startTime, endTime } from "hono/timing";
|
||||
|
||||
const redis = new Redis({ maxRetriesPerRequest: null });
|
||||
const CACHE_EXPIRATION_SECONDS = 60;
|
||||
|
||||
type ContextType = Context<BlankEnv, "/video/:id/info", BlankInput>;
|
||||
|
||||
async function insertVideoSnapshot(client: Client, data: VideoInfoData) {
|
||||
async function insertVideoSnapshot(data: VideoInfoData) {
|
||||
const views = data.stat.view;
|
||||
const danmakus = data.stat.danmaku;
|
||||
const replies = data.stat.reply;
|
||||
@ -25,21 +25,16 @@ async function insertVideoSnapshot(client: Client, data: VideoInfoData) {
|
||||
const favorites = data.stat.favorite;
|
||||
const aid = data.aid;
|
||||
|
||||
const query: string = `
|
||||
await sql`
|
||||
INSERT INTO video_snapshot (aid, views, danmakus, replies, likes, coins, shares, favorites)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
VALUES (${aid}, ${views}, ${danmakus}, ${replies}, ${likes}, ${coins}, ${shares}, ${favorites})
|
||||
`;
|
||||
|
||||
await client.queryObject(
|
||||
query,
|
||||
[aid, views, danmakus, replies, likes, coins, shares, favorites],
|
||||
);
|
||||
|
||||
logger.log(`Inserted into snapshot for video ${aid} by videoInfo API.`, "api", "fn:insertVideoSnapshot");
|
||||
}
|
||||
|
||||
export const videoInfoHandler = createHandlers(async (c: ContextType) => {
|
||||
const client = c.get("db");
|
||||
startTime(c, "parse", "Parse the request");
|
||||
try {
|
||||
const id = await idSchema.validate(c.req.param("id"));
|
||||
let videoId: string | number = id as string;
|
||||
@ -50,27 +45,33 @@ export const videoInfoHandler = createHandlers(async (c: ContextType) => {
|
||||
}
|
||||
|
||||
const cacheKey = `cvsa:videoInfo:${videoId}`;
|
||||
endTime(c, "parse");
|
||||
startTime(c, "cache", "Check for cached data");
|
||||
const cachedData = await redis.get(cacheKey);
|
||||
|
||||
endTime(c, "cache");
|
||||
if (cachedData) {
|
||||
return c.json(JSON.parse(cachedData));
|
||||
}
|
||||
|
||||
startTime(c, "net", "Fetch data");
|
||||
let result: VideoInfoData | number;
|
||||
if (typeof videoId === "number") {
|
||||
result = await getVideoInfo(videoId, "getVideoInfo");
|
||||
} else {
|
||||
result = await getVideoInfoByBV(videoId, "getVideoInfo");
|
||||
}
|
||||
endTime(c, "net");
|
||||
|
||||
if (typeof result === "number") {
|
||||
return c.json({ message: "Error fetching video info", code: result }, 500);
|
||||
}
|
||||
|
||||
startTime(c, "db", "Write data to database");
|
||||
|
||||
await redis.setex(cacheKey, CACHE_EXPIRATION_SECONDS, JSON.stringify(result));
|
||||
|
||||
await insertVideoSnapshot(client, result);
|
||||
await insertVideoSnapshot(result);
|
||||
|
||||
endTime(c, "db");
|
||||
return c.json(result);
|
||||
} catch (e) {
|
||||
if (e instanceof ValidationError) {
|
@ -1,22 +1,24 @@
|
||||
import type { Context } from "hono";
|
||||
import { createHandlers } from "./utils.ts";
|
||||
import { createHandlers } from "src/utils.ts";
|
||||
import type { BlankEnv, BlankInput } from "hono/types";
|
||||
import { getVideoSnapshots, getVideoSnapshotsByBV } from "@core/db/videoSnapshot.ts";
|
||||
import { getVideoSnapshots, getVideoSnapshotsByBV } from "db/snapshots.ts";
|
||||
import type { VideoSnapshotType } from "@core/db/schema.d.ts";
|
||||
import { boolean, mixed, number, object, ValidationError } from "yup";
|
||||
import { ErrorResponse } from "src/schema";
|
||||
import { startTime, endTime } from "hono/timing";
|
||||
|
||||
const SnapshotQueryParamsSchema = object({
|
||||
ps: number().integer().optional().positive(),
|
||||
pn: number().integer().optional().positive(),
|
||||
offset: number().integer().optional().positive(),
|
||||
reverse: boolean().optional(),
|
||||
reverse: boolean().optional()
|
||||
});
|
||||
|
||||
export const idSchema = mixed().test(
|
||||
"is-valid-id",
|
||||
'id must be a string starting with "av" followed by digits, or "BV" followed by 10 alphanumeric characters, or a positive integer',
|
||||
async (value) => {
|
||||
if (value && await number().integer().isValid(value)) {
|
||||
if (value && (await number().integer().isValid(value))) {
|
||||
const v = parseInt(value as string);
|
||||
return Number.isInteger(v) && v > 0;
|
||||
}
|
||||
@ -34,13 +36,12 @@ export const idSchema = mixed().test(
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
type ContextType = Context<BlankEnv, "/video/:id/snapshots", BlankInput>;
|
||||
export const getSnapshotsHanlder = createHandlers(async (c: ContextType) => {
|
||||
const client = c.get("db");
|
||||
|
||||
startTime(c, "parse", "Parse the request");
|
||||
try {
|
||||
const idParam = await idSchema.validate(c.req.param("id"));
|
||||
let videoId: string | number = idParam as string;
|
||||
@ -70,23 +71,36 @@ export const getSnapshotsHanlder = createHandlers(async (c: ContextType) => {
|
||||
|
||||
let result: VideoSnapshotType[];
|
||||
|
||||
endTime(c, "parse");
|
||||
startTime(c, "db", "Query the database");
|
||||
if (typeof videoId === "number") {
|
||||
result = await getVideoSnapshots(client, videoId, limit, pageOrOffset, reverse, mode);
|
||||
result = await getVideoSnapshots(videoId, limit, pageOrOffset, reverse, mode);
|
||||
} else {
|
||||
result = await getVideoSnapshotsByBV(client, videoId, limit, pageOrOffset, reverse, mode);
|
||||
result = await getVideoSnapshotsByBV(videoId, limit, pageOrOffset, reverse, mode);
|
||||
}
|
||||
endTime(c, "db");
|
||||
|
||||
const rows = result.map((row) => ({
|
||||
...row,
|
||||
aid: Number(row.aid),
|
||||
aid: Number(row.aid)
|
||||
}));
|
||||
|
||||
return c.json(rows);
|
||||
} catch (e) {
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof ValidationError) {
|
||||
return c.json({ message: "Invalid query parameters", errors: e.errors }, 400);
|
||||
const response: ErrorResponse<string> = {
|
||||
code: "INVALID_QUERY_PARAMS",
|
||||
message: "Invalid query parameters",
|
||||
errors: e.errors
|
||||
};
|
||||
return c.json<ErrorResponse<string>>(response, 400);
|
||||
} else {
|
||||
return c.json({ message: "Unhandled error", error: e }, 500);
|
||||
const response: ErrorResponse<unknown> = {
|
||||
code: "UNKNOWN_ERROR",
|
||||
message: "Unhandled error",
|
||||
errors: [e]
|
||||
};
|
||||
return c.json<ErrorResponse<unknown>>(response, 500);
|
||||
}
|
||||
}
|
||||
});
|
2
packages/backend/routes/video/index.ts
Normal file
2
packages/backend/routes/video/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./[id]/info";
|
||||
export * from "./[id]/snapshots";
|
65
packages/backend/routes/videos/GET.ts
Normal file
65
packages/backend/routes/videos/GET.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import type { Context } from "hono";
|
||||
import { createHandlers } from "src/utils.ts";
|
||||
import type { BlankEnv, BlankInput } from "hono/types";
|
||||
import { number, object, ValidationError } from "yup";
|
||||
import { ErrorResponse } from "src/schema";
|
||||
import { startTime, endTime } from "hono/timing";
|
||||
import { getVideosInViewsRange } from "@/db/latestSnapshots";
|
||||
|
||||
const SnapshotQueryParamsSchema = object({
|
||||
min_views: number().integer().optional().positive(),
|
||||
max_views: number().integer().optional().positive()
|
||||
});
|
||||
|
||||
type ContextType = Context<BlankEnv, "/videos", BlankInput>;
|
||||
|
||||
export const getVideosHanlder = createHandlers(async (c: ContextType) => {
|
||||
startTime(c, "parse", "Parse the request");
|
||||
try {
|
||||
const queryParams = await SnapshotQueryParamsSchema.validate(c.req.query());
|
||||
const { min_views, max_views } = queryParams;
|
||||
|
||||
if (!min_views && !max_views) {
|
||||
const response: ErrorResponse<string> = {
|
||||
code: "INVALID_QUERY_PARAMS",
|
||||
message: "Invalid query parameters",
|
||||
errors: ["Must provide one of these query parameters: min_views, max_views"]
|
||||
};
|
||||
return c.json<ErrorResponse<string>>(response, 400);
|
||||
}
|
||||
|
||||
endTime(c, "parse");
|
||||
|
||||
startTime(c, "db", "Query the database");
|
||||
|
||||
const minViews = min_views ? min_views : 0;
|
||||
const maxViews = max_views ? max_views : 2147483647;
|
||||
|
||||
const result = await getVideosInViewsRange(minViews, maxViews);
|
||||
|
||||
endTime(c, "db");
|
||||
|
||||
const rows = result.map((row) => ({
|
||||
...row,
|
||||
aid: Number(row.aid)
|
||||
}));
|
||||
|
||||
return c.json(rows);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof ValidationError) {
|
||||
const response: ErrorResponse<string> = {
|
||||
code: "INVALID_QUERY_PARAMS",
|
||||
message: "Invalid query parameters",
|
||||
errors: e.errors
|
||||
};
|
||||
return c.json<ErrorResponse<string>>(response, 400);
|
||||
} else {
|
||||
const response: ErrorResponse<unknown> = {
|
||||
code: "UNKNOWN_ERROR",
|
||||
message: "Unhandled error",
|
||||
errors: [e]
|
||||
};
|
||||
return c.json<ErrorResponse<unknown>>(response, 500);
|
||||
}
|
||||
}
|
||||
});
|
1
packages/backend/routes/videos/index.ts
Normal file
1
packages/backend/routes/videos/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./GET.ts";
|
@ -1,103 +0,0 @@
|
||||
export const singers = [
|
||||
{
|
||||
"name": "洛天依",
|
||||
"color": "#66CCFF",
|
||||
"birthday": "0712",
|
||||
},
|
||||
{
|
||||
"name": "言和",
|
||||
"color": "#00FFCC",
|
||||
"birthday": "0711",
|
||||
},
|
||||
{
|
||||
"name": "乐正绫",
|
||||
"color": "#EE0000",
|
||||
"birthday": "0412",
|
||||
},
|
||||
{
|
||||
"name": "乐正龙牙",
|
||||
"color": "#006666",
|
||||
"birthday": "1002",
|
||||
},
|
||||
{
|
||||
"name": "徵羽摩柯",
|
||||
"color": "#0080FF",
|
||||
"birthday": "1210",
|
||||
},
|
||||
{
|
||||
"name": "墨清弦",
|
||||
"color": "#FFFF00",
|
||||
"birthday": "0520",
|
||||
},
|
||||
{
|
||||
"name": "星尘",
|
||||
"color": "#9999FF",
|
||||
"birthday": "0812",
|
||||
},
|
||||
{
|
||||
"name": "心华",
|
||||
"color": "#EE82EE",
|
||||
"birthday": "0210",
|
||||
},
|
||||
{
|
||||
"name": "海伊",
|
||||
"color": "#3399FF",
|
||||
"birthday": "0722",
|
||||
},
|
||||
{
|
||||
"name": "苍穹",
|
||||
"color": "#8BC0B5",
|
||||
"birthday": "0520",
|
||||
},
|
||||
{
|
||||
"name": "赤羽",
|
||||
"color": "#FF4004",
|
||||
"birthday": "1126",
|
||||
},
|
||||
{
|
||||
"name": "诗岸",
|
||||
"color": "#F6BE72",
|
||||
"birthday": "0119",
|
||||
},
|
||||
{
|
||||
"name": "牧心",
|
||||
"color": "#2A2859",
|
||||
"birthday": "0807",
|
||||
},
|
||||
];
|
||||
|
||||
export interface Singer {
|
||||
name: string;
|
||||
color?: string;
|
||||
birthday?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export const specialSingers = [
|
||||
{
|
||||
"name": "雅音宫羽",
|
||||
"message": "你是我最真模样,从来不曾遗忘。",
|
||||
},
|
||||
{
|
||||
"name": "初音未来",
|
||||
"message": "初始之音,响彻未来!",
|
||||
},
|
||||
];
|
||||
|
||||
export const pickSinger = () => {
|
||||
const index = Math.floor(Math.random() * singers.length);
|
||||
return singers[index];
|
||||
};
|
||||
|
||||
export const pickSpecialSinger = () => {
|
||||
const index = Math.floor(Math.random() * specialSingers.length);
|
||||
return specialSingers[index];
|
||||
};
|
||||
|
||||
export const getSingerForBirthday = (): Singer[] => {
|
||||
const today = new Date();
|
||||
const month = String(today.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(today.getDate()).padStart(2, "0");
|
||||
const datestring = `${month}${day}`;
|
||||
return singers.filter((singer) => singer.birthday === datestring);
|
||||
};
|
18
packages/backend/src/main.ts
Normal file
18
packages/backend/src/main.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Hono } from "hono";
|
||||
import type { TimingVariables } from "hono/timing";
|
||||
import { startServer } from "./startServer.ts";
|
||||
import { configureRoutes } from "./routing.ts";
|
||||
import { configureMiddleWares } from "./middleware.ts";
|
||||
import { notFoundRoute } from "routes/404.ts";
|
||||
|
||||
type Variables = TimingVariables;
|
||||
const app = new Hono<{ Variables: Variables }>();
|
||||
|
||||
app.notFound(notFoundRoute);
|
||||
|
||||
configureMiddleWares(app);
|
||||
configureRoutes(app);
|
||||
|
||||
await startServer(app);
|
||||
|
||||
export const VERSION = "0.6.0";
|
24
packages/backend/src/middleware.ts
Normal file
24
packages/backend/src/middleware.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Hono } from "hono";
|
||||
import { timing } from "hono/timing";
|
||||
import { Variables } from "hono/types";
|
||||
import { pingHandler } from "routes/ping";
|
||||
import { logger } from "middleware/logger.ts";
|
||||
import { corsMiddleware } from "@/middleware/cors";
|
||||
import { contentType } from "middleware/contentType.ts";
|
||||
import { captchaMiddleware } from "middleware/captcha.ts";
|
||||
import { bodyLimitForPing } from "middleware/bodyLimits.ts";
|
||||
import { registerRateLimiter } from "middleware/rateLimiters.ts";
|
||||
import { preetifyResponse } from "middleware/preetifyResponse.ts";
|
||||
|
||||
export function configureMiddleWares(app: Hono<{ Variables: Variables }>) {
|
||||
app.use("*", corsMiddleware);
|
||||
|
||||
app.use("*", contentType);
|
||||
app.use(timing());
|
||||
app.use("*", preetifyResponse);
|
||||
app.use("*", logger({}));
|
||||
|
||||
app.post("/user", registerRateLimiter);
|
||||
app.post("/user", captchaMiddleware);
|
||||
app.all("/ping", bodyLimitForPing, ...pingHandler);
|
||||
}
|
32
packages/backend/src/routing.ts
Normal file
32
packages/backend/src/routing.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { rootHandler } from "routes";
|
||||
import { pingHandler } from "routes/ping";
|
||||
import { getUserByLoginSessionHandler, registerHandler } from "routes/user";
|
||||
import { videoInfoHandler, getSnapshotsHanlder } from "routes/video";
|
||||
import { Hono } from "hono";
|
||||
import { Variables } from "hono/types";
|
||||
import { createCaptchaSessionHandler, verifyChallengeHandler } from "routes/captcha";
|
||||
import { getCaptchaDifficultyHandler } from "routes/captcha/difficulty/GET.ts";
|
||||
import { getVideosHanlder } from "@/routes/videos";
|
||||
import { loginHandler } from "@/routes/login/session/POST";
|
||||
import { logoutHandler } from "@/routes/session";
|
||||
|
||||
export function configureRoutes(app: Hono<{ Variables: Variables }>) {
|
||||
app.get("/", ...rootHandler);
|
||||
app.all("/ping", ...pingHandler);
|
||||
|
||||
app.get("/videos", ...getVideosHanlder);
|
||||
|
||||
app.get("/video/:id/snapshots", ...getSnapshotsHanlder);
|
||||
app.get("/video/:id/info", ...videoInfoHandler);
|
||||
|
||||
app.post("/login/session", ...loginHandler);
|
||||
|
||||
app.delete("/session/:id", ...logoutHandler);
|
||||
|
||||
app.post("/user", ...registerHandler);
|
||||
app.get("/user/session/:id", ...getUserByLoginSessionHandler);
|
||||
|
||||
app.post("/captcha/session", ...createCaptchaSessionHandler);
|
||||
app.get("/captcha/:id/result", ...verifyChallengeHandler);
|
||||
app.get("/captcha/difficulty", ...getCaptchaDifficultyHandler);
|
||||
}
|
66
packages/backend/src/schema.d.ts
vendored
Normal file
66
packages/backend/src/schema.d.ts
vendored
Normal file
@ -0,0 +1,66 @@
|
||||
export type ErrorCode =
|
||||
| "INVALID_QUERY_PARAMS"
|
||||
| "UNKNOWN_ERROR"
|
||||
| "INVALID_PAYLOAD"
|
||||
| "INVALID_FORMAT"
|
||||
| "INVALID_HEADER"
|
||||
| "BODY_TOO_LARGE"
|
||||
| "UNAUTHORIZED"
|
||||
| "INVALID_CREDENTIALS"
|
||||
| "ENTITY_NOT_FOUND"
|
||||
| "SERVER_ERROR"
|
||||
| "RATE_LIMIT_EXCEEDED"
|
||||
| "ENTITY_EXISTS";
|
||||
|
||||
export interface ErrorResponse<E = string> {
|
||||
code: ErrorCode;
|
||||
message: string;
|
||||
errors: E[] = [];
|
||||
i18n?: {
|
||||
key: string;
|
||||
values?: {
|
||||
[key: string]: string | number | Date;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface StatusResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type CaptchaSessionResponse = ErrorResponse | CaptchaSessionRawResponse;
|
||||
|
||||
interface CaptchaSessionRawResponse {
|
||||
success: boolean;
|
||||
id: string;
|
||||
g: string;
|
||||
n: string;
|
||||
t: number;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
uid: number;
|
||||
username: string;
|
||||
nickname: string | null;
|
||||
role: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface SignUpResponse {
|
||||
username: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface UserResponse {
|
||||
username: string;
|
||||
nickname: string | null;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export type CaptchaVerificationRawResponse = {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export type CaptchaVerificationResponse =
|
||||
| ErrorResponse
|
||||
| CaptchaVerificationRawResponse;
|
91
packages/backend/src/startServer.ts
Normal file
91
packages/backend/src/startServer.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import { serve } from "bun";
|
||||
import { Hono } from "hono";
|
||||
import os from "os";
|
||||
import { BlankSchema, Variables } from "hono/types";
|
||||
|
||||
function getLocalIpAddress(): string {
|
||||
const interfaces = os.networkInterfaces();
|
||||
for (const name of Object.keys(interfaces)) {
|
||||
for (const iface of interfaces[name]!) {
|
||||
if (iface.family === "IPv4" && !iface.internal) {
|
||||
return iface.address;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "localhost";
|
||||
}
|
||||
|
||||
function logStartup(hostname: string, port: number, wasAutoIncremented: boolean, originalPort: number) {
|
||||
const localUrl = `http://localhost:${port}`;
|
||||
const networkIp = hostname === "0.0.0.0" ? getLocalIpAddress() : "";
|
||||
const networkUrl = networkIp ? `http://${networkIp}:${port}` : "";
|
||||
|
||||
console.log("\n");
|
||||
console.log("🚀 Server is running at:");
|
||||
console.log(`> Local: ${localUrl}`);
|
||||
if (networkIp) {
|
||||
console.log(`> Network: ${networkUrl}`);
|
||||
}
|
||||
if (wasAutoIncremented) {
|
||||
console.log(`\n⚠️ Port ${originalPort} is in use, using port ${port} instead.`);
|
||||
}
|
||||
console.log("\nPress Ctrl+C to quit.");
|
||||
}
|
||||
|
||||
export async function startServer(app: Hono<{ Variables: Variables }>) {
|
||||
const NODE_ENV = process.env.NODE_ENV || "production";
|
||||
const HOST = process.env.HOST ?? (NODE_ENV === "development" ? "0.0.0.0" : "127.0.0.1");
|
||||
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
|
||||
|
||||
const DEFAULT_PORT = 3000;
|
||||
const MAX_ATTEMPTS = 15;
|
||||
|
||||
if (PORT !== undefined) {
|
||||
try {
|
||||
const server = serve({
|
||||
fetch: app.fetch,
|
||||
hostname: HOST,
|
||||
port: PORT
|
||||
});
|
||||
logStartup(HOST, PORT, false, DEFAULT_PORT);
|
||||
return server;
|
||||
} catch (e: any) {
|
||||
console.error(`Failed to start server on port ${PORT}:`, e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
let attemptPort = DEFAULT_PORT;
|
||||
let success = false;
|
||||
let error: unknown = null;
|
||||
|
||||
for (let i = 0; i <= MAX_ATTEMPTS; i++) {
|
||||
try {
|
||||
const server = serve({
|
||||
fetch: app.fetch,
|
||||
hostname: HOST,
|
||||
port: attemptPort
|
||||
});
|
||||
|
||||
const wasAutoIncremented = attemptPort !== DEFAULT_PORT;
|
||||
|
||||
logStartup(HOST, attemptPort, wasAutoIncremented, DEFAULT_PORT);
|
||||
return server;
|
||||
} catch (e: any) {
|
||||
if (e.code === "EADDRINUSE") {
|
||||
attemptPort++;
|
||||
} else {
|
||||
error = e;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
console.error(`Could not find an available port after ${MAX_ATTEMPTS + 1} attempts.`);
|
||||
if (error) {
|
||||
console.error("Last error:", (error as Error).message);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
1
packages/backend/src/types.d.ts
vendored
Normal file
1
packages/backend/src/types.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export * from "./schema";
|
20
packages/backend/tsconfig.json
Normal file
20
packages/backend/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"include": ["**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@core/*": ["../core/*"],
|
||||
"@/*": ["./*"],
|
||||
"@crawler/*": ["../crawler/*"]
|
||||
},
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
104
packages/core/bun.lock
Normal file
104
packages/core/bun.lock
Normal file
@ -0,0 +1,104 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"chalk": "^5.4.1",
|
||||
"ioredis": "^5.6.1",
|
||||
"logform": "^2.7.0",
|
||||
"postgres": "^3.4.5",
|
||||
"winston": "^3.17.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ioredis": "^5.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="],
|
||||
|
||||
"@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="],
|
||||
|
||||
"@ioredis/commands": ["@ioredis/commands@1.2.0", "", {}, "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="],
|
||||
|
||||
"@types/ioredis": ["@types/ioredis@5.0.0", "", { "dependencies": { "ioredis": "*" } }, "sha512-zJbJ3FVE17CNl5KXzdeSPtdltc4tMT3TzC6fxQS0sQngkbFZ6h+0uTafsRqu+eSLIugf6Yb0Ea0SUuRr42Nk9g=="],
|
||||
|
||||
"@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="],
|
||||
|
||||
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
||||
|
||||
"chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
|
||||
|
||||
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
|
||||
|
||||
"color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="],
|
||||
|
||||
"color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
||||
|
||||
"color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
|
||||
|
||||
"color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
|
||||
|
||||
"colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="],
|
||||
|
||||
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
||||
|
||||
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
|
||||
|
||||
"enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="],
|
||||
|
||||
"fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="],
|
||||
|
||||
"fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ioredis": ["ioredis@5.6.1", "", { "dependencies": { "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA=="],
|
||||
|
||||
"is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
|
||||
|
||||
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
|
||||
"kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="],
|
||||
|
||||
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
|
||||
|
||||
"lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="],
|
||||
|
||||
"logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="],
|
||||
|
||||
"postgres": ["postgres@3.4.5", "", {}, "sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg=="],
|
||||
|
||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
|
||||
|
||||
"redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
|
||||
|
||||
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
|
||||
|
||||
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
|
||||
|
||||
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="],
|
||||
|
||||
"triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="],
|
||||
|
||||
"winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="],
|
||||
}
|
||||
}
|
5
packages/core/const/time.ts
Normal file
5
packages/core/const/time.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export const SECOND = 1000;
|
||||
export const MINUTE = 60 * SECOND;
|
||||
export const HOUR = 60 * MINUTE;
|
||||
export const DAY = 24 * HOUR;
|
||||
export const WEEK = 7 * DAY;
|
8
packages/core/db/dbNew.ts
Normal file
8
packages/core/db/dbNew.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import postgres from "postgres";
|
||||
import { postgresConfigCred, postgresConfig } from "./pgConfigNew";
|
||||
|
||||
export const sql = postgres(postgresConfig);
|
||||
|
||||
export const sqlCred = postgres(postgresConfigCred);
|
||||
|
||||
export const sqlTest = postgres(postgresConfig);
|
@ -1,30 +0,0 @@
|
||||
const requiredEnvVars = ["DB_HOST", "DB_NAME", "DB_USER", "DB_PASSWORD", "DB_PORT"];
|
||||
|
||||
const unsetVars = requiredEnvVars.filter((key) => Deno.env.get(key) === undefined);
|
||||
|
||||
if (unsetVars.length > 0) {
|
||||
throw new Error(`Missing required environment variables: ${unsetVars.join(", ")}`);
|
||||
}
|
||||
|
||||
const databaseHost = Deno.env.get("DB_HOST")!;
|
||||
const databaseName = Deno.env.get("DB_NAME")!;
|
||||
const databaseNameCred = Deno.env.get("DB_NAME_CRED")!;
|
||||
const databaseUser = Deno.env.get("DB_USER")!;
|
||||
const databasePassword = Deno.env.get("DB_PASSWORD")!;
|
||||
const databasePort = Deno.env.get("DB_PORT")!;
|
||||
|
||||
export const postgresConfig = {
|
||||
hostname: databaseHost,
|
||||
port: parseInt(databasePort),
|
||||
database: databaseName,
|
||||
user: databaseUser,
|
||||
password: databasePassword,
|
||||
};
|
||||
|
||||
export const postgresConfigCred = {
|
||||
hostname: databaseHost,
|
||||
port: parseInt(databasePort),
|
||||
database: databaseNameCred,
|
||||
user: databaseUser,
|
||||
password: databasePassword,
|
||||
};
|
34
packages/core/db/pgConfigNew.ts
Normal file
34
packages/core/db/pgConfigNew.ts
Normal file
@ -0,0 +1,34 @@
|
||||
const requiredEnvVars = ["DB_HOST", "DB_NAME", "DB_USER", "DB_PASSWORD", "DB_PORT", "DB_NAME_CRED"];
|
||||
|
||||
const getEnvVar = (key: string) => {
|
||||
return process.env[key] || import.meta.env[key];
|
||||
};
|
||||
|
||||
const unsetVars = requiredEnvVars.filter((key) => getEnvVar(key) === undefined);
|
||||
|
||||
if (unsetVars.length > 0) {
|
||||
throw new Error(`Missing required environment variables: ${unsetVars.join(", ")}`);
|
||||
}
|
||||
|
||||
const databaseHost = getEnvVar("DB_HOST")!;
|
||||
const databaseName = getEnvVar("DB_NAME");
|
||||
const databaseNameCred = getEnvVar("DB_NAME_CRED")!;
|
||||
const databaseUser = getEnvVar("DB_USER")!;
|
||||
const databasePassword = getEnvVar("DB_PASSWORD")!;
|
||||
const databasePort = getEnvVar("DB_PORT")!;
|
||||
|
||||
export const postgresConfig = {
|
||||
host: databaseHost,
|
||||
port: parseInt(databasePort),
|
||||
database: databaseName,
|
||||
username: databaseUser,
|
||||
password: databasePassword
|
||||
};
|
||||
|
||||
export const postgresConfigCred = {
|
||||
hostname: databaseHost,
|
||||
port: parseInt(databasePort),
|
||||
database: databaseNameCred,
|
||||
user: databaseUser,
|
||||
password: databasePassword
|
||||
};
|
3
packages/core/db/psql.d.ts
vendored
Normal file
3
packages/core/db/psql.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import type postgres from "postgres";
|
||||
|
||||
export type Psql = postgres.Sql;
|
@ -1,3 +1,10 @@
|
||||
import { Redis } from "ioredis";
|
||||
|
||||
export const redis = new Redis({ maxRetriesPerRequest: null });
|
||||
const host = process.env.REDIS_HOST || "localhost";
|
||||
const port = parseInt(process.env.REDIS_PORT) || 6379;
|
||||
|
||||
export const redis = new Redis({
|
||||
port: port,
|
||||
host: host,
|
||||
maxRetriesPerRequest: null,
|
||||
});
|
||||
|
52
packages/core/db/schema.d.ts
vendored
52
packages/core/db/schema.d.ts
vendored
@ -1,16 +1,3 @@
|
||||
export interface AllDataType {
|
||||
id: number;
|
||||
aid: bigint;
|
||||
bvid: string | null;
|
||||
description: string | null;
|
||||
uid: number | null;
|
||||
tags: string | null;
|
||||
title: string | null;
|
||||
published_at: string | null;
|
||||
duration: number;
|
||||
created_at: string | null;
|
||||
}
|
||||
|
||||
export interface BiliUserType {
|
||||
id: number;
|
||||
uid: number;
|
||||
@ -21,19 +8,19 @@ export interface BiliUserType {
|
||||
|
||||
export interface VideoSnapshotType {
|
||||
id: number;
|
||||
created_at: string;
|
||||
created_at: Date;
|
||||
views: number;
|
||||
coins: number;
|
||||
likes: number;
|
||||
favorites: number;
|
||||
shares: number;
|
||||
danmakus: number;
|
||||
aid: bigint;
|
||||
aid: number;
|
||||
replies: number;
|
||||
}
|
||||
|
||||
export interface LatestSnapshotType {
|
||||
aid: bigint;
|
||||
aid: number;
|
||||
time: number;
|
||||
views: number;
|
||||
danmakus: number;
|
||||
@ -46,10 +33,35 @@ export interface LatestSnapshotType {
|
||||
|
||||
export interface SnapshotScheduleType {
|
||||
id: number;
|
||||
aid: bigint;
|
||||
aid: number;
|
||||
type?: string;
|
||||
created_at: string;
|
||||
started_at?: string;
|
||||
finished_at?: string;
|
||||
created_at: Date;
|
||||
started_at?: Date;
|
||||
finished_at?: Date;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface UserType {
|
||||
id: number;
|
||||
username: string;
|
||||
nickname: string | null;
|
||||
password: string;
|
||||
unq_id: string;
|
||||
role: string;
|
||||
created_at: Date;
|
||||
}
|
||||
|
||||
export interface BiliVideoMetadataType {
|
||||
id: number;
|
||||
aid: number;
|
||||
bvid: string | null;
|
||||
description: string | null;
|
||||
uid: number | null;
|
||||
tags: string | null;
|
||||
title: string | null;
|
||||
published_at: Date | null;
|
||||
duration: number | null;
|
||||
created_at: Date;
|
||||
status: number;
|
||||
cover_url: string | null;
|
||||
}
|
||||
|
@ -1,62 +0,0 @@
|
||||
import type { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
|
||||
import type { VideoSnapshotType } from "./schema.d.ts";
|
||||
|
||||
export async function getVideoSnapshots(
|
||||
client: Client,
|
||||
aid: number,
|
||||
limit: number,
|
||||
pageOrOffset: number,
|
||||
reverse: boolean,
|
||||
mode: "page" | "offset" = "page",
|
||||
) {
|
||||
const offset = mode === "page" ? (pageOrOffset - 1) * limit : pageOrOffset;
|
||||
const queryDesc: string = `
|
||||
SELECT *
|
||||
FROM video_snapshot
|
||||
WHERE aid = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2
|
||||
OFFSET $3
|
||||
`;
|
||||
const queryAsc: string = `
|
||||
SELECT *
|
||||
FROM video_snapshot
|
||||
WHERE aid = $1
|
||||
ORDER BY created_at
|
||||
LIMIT $2 OFFSET $3
|
||||
`;
|
||||
const query = reverse ? queryAsc : queryDesc;
|
||||
const queryResult = await client.queryObject<VideoSnapshotType>(query, [aid, limit, offset]);
|
||||
return queryResult.rows;
|
||||
}
|
||||
|
||||
export async function getVideoSnapshotsByBV(
|
||||
client: Client,
|
||||
bv: string,
|
||||
limit: number,
|
||||
pageOrOffset: number,
|
||||
reverse: boolean,
|
||||
mode: "page" | "offset" = "page",
|
||||
) {
|
||||
const offset = mode === "page" ? (pageOrOffset - 1) * limit : pageOrOffset;
|
||||
const queryAsc = `
|
||||
SELECT vs.*
|
||||
FROM video_snapshot vs
|
||||
JOIN bilibili_metadata bm ON vs.aid = bm.aid
|
||||
WHERE bm.bvid = $1
|
||||
ORDER BY vs.created_at
|
||||
LIMIT $2
|
||||
OFFSET $3
|
||||
`;
|
||||
const queryDesc: string = `
|
||||
SELECT *
|
||||
FROM video_snapshot vs
|
||||
JOIN bilibili_metadata bm ON vs.aid = bm.aid
|
||||
WHERE bm.bvid = $1
|
||||
ORDER BY vs.created_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
`;
|
||||
const query = reverse ? queryAsc : queryDesc;
|
||||
const queryResult = await client.queryObject<VideoSnapshotType>(query, [bv, limit, offset]);
|
||||
return queryResult.rows;
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"name": "@cvsa/core",
|
||||
"exports": "./main.ts",
|
||||
"imports": {
|
||||
"ioredis": "npm:ioredis",
|
||||
"log/": "./log/",
|
||||
"db/": "./db/",
|
||||
"$std/": "https://deno.land/std@0.216.0/",
|
||||
"mq/": "./mq/",
|
||||
"chalk": "npm:chalk"
|
||||
}
|
||||
}
|
1
packages/core/index.ts
Normal file
1
packages/core/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./db/dbNew";
|
15
packages/core/lib/randomID.ts
Normal file
15
packages/core/lib/randomID.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export function generateRandomId(length: number): string {
|
||||
const characters = 'abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||
const charactersLength = characters.length;
|
||||
const randomBytes = new Uint8Array(length);
|
||||
|
||||
crypto.getRandomValues(randomBytes);
|
||||
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
const randomIndex = randomBytes[i] % charactersLength;
|
||||
result += characters.charAt(randomIndex);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import winston, { format, transports } from "npm:winston";
|
||||
import type { TransformableInfo } from "npm:logform";
|
||||
import winston, { format, transports } from "winston";
|
||||
import type { TransformableInfo } from "logform";
|
||||
import chalk from "chalk";
|
||||
|
||||
const customFormat = format.printf((info: TransformableInfo) => {
|
||||
@ -24,13 +24,13 @@ const createTransport = (level: string, filename: string) => {
|
||||
let maxsize = undefined;
|
||||
let maxFiles = undefined;
|
||||
let tailable = undefined;
|
||||
if (level === "verbose") {
|
||||
maxsize = 10 * MB;
|
||||
maxFiles = 10;
|
||||
if (level === "silly") {
|
||||
maxsize = 500 * MB;
|
||||
maxFiles = undefined;
|
||||
tailable = false;
|
||||
} else if (level === "warn") {
|
||||
maxsize = 10 * MB;
|
||||
maxFiles = 1;
|
||||
maxFiles = 5;
|
||||
tailable = false;
|
||||
}
|
||||
function replacer(key: unknown, value: unknown) {
|
||||
@ -52,9 +52,9 @@ const createTransport = (level: string, filename: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
const sillyLogPath = Deno.env.get("LOG_VERBOSE") ?? "logs/verbose.log";
|
||||
const warnLogPath = Deno.env.get("LOG_WARN") ?? "logs/warn.log";
|
||||
const errorLogPath = Deno.env.get("LOG_ERROR") ?? "logs/error.log";
|
||||
const sillyLogPath = process.env["LOG_VERBOSE"] ?? "logs/verbose.log";
|
||||
const warnLogPath = process.env["LOG_WARN"] ?? "logs/warn.log";
|
||||
const errorLogPath = process.env["LOG_ERROR"] ?? "logs/error.log";
|
||||
|
||||
const winstonLogger = winston.createLogger({
|
||||
levels: winston.config.npm.levels,
|
||||
@ -62,7 +62,7 @@ const winstonLogger = winston.createLogger({
|
||||
new transports.Console({
|
||||
level: "debug",
|
||||
format: format.combine(
|
||||
format.timestamp({ format: "HH:mm:ss.SSS" }),
|
||||
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" }),
|
||||
format.colorize(),
|
||||
format.errors({ stack: true }),
|
||||
customFormat,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import logger from "log/logger.ts";
|
||||
import logger from "@core/log/logger.ts";
|
||||
|
||||
logger.error(Error("test error"), "test service");
|
||||
logger.debug(`some string`);
|
||||
|
@ -1 +0,0 @@
|
||||
export const DB_VERSION = 10;
|
@ -1,5 +1,5 @@
|
||||
import { Redis } from "ioredis";
|
||||
import { redis } from "../../core/db/redis.ts";
|
||||
import { redis } from "@core/db/redis.ts";
|
||||
|
||||
class LockManager {
|
||||
private redis: Redis;
|
55
packages/core/mq/multipleRateLimiter.ts
Normal file
55
packages/core/mq/multipleRateLimiter.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { RateLimiter as Limiter } from "@koshnic/ratelimit";
|
||||
import { redis } from "@core/db/redis.ts";
|
||||
|
||||
export interface RateLimiterConfig {
|
||||
duration: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
export class RateLimiterError extends Error {
|
||||
public code: string;
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "RateLimiterError";
|
||||
this.code = "RATE_LIMIT_EXCEEDED";
|
||||
}
|
||||
}
|
||||
|
||||
export class MultipleRateLimiter {
|
||||
private readonly name: string;
|
||||
private readonly configs: RateLimiterConfig[] = [];
|
||||
private readonly limiter: Limiter;
|
||||
|
||||
/*
|
||||
* @param name The name of the rate limiter
|
||||
* @param configs The configuration of the rate limiter, containing:
|
||||
* - duration: The duration of window in seconds
|
||||
* - max: The maximum number of tokens allowed in the window
|
||||
*/
|
||||
constructor(
|
||||
name: string,
|
||||
configs: RateLimiterConfig[]
|
||||
) {
|
||||
this.configs = configs;
|
||||
this.limiter = new Limiter(redis);
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/*
|
||||
* Trigger an event in the rate limiter
|
||||
*/
|
||||
async trigger(shouldThrow = true): Promise<void> {
|
||||
for (let i = 0; i < this.configs.length; i++) {
|
||||
const { duration, max } = this.configs[i];
|
||||
const { allowed } = await this.limiter.allow(`cvsa:${this.name}_${i}`, {
|
||||
burst: max,
|
||||
ratePerPeriod: max,
|
||||
period: duration,
|
||||
cost: 1
|
||||
});
|
||||
if (!allowed && shouldThrow) {
|
||||
throw new RateLimiterError("Rate limit exceeded")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
import { SlidingWindow } from "./slidingWindow.ts";
|
||||
|
||||
export interface RateLimiterConfig {
|
||||
window: SlidingWindow;
|
||||
max: number;
|
||||
}
|
||||
|
||||
export class RateLimiter {
|
||||
private readonly configs: RateLimiterConfig[];
|
||||
private readonly configEventNames: string[];
|
||||
|
||||
/*
|
||||
* @param name The name of the rate limiter
|
||||
* @param configs The configuration of the rate limiter, containing:
|
||||
* - window: The sliding window to use
|
||||
* - max: The maximum number of events allowed in the window
|
||||
*/
|
||||
constructor(name: string, configs: RateLimiterConfig[]) {
|
||||
this.configs = configs;
|
||||
this.configEventNames = configs.map((_, index) => `${name}_config_${index}`);
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if the event has reached the rate limit
|
||||
*/
|
||||
async getAvailability(): Promise<boolean> {
|
||||
for (let i = 0; i < this.configs.length; i++) {
|
||||
const config = this.configs[i];
|
||||
const eventName = this.configEventNames[i];
|
||||
const count = await config.window.count(eventName);
|
||||
if (count >= config.max) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Trigger an event in the rate limiter
|
||||
*/
|
||||
async trigger(): Promise<void> {
|
||||
for (let i = 0; i < this.configs.length; i++) {
|
||||
const config = this.configs[i];
|
||||
const eventName = this.configEventNames[i];
|
||||
await config.window.event(eventName);
|
||||
}
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
for (let i = 0; i < this.configs.length; i++) {
|
||||
const config = this.configs[i];
|
||||
const eventName = this.configEventNames[i];
|
||||
await config.window.clear(eventName);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { Redis } from "ioredis";
|
||||
import type { Redis } from "ioredis";
|
||||
|
||||
export class SlidingWindow {
|
||||
private redis: Redis;
|
||||
@ -32,14 +32,20 @@ export class SlidingWindow {
|
||||
|
||||
/*
|
||||
* Count the number of events in the sliding window
|
||||
* @param eventName The name of the event
|
||||
* @param {string} eventName The name of the event
|
||||
* @param {number} [duration] The duration of the window in seconds
|
||||
*/
|
||||
async count(eventName: string): Promise<number> {
|
||||
async count(eventName: string, duration?: number): Promise<number> {
|
||||
const key = `cvsa:sliding_window:${eventName}`;
|
||||
const now = Date.now();
|
||||
|
||||
// Remove timestamps outside the window
|
||||
await this.redis.zremrangebyscore(key, 0, now - this.windowSize);
|
||||
|
||||
if (duration) {
|
||||
return this.redis.zcount(key, now - duration * 1000, now);
|
||||
}
|
||||
|
||||
// Get the number of timestamps in the window
|
||||
return this.redis.zcard(key);
|
||||
}
|
||||
@ -48,4 +54,4 @@ export class SlidingWindow {
|
||||
const key = `cvsa:sliding_window:${eventName}`;
|
||||
return this.redis.del(key);
|
||||
}
|
||||
}
|
||||
}
|
4
packages/core/net/bilibili.d.ts
vendored
4
packages/core/net/bilibili.d.ts
vendored
@ -38,6 +38,10 @@ interface VideoInfoData {
|
||||
ctime: number;
|
||||
desc: string;
|
||||
desc_v2: string;
|
||||
tname: string;
|
||||
tid: number;
|
||||
tid_v2: number;
|
||||
tname_v2: string;
|
||||
state: number;
|
||||
duration: number;
|
||||
owner: {
|
||||
|
@ -1,9 +1,41 @@
|
||||
import logger from "log/logger.ts";
|
||||
import { RateLimiter, type RateLimiterConfig } from "mq/rateLimiter.ts";
|
||||
import { SlidingWindow } from "mq/slidingWindow.ts";
|
||||
import { redis } from "db/redis.ts";
|
||||
import Redis from "ioredis";
|
||||
import { SECOND } from "$std/datetime/constants.ts";
|
||||
import logger from "@core/log/logger.ts";
|
||||
import { MultipleRateLimiter, RateLimiterError, type RateLimiterConfig } from "@core/mq/multipleRateLimiter.ts";
|
||||
import { ReplyError } from "ioredis";
|
||||
import { SECOND } from "@core/const/time.ts";
|
||||
import { spawn, SpawnOptions } from "child_process";
|
||||
|
||||
export function spawnPromise(
|
||||
command: string,
|
||||
args: string[] = [],
|
||||
options?: SpawnOptions,
|
||||
): Promise<{ stdout: string; stderr: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, options);
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
child.stdout?.on("data", (data) => {
|
||||
stdout += data;
|
||||
});
|
||||
|
||||
child.stderr?.on("data", (data) => {
|
||||
stderr += data;
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`Error code: ${code}\nstderr: ${stderr}`));
|
||||
} else {
|
||||
resolve({ stdout, stderr });
|
||||
}
|
||||
});
|
||||
|
||||
child.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
interface Proxy {
|
||||
type: string;
|
||||
@ -39,11 +71,11 @@ export class NetSchedulerError extends Error {
|
||||
}
|
||||
|
||||
type LimiterMap = {
|
||||
[name: string]: RateLimiter;
|
||||
[name: string]: MultipleRateLimiter;
|
||||
};
|
||||
|
||||
type OptionalLimiterMap = {
|
||||
[name: string]: RateLimiter | null;
|
||||
[name: string]: MultipleRateLimiter | null;
|
||||
};
|
||||
|
||||
type TaskMap = {
|
||||
@ -87,20 +119,23 @@ class NetworkDelegate {
|
||||
const proxies = this.getTaskProxies(taskName);
|
||||
for (const proxyName of proxies) {
|
||||
const limiterId = "proxy-" + proxyName + "-" + taskName;
|
||||
this.proxyLimiters[limiterId] = config ? new RateLimiter(limiterId, config) : null;
|
||||
this.proxyLimiters[limiterId] = config ? new MultipleRateLimiter(limiterId, config) : null;
|
||||
}
|
||||
}
|
||||
|
||||
async triggerLimiter(task: string, proxy: string): Promise<void> {
|
||||
async triggerLimiter(task: string, proxy: string, force: boolean = false): Promise<void> {
|
||||
const limiterId = "proxy-" + proxy + "-" + task;
|
||||
const providerLimiterId = "provider-" + proxy + "-" + this.tasks[task].provider;
|
||||
try {
|
||||
await this.proxyLimiters[limiterId]?.trigger();
|
||||
await this.providerLimiters[providerLimiterId]?.trigger();
|
||||
await this.proxyLimiters[limiterId]?.trigger(!force);
|
||||
await this.providerLimiters[providerLimiterId]?.trigger(!force);
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
if (e instanceof Redis.ReplyError) {
|
||||
if (e instanceof ReplyError) {
|
||||
logger.error(error, "redis");
|
||||
} else if (e instanceof RateLimiterError) {
|
||||
// Re-throw it to ensure this.request can catch it
|
||||
throw e;
|
||||
}
|
||||
logger.warn(`Unhandled error: ${error.message}`, "mq", "proxyRequest");
|
||||
}
|
||||
@ -115,7 +150,7 @@ class NetworkDelegate {
|
||||
}
|
||||
for (const proxyName of bindProxies) {
|
||||
const limiterId = "provider-" + proxyName + "-" + providerName;
|
||||
this.providerLimiters[limiterId] = new RateLimiter(limiterId, config);
|
||||
this.providerLimiters[limiterId] = new MultipleRateLimiter(limiterId, config);
|
||||
}
|
||||
}
|
||||
|
||||
@ -134,9 +169,15 @@ class NetworkDelegate {
|
||||
// find a available proxy
|
||||
const proxiesNames = this.getTaskProxies(task);
|
||||
for (const proxyName of shuffleArray(proxiesNames)) {
|
||||
if (await this.getProxyAvailability(proxyName, task)) {
|
||||
try {
|
||||
return await this.proxyRequest<R>(url, proxyName, task, method);
|
||||
}
|
||||
catch (e) {
|
||||
if (e instanceof RateLimiterError) {
|
||||
continue;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
throw new NetSchedulerError("No proxy is available currently.", "NO_PROXY_AVAILABLE");
|
||||
}
|
||||
@ -168,16 +209,8 @@ class NetworkDelegate {
|
||||
throw new NetSchedulerError(`Proxy "${proxyName}" not found`, "PROXY_NOT_FOUND");
|
||||
}
|
||||
|
||||
if (!force) {
|
||||
const isAvailable = await this.getProxyAvailability(proxyName, task);
|
||||
const limiter = "proxy-" + proxyName + "-" + task;
|
||||
if (!isAvailable) {
|
||||
throw new NetSchedulerError(`Proxy "${limiter}" is rate limited`, "PROXY_RATE_LIMITED");
|
||||
}
|
||||
}
|
||||
|
||||
await this.triggerLimiter(task, proxyName, force);
|
||||
const result = await this.makeRequest<R>(url, proxy, method);
|
||||
await this.triggerLimiter(task, proxyName);
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -192,32 +225,6 @@ class NetworkDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private async getProxyAvailability(proxyName: string, taskName: string): Promise<boolean> {
|
||||
try {
|
||||
const task = this.tasks[taskName];
|
||||
const provider = task.provider;
|
||||
const proxyLimiterId = "proxy-" + proxyName + "-" + task;
|
||||
const providerLimiterId = "provider-" + proxyName + "-" + provider;
|
||||
if (!this.proxyLimiters[proxyLimiterId]) {
|
||||
const providerLimiter = this.providerLimiters[providerLimiterId];
|
||||
return await providerLimiter.getAvailability();
|
||||
}
|
||||
const proxyLimiter = this.proxyLimiters[proxyLimiterId];
|
||||
const providerLimiter = this.providerLimiters[providerLimiterId];
|
||||
const providerAvailable = await providerLimiter.getAvailability();
|
||||
const proxyAvailable = await proxyLimiter.getAvailability();
|
||||
return providerAvailable && proxyAvailable;
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
if (e instanceof Redis.ReplyError) {
|
||||
logger.error(error, "redis");
|
||||
return false;
|
||||
}
|
||||
logger.error(error, "mq", "getProxyAvailability");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async nativeRequest<R>(url: string, method: string): Promise<R> {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
@ -230,7 +237,7 @@ class NetworkDelegate {
|
||||
|
||||
clearTimeout(timeout);
|
||||
|
||||
return await response.json() as R;
|
||||
return (await response.json()) as R;
|
||||
} catch (e) {
|
||||
throw new NetSchedulerError("Fetch error", "FETCH_ERROR", e);
|
||||
}
|
||||
@ -238,29 +245,26 @@ class NetworkDelegate {
|
||||
|
||||
private async alicloudFcRequest<R>(url: string, region: string): Promise<R> {
|
||||
try {
|
||||
const decoder = new TextDecoder();
|
||||
const output = await new Deno.Command("aliyun", {
|
||||
args: [
|
||||
"fc",
|
||||
"POST",
|
||||
`/2023-03-30/functions/proxy-${region}/invocations`,
|
||||
"--qualifier",
|
||||
"LATEST",
|
||||
"--header",
|
||||
"Content-Type=application/json;x-fc-invocation-type=Sync;x-fc-log-type=None;",
|
||||
"--body",
|
||||
JSON.stringify({ url: url }),
|
||||
"--retry-count",
|
||||
"5",
|
||||
"--read-timeout",
|
||||
"30",
|
||||
"--connect-timeout",
|
||||
"10",
|
||||
"--profile",
|
||||
`CVSA-${region}`,
|
||||
],
|
||||
}).output();
|
||||
const out = decoder.decode(output.stdout);
|
||||
const output = await spawnPromise("aliyun", [
|
||||
"fc",
|
||||
"POST",
|
||||
`/2023-03-30/functions/proxy-${region}/invocations`,
|
||||
"--qualifier",
|
||||
"LATEST",
|
||||
"--header",
|
||||
"Content-Type=application/json;x-fc-invocation-type=Sync;x-fc-log-type=None;",
|
||||
"--body",
|
||||
JSON.stringify({ url: url }),
|
||||
"--retry-count",
|
||||
"5",
|
||||
"--read-timeout",
|
||||
"30",
|
||||
"--connect-timeout",
|
||||
"10",
|
||||
"--profile",
|
||||
`CVSA-${region}`,
|
||||
]);
|
||||
const out = output.stdout;
|
||||
const rawData = JSON.parse(out);
|
||||
if (rawData.statusCode !== 200) {
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
@ -269,11 +273,15 @@ class NetworkDelegate {
|
||||
"ALICLOUD_PROXY_ERR",
|
||||
);
|
||||
} else {
|
||||
return JSON.parse(JSON.parse(rawData.body)) as R;
|
||||
return JSON.parse(rawData.body) as R;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(e as Error, "net", "fn:alicloudFcRequest");
|
||||
throw new NetSchedulerError(`Unhandled error: Cannot proxy ${url} to ali-fc.`, "ALICLOUD_PROXY_ERR", e);
|
||||
throw new NetSchedulerError(
|
||||
`Unhandled error: Cannot proxy ${url} to ali-fc-${region}.`,
|
||||
"ALICLOUD_PROXY_ERR",
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -281,37 +289,37 @@ class NetworkDelegate {
|
||||
const networkDelegate = new NetworkDelegate();
|
||||
const videoInfoRateLimiterConfig: RateLimiterConfig[] = [
|
||||
{
|
||||
window: new SlidingWindow(redis, 0.3),
|
||||
duration: 0.3,
|
||||
max: 1,
|
||||
},
|
||||
{
|
||||
window: new SlidingWindow(redis, 3),
|
||||
duration: 3,
|
||||
max: 5,
|
||||
},
|
||||
{
|
||||
window: new SlidingWindow(redis, 30),
|
||||
duration: 30,
|
||||
max: 30,
|
||||
},
|
||||
{
|
||||
window: new SlidingWindow(redis, 2 * 60),
|
||||
duration: 2 * 60,
|
||||
max: 50,
|
||||
},
|
||||
];
|
||||
const biliLimiterConfig: RateLimiterConfig[] = [
|
||||
{
|
||||
window: new SlidingWindow(redis, 1),
|
||||
duration: 1,
|
||||
max: 6,
|
||||
},
|
||||
{
|
||||
window: new SlidingWindow(redis, 5),
|
||||
duration: 5,
|
||||
max: 20,
|
||||
},
|
||||
{
|
||||
window: new SlidingWindow(redis, 30),
|
||||
duration: 30,
|
||||
max: 100,
|
||||
},
|
||||
{
|
||||
window: new SlidingWindow(redis, 5 * 60),
|
||||
duration: 5 * 60,
|
||||
max: 200,
|
||||
},
|
||||
];
|
||||
@ -358,7 +366,11 @@ for (const region of regions) {
|
||||
}
|
||||
networkDelegate.addTask("getVideoInfo", "bilibili", "all");
|
||||
networkDelegate.addTask("getLatestVideos", "bilibili", "all");
|
||||
networkDelegate.addTask("snapshotMilestoneVideo", "bilibili", regions.map((region) => `alicloud-${region}`));
|
||||
networkDelegate.addTask(
|
||||
"snapshotMilestoneVideo",
|
||||
"bilibili",
|
||||
regions.map((region) => `alicloud-${region}`),
|
||||
);
|
||||
networkDelegate.addTask("snapshotVideo", "bili_test", [
|
||||
"alicloud-qingdao",
|
||||
"alicloud-shanghai",
|
||||
|
@ -1,6 +1,6 @@
|
||||
import networkDelegate from "@core/net/delegate.ts";
|
||||
import { VideoInfoData, VideoInfoResponse } from "@core/net/bilibili.d.ts";
|
||||
import logger from "log/logger.ts";
|
||||
import type { VideoInfoData, VideoInfoResponse } from "@core/net/bilibili.d.ts";
|
||||
import logger from "@core/log/logger.ts";
|
||||
|
||||
/*
|
||||
* Fetch video metadata from bilibili API
|
22
packages/core/package.json
Normal file
22
packages/core/package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@cvsa/core",
|
||||
"private": false,
|
||||
"version": "0.0.10",
|
||||
"scripts": {
|
||||
"test": "bun --env-file=.env.test run vitest",
|
||||
"build": "bun build ./index.ts --target node --outdir ./dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@koshnic/ratelimit": "^1.0.3",
|
||||
"chalk": "^5.4.1",
|
||||
"ioredis": "^5.6.1",
|
||||
"logform": "^2.7.0",
|
||||
"postgres": "^3.4.5",
|
||||
"winston": "^3.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ioredis": "^5.0.0"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"types": "./types.d.ts"
|
||||
}
|
18
packages/core/test/lib/randomID.test.ts
Normal file
18
packages/core/test/lib/randomID.test.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { generateRandomId } from "@core/lib/randomID.ts";
|
||||
|
||||
describe("generateRandomId", () => {
|
||||
it("should generate an ID of the specified length", () => {
|
||||
const length = 15;
|
||||
const id = generateRandomId(length);
|
||||
expect(id).toHaveLength(length);
|
||||
});
|
||||
|
||||
it("should generate an ID containing only allowed characters", () => {
|
||||
const allowedChars = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||||
const id = generateRandomId(20);
|
||||
for (const char of id) {
|
||||
expect(allowedChars).toContain(char);
|
||||
}
|
||||
});
|
||||
});
|
18
packages/core/tsconfig.json
Normal file
18
packages/core/tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"include": ["**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@core/*": ["./*"]
|
||||
},
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
3
packages/core/types.d.ts
vendored
Normal file
3
packages/core/types.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./db/schema";
|
||||
export * from "./index";
|
||||
export * from "./net/bilibili";
|
2
packages/crawler/.gitignore
vendored
Normal file
2
packages/crawler/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
.cache
|
||||
build/
|
433
packages/crawler/bun.lock
Normal file
433
packages/crawler/bun.lock
Normal file
@ -0,0 +1,433 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "core",
|
||||
"dependencies": {
|
||||
"@bull-board/api": "^6.9.5",
|
||||
"@bull-board/express": "^6.9.5",
|
||||
"@huggingface/transformers": "^3.5.1",
|
||||
"bullmq": "^5.52.1",
|
||||
"express": "^5.1.0",
|
||||
"ioredis": "^5.6.1",
|
||||
"onnxruntime-node": "1.19.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.1.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@bull-board/api": ["@bull-board/api@6.9.5", "", { "dependencies": { "redis-info": "^3.1.0" }, "peerDependencies": { "@bull-board/ui": "6.9.5" } }, "sha512-9d03Mu9fuQ3sHAxAwUmlTo8C9KwKHd6Ef2IUOZMG93cAHsQ0oaOpYc5d0roZacYFvfYKr7bTANptKO1f3GLp2g=="],
|
||||
|
||||
"@bull-board/express": ["@bull-board/express@6.9.5", "", { "dependencies": { "@bull-board/api": "6.9.5", "@bull-board/ui": "6.9.5", "ejs": "^3.1.10", "express": "^4.21.1 || ^5.0.0" } }, "sha512-N2B/RSEX/EpIvT/uOJXCACevElv2CUEqrvgoLuwM2n1Fb3VWaatj/S004VLUtHjOCPfODosJ1xoxelelQcV3Dw=="],
|
||||
|
||||
"@bull-board/ui": ["@bull-board/ui@6.9.5", "", { "dependencies": { "@bull-board/api": "6.9.5" } }, "sha512-+4YnDvyuY3MOVkXFxkspRbqawLtIExHzRQ4raQWagOc35KD7v2/ccFGyRPDI/N0bemsiNkOPkcZGf/LFcaOZmA=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
|
||||
|
||||
"@huggingface/jinja": ["@huggingface/jinja@0.4.1", "", {}, "sha512-3WXbMFaPkk03LRCM0z0sylmn8ddDm4ubjU7X+Hg4M2GOuMklwoGAFXp9V2keq7vltoB/c7McE5aHUVVddAewsw=="],
|
||||
|
||||
"@huggingface/transformers": ["@huggingface/transformers@3.5.1", "", { "dependencies": { "@huggingface/jinja": "^0.4.1", "onnxruntime-node": "1.21.0", "onnxruntime-web": "1.22.0-dev.20250409-89f8206ba4", "sharp": "^0.34.1" } }, "sha512-qWsPoJMBPYcrGuzRMVL//3dwcLXED9r+j+Hzw+hZpBfrMPdyq57ehNs7lew0D34Paj0DO0E0kZQjOZcIVN17hQ=="],
|
||||
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.1.0" }, "os": "darwin", "cpu": "arm64" }, "sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A=="],
|
||||
|
||||
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.1.0" }, "os": "darwin", "cpu": "x64" }, "sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.1.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.1.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.1.0", "", { "os": "linux", "cpu": "arm" }, "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew=="],
|
||||
|
||||
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.1.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ=="],
|
||||
|
||||
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.1.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A=="],
|
||||
|
||||
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.1.0" }, "os": "linux", "cpu": "arm" }, "sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA=="],
|
||||
|
||||
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ=="],
|
||||
|
||||
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.1.0" }, "os": "linux", "cpu": "s390x" }, "sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA=="],
|
||||
|
||||
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA=="],
|
||||
|
||||
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ=="],
|
||||
|
||||
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg=="],
|
||||
|
||||
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.1", "", { "dependencies": { "@emnapi/runtime": "^1.4.0" }, "cpu": "none" }, "sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg=="],
|
||||
|
||||
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw=="],
|
||||
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.1", "", { "os": "win32", "cpu": "x64" }, "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw=="],
|
||||
|
||||
"@ioredis/commands": ["@ioredis/commands@1.2.0", "", {}, "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="],
|
||||
|
||||
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="],
|
||||
|
||||
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
|
||||
|
||||
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
|
||||
|
||||
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="],
|
||||
|
||||
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="],
|
||||
|
||||
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="],
|
||||
|
||||
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
|
||||
|
||||
"@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="],
|
||||
|
||||
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
|
||||
|
||||
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
|
||||
|
||||
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
|
||||
|
||||
"@types/node": ["@types/node@22.15.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw=="],
|
||||
|
||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
|
||||
|
||||
"boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
||||
|
||||
"bullmq": ["bullmq@5.52.1", "", { "dependencies": { "cron-parser": "^4.9.0", "ioredis": "^5.4.1", "msgpackr": "^1.11.2", "node-abort-controller": "^3.1.1", "semver": "^7.5.4", "tslib": "^2.0.0", "uuid": "^9.0.0" } }, "sha512-u7CSV9wID3MBEX2DNubEErbAlrADgm8abUBAi6h8rQTnuTkhhgMs2iD7uhqplK8lIgUOkBIW3sDJWaMSInH47A=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
||||
|
||||
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||
|
||||
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
|
||||
|
||||
"color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"concurrently": ["concurrently@9.1.2", "", { "dependencies": { "chalk": "^4.1.2", "lodash": "^4.17.21", "rxjs": "^7.8.1", "shell-quote": "^1.8.1", "supports-color": "^8.1.1", "tree-kill": "^1.2.2", "yargs": "^17.7.2" }, "bin": { "concurrently": "dist/bin/concurrently.js", "conc": "dist/bin/concurrently.js" } }, "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ=="],
|
||||
|
||||
"content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="],
|
||||
|
||||
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||
|
||||
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||
|
||||
"cron-parser": ["cron-parser@4.9.0", "", { "dependencies": { "luxon": "^3.2.1" } }, "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q=="],
|
||||
|
||||
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
||||
|
||||
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
|
||||
|
||||
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
||||
|
||||
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
|
||||
|
||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
|
||||
|
||||
"detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
||||
"express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
|
||||
|
||||
"filelist": ["filelist@1.0.4", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q=="],
|
||||
|
||||
"finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="],
|
||||
|
||||
"flatbuffers": ["flatbuffers@25.2.10", "", {}, "sha512-7JlN9ZvLDG1McO3kbX0k4v+SUAg48L1rIwEvN6ZQl/eCtgJz9UylTMzE9wrmYrcorgxm3CX/3T/w5VAub99UUw=="],
|
||||
|
||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
|
||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="],
|
||||
|
||||
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"guid-typescript": ["guid-typescript@1.0.9", "", {}, "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ioredis": ["ioredis@5.6.1", "", { "dependencies": { "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||
|
||||
"jake": ["jake@10.9.2", "", { "dependencies": { "async": "^3.2.3", "chalk": "^4.0.2", "filelist": "^1.0.4", "minimatch": "^3.1.2" }, "bin": { "jake": "bin/cli.js" } }, "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA=="],
|
||||
|
||||
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
|
||||
|
||||
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
|
||||
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
|
||||
|
||||
"lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="],
|
||||
|
||||
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
||||
|
||||
"luxon": ["luxon@3.6.1", "", {}, "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ=="],
|
||||
|
||||
"matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||
|
||||
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="],
|
||||
|
||||
"mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"msgpackr": ["msgpackr@1.11.2", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g=="],
|
||||
|
||||
"msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="],
|
||||
|
||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"node-abort-controller": ["node-abort-controller@3.1.1", "", {}, "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="],
|
||||
|
||||
"node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"onnxruntime-common": ["onnxruntime-common@1.19.2", "", {}, "sha512-a4R7wYEVFbZBlp0BfhpbFWqe4opCor3KM+5Wm22Az3NGDcQMiU2hfG/0MfnBs+1ZrlSGmlgWeMcXQkDk1UFb8Q=="],
|
||||
|
||||
"onnxruntime-node": ["onnxruntime-node@1.19.2", "", { "dependencies": { "onnxruntime-common": "1.19.2", "tar": "^7.0.1" }, "os": [ "linux", "win32", "darwin", ] }, "sha512-9eHMP/HKbbeUcqte1JYzaaRC8JPn7ojWeCeoyShO86TOR97OCyIyAIOGX3V95ErjslVhJRXY8Em/caIUc0hm1Q=="],
|
||||
|
||||
"onnxruntime-web": ["onnxruntime-web@1.22.0-dev.20250409-89f8206ba4", "", { "dependencies": { "flatbuffers": "^25.1.24", "guid-typescript": "^1.0.9", "long": "^5.2.3", "onnxruntime-common": "1.22.0-dev.20250409-89f8206ba4", "platform": "^1.3.6", "protobufjs": "^7.2.4" } }, "sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="],
|
||||
|
||||
"platform": ["platform@1.3.6", "", {}, "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="],
|
||||
|
||||
"protobufjs": ["protobufjs@7.5.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-Z2E/kOY1QjoMlCytmexzYfDm/w5fKAiRwpSzGtdnXW1zC88Z2yXazHHrOtwCzn+7wSxyE8PYM4rvVcMphF9sOA=="],
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
"raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="],
|
||||
|
||||
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
|
||||
|
||||
"redis-info": ["redis-info@3.1.0", "", { "dependencies": { "lodash": "^4.17.11" } }, "sha512-ER4L9Sh/vm63DkIE0bkSjxluQlioBiBgf5w1UuldaW/3vPcecdljVDisZhmnCMvsxHNiARTTDDHGg9cGwTfrKg=="],
|
||||
|
||||
"redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="],
|
||||
|
||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||
|
||||
"roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
|
||||
|
||||
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||
|
||||
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
||||
|
||||
"semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="],
|
||||
|
||||
"send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="],
|
||||
|
||||
"serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="],
|
||||
|
||||
"serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="],
|
||||
|
||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||
|
||||
"sharp": ["sharp@0.34.1", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.7.1" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.1", "@img/sharp-darwin-x64": "0.34.1", "@img/sharp-libvips-darwin-arm64": "1.1.0", "@img/sharp-libvips-darwin-x64": "1.1.0", "@img/sharp-libvips-linux-arm": "1.1.0", "@img/sharp-libvips-linux-arm64": "1.1.0", "@img/sharp-libvips-linux-ppc64": "1.1.0", "@img/sharp-libvips-linux-s390x": "1.1.0", "@img/sharp-libvips-linux-x64": "1.1.0", "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", "@img/sharp-libvips-linuxmusl-x64": "1.1.0", "@img/sharp-linux-arm": "0.34.1", "@img/sharp-linux-arm64": "0.34.1", "@img/sharp-linux-s390x": "0.34.1", "@img/sharp-linux-x64": "0.34.1", "@img/sharp-linuxmusl-arm64": "0.34.1", "@img/sharp-linuxmusl-x64": "0.34.1", "@img/sharp-wasm32": "0.34.1", "@img/sharp-win32-ia32": "0.34.1", "@img/sharp-win32-x64": "0.34.1" } }, "sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg=="],
|
||||
|
||||
"shell-quote": ["shell-quote@1.8.2", "", {}, "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA=="],
|
||||
|
||||
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||
|
||||
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||
|
||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
|
||||
|
||||
"sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
|
||||
|
||||
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
|
||||
|
||||
"statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
|
||||
|
||||
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
||||
|
||||
"tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
|
||||
|
||||
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||
|
||||
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
||||
|
||||
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||
|
||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
|
||||
"@huggingface/transformers/onnxruntime-node": ["onnxruntime-node@1.21.0", "", { "dependencies": { "global-agent": "^3.0.0", "onnxruntime-common": "1.21.0", "tar": "^7.0.1" }, "os": [ "linux", "win32", "darwin", ] }, "sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw=="],
|
||||
|
||||
"chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"filelist/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
|
||||
|
||||
"onnxruntime-web/onnxruntime-common": ["onnxruntime-common@1.22.0-dev.20250409-89f8206ba4", "", {}, "sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ=="],
|
||||
|
||||
"@huggingface/transformers/onnxruntime-node/onnxruntime-common": ["onnxruntime-common@1.21.0", "", {}, "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ=="],
|
||||
|
||||
"filelist/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||
}
|
||||
}
|
@ -1,48 +1,45 @@
|
||||
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
|
||||
import { AllDataType, BiliUserType } from "@core/db/schema";
|
||||
import Akari from "ml/akari.ts";
|
||||
import type { Psql } from "@core/db/psql.d.ts";
|
||||
import { BiliVideoMetadataType, BiliUserType } from "@core/db/schema";
|
||||
import { AkariModelVersion } from "ml/const";
|
||||
|
||||
export async function videoExistsInAllData(client: Client, aid: number) {
|
||||
return await client.queryObject<{ exists: boolean }>(
|
||||
`SELECT EXISTS(SELECT 1 FROM bilibili_metadata WHERE aid = $1)`,
|
||||
[aid],
|
||||
)
|
||||
.then((result) => result.rows[0].exists);
|
||||
export async function videoExistsInAllData(sql: Psql, aid: number) {
|
||||
const rows = await sql<{ exists: boolean }[]>`
|
||||
SELECT EXISTS(SELECT 1 FROM bilibili_metadata WHERE aid = ${aid})
|
||||
`;
|
||||
return rows[0].exists;
|
||||
}
|
||||
|
||||
export async function userExistsInBiliUsers(client: Client, uid: number) {
|
||||
return await client.queryObject<{ exists: boolean }>(`SELECT EXISTS(SELECT 1 FROM bilibili_user WHERE uid = $1)`, [
|
||||
uid,
|
||||
]);
|
||||
export async function userExistsInBiliUsers(sql: Psql, uid: number) {
|
||||
const rows = await sql<{ exists: boolean }[]>`
|
||||
SELECT EXISTS(SELECT 1 FROM bilibili_user WHERE uid = ${uid})
|
||||
`;
|
||||
return rows[0].exists;
|
||||
}
|
||||
|
||||
export async function getUnlabelledVideos(client: Client) {
|
||||
const queryResult = await client.queryObject<{ aid: number }>(
|
||||
`SELECT a.aid FROM bilibili_metadata a LEFT JOIN labelling_result l ON a.aid = l.aid WHERE l.aid IS NULL`,
|
||||
);
|
||||
return queryResult.rows.map((row) => row.aid);
|
||||
export async function getUnlabelledVideos(sql: Psql) {
|
||||
const rows = await sql<{ aid: number }[]>`
|
||||
SELECT a.aid FROM bilibili_metadata a LEFT JOIN labelling_result l ON a.aid = l.aid WHERE l.aid IS NULL
|
||||
`;
|
||||
return rows.map((row) => row.aid);
|
||||
}
|
||||
|
||||
export async function insertVideoLabel(client: Client, aid: number, label: number) {
|
||||
return await client.queryObject(
|
||||
`INSERT INTO labelling_result (aid, label, model_version) VALUES ($1, $2, $3) ON CONFLICT (aid, model_version) DO NOTHING`,
|
||||
[aid, label, Akari.getModelVersion()],
|
||||
);
|
||||
export async function insertVideoLabel(sql: Psql, aid: number, label: number) {
|
||||
await sql`
|
||||
INSERT INTO labelling_result (aid, label, model_version) VALUES (${aid}, ${label}, ${AkariModelVersion}) ON CONFLICT (aid, model_version) DO NOTHING
|
||||
`;
|
||||
}
|
||||
|
||||
export async function getVideoInfoFromAllData(client: Client, aid: number) {
|
||||
const queryResult = await client.queryObject<AllDataType>(
|
||||
`SELECT * FROM bilibili_metadata WHERE aid = $1`,
|
||||
[aid],
|
||||
);
|
||||
const row = queryResult.rows[0];
|
||||
export async function getVideoInfoFromAllData(sql: Psql, aid: number) {
|
||||
const rows = await sql<AllDataType[]>`
|
||||
SELECT * FROM bilibili_metadata WHERE aid = ${aid}
|
||||
`;
|
||||
const row = rows[0];
|
||||
let authorInfo = "";
|
||||
if (row.uid && await userExistsInBiliUsers(client, row.uid)) {
|
||||
const q = await client.queryObject<BiliUserType>(
|
||||
`SELECT * FROM bilibili_user WHERE uid = $1`,
|
||||
[row.uid],
|
||||
);
|
||||
const userRow = q.rows[0];
|
||||
if (row.uid && (await userExistsInBiliUsers(sql, row.uid))) {
|
||||
const userRows = await sql<BiliUserType[]>`
|
||||
SELECT * FROM bilibili_user WHERE uid = ${row.uid}
|
||||
`;
|
||||
const userRow = userRows[0];
|
||||
if (userRow) {
|
||||
authorInfo = userRow.desc;
|
||||
}
|
||||
@ -51,37 +48,30 @@ export async function getVideoInfoFromAllData(client: Client, aid: number) {
|
||||
title: row.title,
|
||||
description: row.description,
|
||||
tags: row.tags,
|
||||
author_info: authorInfo,
|
||||
author_info: authorInfo
|
||||
};
|
||||
}
|
||||
|
||||
export async function getUnArchivedBiliUsers(client: Client) {
|
||||
const queryResult = await client.queryObject<{ uid: number }>(
|
||||
`
|
||||
SELECT ad.uid
|
||||
FROM bilibili_metadata ad
|
||||
LEFT JOIN bilibili_user bu ON ad.uid = bu.uid
|
||||
WHERE bu.uid IS NULL;
|
||||
`,
|
||||
[],
|
||||
);
|
||||
const rows = queryResult.rows;
|
||||
export async function getUnArchivedBiliUsers(sql: Psql) {
|
||||
const rows = await sql<{ uid: number }[]>`
|
||||
SELECT ad.uid
|
||||
FROM bilibili_metadata ad
|
||||
LEFT JOIN bilibili_user bu ON ad.uid = bu.uid
|
||||
WHERE bu.uid IS NULL;
|
||||
`;
|
||||
return rows.map((row) => row.uid);
|
||||
}
|
||||
|
||||
export async function setBiliVideoStatus(client: Client, aid: number, status: number) {
|
||||
return await client.queryObject(
|
||||
`UPDATE bilibili_metadata SET status = $1 WHERE aid = $2`,
|
||||
[status, aid],
|
||||
);
|
||||
export async function setBiliVideoStatus(sql: Psql, aid: number, status: number) {
|
||||
await sql`
|
||||
UPDATE bilibili_metadata SET status = ${status} WHERE aid = ${aid}
|
||||
`;
|
||||
}
|
||||
|
||||
export async function getBiliVideoStatus(client: Client, aid: number) {
|
||||
const queryResult = await client.queryObject<{ status: number }>(
|
||||
`SELECT status FROM bilibili_metadata WHERE aid = $1`,
|
||||
[aid],
|
||||
);
|
||||
const rows = queryResult.rows;
|
||||
export async function getBiliVideoStatus(sql: Psql, aid: number) {
|
||||
const rows = await sql<{ status: number }[]>`
|
||||
SELECT status FROM bilibili_metadata WHERE aid = ${aid}
|
||||
`;
|
||||
if (rows.length === 0) return 0;
|
||||
return rows[0].status;
|
||||
}
|
||||
|
@ -1,6 +0,0 @@
|
||||
import { Pool } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
|
||||
import { postgresConfig } from "@core/db/pgConfig";
|
||||
|
||||
const pool = new Pool(postgresConfig, 12);
|
||||
|
||||
export const db = pool;
|
@ -1,49 +1,46 @@
|
||||
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
|
||||
import { LatestSnapshotType } from "@core/db/schema";
|
||||
import { SnapshotNumber } from "mq/task/getVideoStats.ts";
|
||||
import type { Psql } from "@core/db/psql.d.ts";
|
||||
|
||||
export async function getVideosNearMilestone(client: Client) {
|
||||
const queryResult = await client.queryObject<LatestSnapshotType>(`
|
||||
SELECT ls.*
|
||||
export async function getVideosNearMilestone(sql: Psql) {
|
||||
const queryResult = await sql<LatestSnapshotType[]>`
|
||||
SELECT ls.*
|
||||
FROM latest_video_snapshot ls
|
||||
RIGHT JOIN songs ON songs.aid = ls.aid
|
||||
RIGHT JOIN songs ON songs.aid = ls.aid
|
||||
WHERE
|
||||
(views >= 50000 AND views < 100000) OR
|
||||
(views >= 900000 AND views < 1000000) OR
|
||||
(views >= 9900000 AND views < 10000000)
|
||||
(views >= CEIL(views::float/1000000::float)*1000000-100000 AND views < CEIL(views::float/1000000::float)*1000000)
|
||||
UNION
|
||||
SELECT ls.*
|
||||
FROM latest_video_snapshot ls
|
||||
WHERE
|
||||
(views >= 90000 AND views < 100000) OR
|
||||
(views >= 900000 AND views < 1000000) OR
|
||||
(views >= 9900000 AND views < 10000000)
|
||||
`);
|
||||
return queryResult.rows.map((row) => {
|
||||
(views >= CEIL(views::float/1000000::float)*1000000-100000 AND views < CEIL(views::float/1000000::float)*1000000)
|
||||
`;
|
||||
return queryResult.map((row) => {
|
||||
return {
|
||||
...row,
|
||||
aid: Number(row.aid),
|
||||
aid: Number(row.aid)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function getLatestVideoSnapshot(client: Client, aid: number): Promise<null | SnapshotNumber> {
|
||||
const queryResult = await client.queryObject<LatestSnapshotType>(
|
||||
`
|
||||
export async function getLatestVideoSnapshot(sql: Psql, aid: number): Promise<null | SnapshotNumber> {
|
||||
const queryResult = await sql<LatestSnapshotType[]>`
|
||||
SELECT *
|
||||
FROM latest_video_snapshot
|
||||
WHERE aid = $1
|
||||
`,
|
||||
[aid],
|
||||
);
|
||||
if (queryResult.rows.length === 0) {
|
||||
WHERE aid = ${aid}
|
||||
`;
|
||||
if (queryResult.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return queryResult.rows.map((row) => {
|
||||
return queryResult.map((row) => {
|
||||
return {
|
||||
...row,
|
||||
aid: Number(row.aid),
|
||||
time: new Date(row.time).getTime(),
|
||||
time: new Date(row.time).getTime()
|
||||
};
|
||||
})[0];
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
|
||||
import { SnapshotScheduleType } from "@core/db/schema";
|
||||
import logger from "log/logger.ts";
|
||||
import { MINUTE } from "$std/datetime/constants.ts";
|
||||
import type { SnapshotScheduleType } from "@core/db/schema.d.ts";
|
||||
import logger from "@core/log/logger.ts";
|
||||
import { MINUTE } from "@core/const/time.ts";
|
||||
import { redis } from "@core/db/redis.ts";
|
||||
import { Redis } from "ioredis";
|
||||
import {parseTimestampFromPsql} from "../utils/formatTimestampToPostgre.ts";
|
||||
import { parseTimestampFromPsql } from "../utils/formatTimestampToPostgre.ts";
|
||||
import type { Psql } from "@core/db/psql.d.ts";
|
||||
|
||||
const REDIS_KEY = "cvsa:snapshot_window_counts";
|
||||
|
||||
@ -14,13 +14,13 @@ function getCurrentWindowIndex(): number {
|
||||
return Math.floor(minutesSinceMidnight / 5);
|
||||
}
|
||||
|
||||
export async function refreshSnapshotWindowCounts(client: Client, redisClient: Redis) {
|
||||
export async function refreshSnapshotWindowCounts(sql: Psql, redisClient: Redis) {
|
||||
const now = new Date();
|
||||
const startTime = now.getTime();
|
||||
|
||||
const result = await client.queryObject<{ window_start: Date; count: number }>`
|
||||
SELECT
|
||||
date_trunc('hour', started_at) +
|
||||
const result = await sql<{ window_start: Date; count: number }[]>`
|
||||
SELECT
|
||||
date_trunc('hour', started_at) +
|
||||
(EXTRACT(minute FROM started_at)::int / 5 * INTERVAL '5 minutes') AS window_start,
|
||||
COUNT(*) AS count
|
||||
FROM snapshot_schedule
|
||||
@ -33,7 +33,7 @@ export async function refreshSnapshotWindowCounts(client: Client, redisClient: R
|
||||
|
||||
const currentWindow = getCurrentWindowIndex();
|
||||
|
||||
for (const row of result.rows) {
|
||||
for (const row of result) {
|
||||
const targetOffset = Math.floor((row.window_start.getTime() - startTime) / (5 * MINUTE));
|
||||
const offset = currentWindow + targetOffset;
|
||||
if (offset >= 0) {
|
||||
@ -42,10 +42,10 @@ export async function refreshSnapshotWindowCounts(client: Client, redisClient: R
|
||||
}
|
||||
}
|
||||
|
||||
export async function initSnapshotWindowCounts(client: Client, redisClient: Redis) {
|
||||
await refreshSnapshotWindowCounts(client, redisClient);
|
||||
export async function initSnapshotWindowCounts(sql: Psql, redisClient: Redis) {
|
||||
await refreshSnapshotWindowCounts(sql, redisClient);
|
||||
setInterval(async () => {
|
||||
await refreshSnapshotWindowCounts(client, redisClient);
|
||||
await refreshSnapshotWindowCounts(sql, redisClient);
|
||||
}, 5 * MINUTE);
|
||||
}
|
||||
|
||||
@ -54,44 +54,44 @@ async function getWindowCount(redisClient: Redis, offset: number): Promise<numbe
|
||||
return count ? parseInt(count, 10) : 0;
|
||||
}
|
||||
|
||||
export async function snapshotScheduleExists(client: Client, id: number) {
|
||||
const res = await client.queryObject<{ id: number }>(
|
||||
`SELECT id FROM snapshot_schedule WHERE id = $1`,
|
||||
[id],
|
||||
);
|
||||
return res.rows.length > 0;
|
||||
export async function snapshotScheduleExists(sql: Psql, id: number) {
|
||||
const rows = await sql<{ id: number }[]>`
|
||||
SELECT id
|
||||
FROM snapshot_schedule
|
||||
WHERE id = ${id}
|
||||
`;
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
export async function videoHasActiveSchedule(client: Client, aid: number) {
|
||||
const res = await client.queryObject<{ status: string }>(
|
||||
`SELECT status FROM snapshot_schedule WHERE aid = $1 AND (status = 'pending' OR status = 'processing')`,
|
||||
[aid],
|
||||
);
|
||||
return res.rows.length > 0;
|
||||
export async function videoHasActiveScheduleWithType(sql: Psql, aid: number, type: string) {
|
||||
const rows = await sql<{ status: string }[]>`
|
||||
SELECT status FROM snapshot_schedule
|
||||
WHERE aid = ${aid}
|
||||
AND (status = 'pending' OR status = 'processing')
|
||||
AND type = ${type}
|
||||
`;
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
export async function videoHasActiveScheduleWithType(client: Client, aid: number, type: string) {
|
||||
const res = await client.queryObject<{ status: string }>(
|
||||
`SELECT status FROM snapshot_schedule WHERE aid = $1 AND (status = 'pending' OR status = 'processing') AND type = $2`,
|
||||
[aid, type],
|
||||
);
|
||||
return res.rows.length > 0;
|
||||
export async function videoHasProcessingSchedule(sql: Psql, aid: number) {
|
||||
const rows = await sql<{ status: string }[]>`
|
||||
SELECT status
|
||||
FROM snapshot_schedule
|
||||
WHERE aid = ${aid}
|
||||
AND status = 'processing'
|
||||
`;
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
export async function videoHasProcessingSchedule(client: Client, aid: number) {
|
||||
const res = await client.queryObject<{ status: string }>(
|
||||
`SELECT status FROM snapshot_schedule WHERE aid = $1 AND status = 'processing'`,
|
||||
[aid],
|
||||
);
|
||||
return res.rows.length > 0;
|
||||
}
|
||||
|
||||
export async function bulkGetVideosWithoutProcessingSchedules(client: Client, aids: number[]) {
|
||||
const res = await client.queryObject<{ aid: number }>(
|
||||
`SELECT aid FROM snapshot_schedule WHERE aid = ANY($1) AND status != 'processing' GROUP BY aid`,
|
||||
[aids],
|
||||
);
|
||||
return res.rows.map((row) => row.aid);
|
||||
export async function bulkGetVideosWithoutProcessingSchedules(sql: Psql, aids: number[]) {
|
||||
const rows = await sql<{ aid: string }[]>`
|
||||
SELECT aid
|
||||
FROM snapshot_schedule
|
||||
WHERE aid = ANY(${aids})
|
||||
AND status != 'processing'
|
||||
GROUP BY aid
|
||||
`;
|
||||
return rows.map((row) => Number(row.aid));
|
||||
}
|
||||
|
||||
interface Snapshot {
|
||||
@ -99,149 +99,144 @@ interface Snapshot {
|
||||
views: number;
|
||||
}
|
||||
|
||||
export async function findClosestSnapshot(
|
||||
client: Client,
|
||||
aid: number,
|
||||
targetTime: Date,
|
||||
): Promise<Snapshot | null> {
|
||||
const query = `
|
||||
export async function findClosestSnapshot(sql: Psql, aid: number, targetTime: Date): Promise<Snapshot | null> {
|
||||
const result = await sql<{ created_at: string; views: number }[]>`
|
||||
SELECT created_at, views
|
||||
FROM video_snapshot
|
||||
WHERE aid = $1
|
||||
ORDER BY ABS(EXTRACT(EPOCH FROM (created_at - $2::timestamptz)))
|
||||
WHERE aid = ${aid}
|
||||
ORDER BY ABS(EXTRACT(EPOCH FROM (created_at - ${targetTime.toISOString()}::timestamptz)))
|
||||
LIMIT 1
|
||||
`;
|
||||
const result = await client.queryObject<{ created_at: string; views: number }>(
|
||||
query,
|
||||
[aid, targetTime.toISOString()],
|
||||
);
|
||||
if (result.rows.length === 0) return null;
|
||||
const row = result.rows[0];
|
||||
if (result.length === 0) return null;
|
||||
const row = result[0];
|
||||
return {
|
||||
created_at: new Date(row.created_at).getTime(),
|
||||
views: row.views,
|
||||
views: row.views
|
||||
};
|
||||
}
|
||||
|
||||
export async function findSnapshotBefore(
|
||||
client: Client,
|
||||
aid: number,
|
||||
targetTime: Date,
|
||||
): Promise<Snapshot | null> {
|
||||
const query = `
|
||||
export async function findSnapshotBefore(sql: Psql, aid: number, targetTime: Date): Promise<Snapshot | null> {
|
||||
const result = await sql<{ created_at: string; views: number }[]>`
|
||||
SELECT created_at, views
|
||||
FROM video_snapshot
|
||||
WHERE aid = $1
|
||||
AND created_at <= $2::timestamptz
|
||||
WHERE aid = ${aid}
|
||||
AND created_at <= ${targetTime}::timestamptz
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
`;
|
||||
const result = await client.queryObject<{ created_at: string; views: number }>(
|
||||
query,
|
||||
[aid, targetTime.toISOString()],
|
||||
);
|
||||
if (result.rows.length === 0) return null;
|
||||
const row = result.rows[0];
|
||||
if (result.length === 0) return null;
|
||||
const row = result[0];
|
||||
return {
|
||||
created_at: new Date(row.created_at).getTime(),
|
||||
views: row.views,
|
||||
views: row.views
|
||||
};
|
||||
}
|
||||
|
||||
export async function hasAtLeast2Snapshots(client: Client, aid: number) {
|
||||
const res = await client.queryObject<{ count: number }>(
|
||||
`SELECT COUNT(*) FROM video_snapshot WHERE aid = $1`,
|
||||
[aid],
|
||||
);
|
||||
return res.rows[0].count >= 2;
|
||||
export async function hasAtLeast2Snapshots(sql: Psql, aid: number) {
|
||||
const res = await sql<{ count: number }[]>`
|
||||
SELECT COUNT(*)
|
||||
FROM video_snapshot
|
||||
WHERE aid = ${aid}
|
||||
`;
|
||||
return res[0].count >= 2;
|
||||
}
|
||||
|
||||
export async function getLatestSnapshot(client: Client, aid: number): Promise<Snapshot | null> {
|
||||
const res = await client.queryObject<{ created_at: string; views: number }>(
|
||||
`SELECT created_at, views FROM video_snapshot WHERE aid = $1 ORDER BY created_at DESC LIMIT 1`,
|
||||
[aid],
|
||||
);
|
||||
if (res.rows.length === 0) return null;
|
||||
const row = res.rows[0];
|
||||
export async function getLatestSnapshot(sql: Psql, aid: number): Promise<Snapshot | null> {
|
||||
const res = await sql<{ created_at: string; views: number }[]>`
|
||||
SELECT created_at, views
|
||||
FROM video_snapshot
|
||||
WHERE aid = ${aid}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
`;
|
||||
if (res.length === 0) return null;
|
||||
const row = res[0];
|
||||
return {
|
||||
created_at: new Date(row.created_at).getTime(),
|
||||
views: row.views,
|
||||
views: row.views
|
||||
};
|
||||
}
|
||||
|
||||
export async function getLatestActiveScheduleWithType(client: Client, aid: number, type: string) {
|
||||
const query: string = `
|
||||
export async function getLatestActiveScheduleWithType(sql: Psql, aid: number, type: string) {
|
||||
const rows = await sql`
|
||||
SELECT *
|
||||
FROM snapshot_schedule
|
||||
WHERE aid = $1
|
||||
AND type = $2
|
||||
WHERE aid = ${aid}
|
||||
AND type = ${type}
|
||||
AND (status = 'pending' OR status = 'processing')
|
||||
ORDER BY started_at DESC
|
||||
LIMIT 1
|
||||
`
|
||||
const res = await client.queryObject<SnapshotScheduleType>(query, [aid, type]);
|
||||
return res.rows[0];
|
||||
`;
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
/*
|
||||
* Creates a new snapshot schedule record.
|
||||
* @param client The database client.
|
||||
* @param aid The aid of the video.
|
||||
* @param type Type of the snapshot.
|
||||
* @param targetTime Scheduled time for snapshot. (Timestamp in milliseconds)
|
||||
* @param force Ignore all restrictions and force the creation of the schedule.
|
||||
*/
|
||||
export async function scheduleSnapshot(
|
||||
client: Client,
|
||||
sql: Psql,
|
||||
aid: number,
|
||||
type: string,
|
||||
targetTime: number,
|
||||
force: boolean = false,
|
||||
adjustTime: boolean = true
|
||||
) {
|
||||
let adjustedTime = new Date(targetTime);
|
||||
const hashActiveSchedule = await videoHasActiveScheduleWithType(client, aid, type);
|
||||
const hashActiveSchedule = await videoHasActiveScheduleWithType(sql, aid, type);
|
||||
if (type == "milestone" && hashActiveSchedule) {
|
||||
const latestActiveSchedule = await getLatestActiveScheduleWithType(client, aid, type);
|
||||
const latestActiveSchedule = await getLatestActiveScheduleWithType(sql, aid, type);
|
||||
const latestScheduleStartedAt = new Date(parseTimestampFromPsql(latestActiveSchedule.started_at!));
|
||||
if (latestScheduleStartedAt > adjustedTime) {
|
||||
await client.queryObject(`
|
||||
await sql`
|
||||
UPDATE snapshot_schedule
|
||||
SET started_at = $1
|
||||
WHERE id = $2
|
||||
`, [adjustedTime, latestActiveSchedule.id]);
|
||||
SET started_at = ${adjustedTime}
|
||||
WHERE id = ${latestActiveSchedule.id}
|
||||
`;
|
||||
logger.log(
|
||||
`Updated snapshot schedule for ${aid} at ${adjustedTime.toISOString()}`,
|
||||
"mq",
|
||||
"fn:scheduleSnapshot",
|
||||
"fn:scheduleSnapshot"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (hashActiveSchedule && !force) return;
|
||||
if (type !== "milestone" && type !== "new") {
|
||||
if (type !== "milestone" && type !== "new" && adjustTime) {
|
||||
adjustedTime = await adjustSnapshotTime(new Date(targetTime), 2000, redis);
|
||||
}
|
||||
logger.log(`Scheduled snapshot for ${aid} at ${adjustedTime.toISOString()}`, "mq", "fn:scheduleSnapshot");
|
||||
return client.queryObject(
|
||||
`INSERT INTO snapshot_schedule (aid, type, started_at) VALUES ($1, $2, $3)`,
|
||||
[aid, type, adjustedTime.toISOString()],
|
||||
);
|
||||
return sql`
|
||||
INSERT INTO snapshot_schedule
|
||||
(aid, type, started_at)
|
||||
VALUES (
|
||||
${aid},
|
||||
${type},
|
||||
${adjustedTime.toISOString()}
|
||||
)
|
||||
`;
|
||||
}
|
||||
|
||||
export async function bulkScheduleSnapshot(
|
||||
client: Client,
|
||||
sql: Psql,
|
||||
aids: number[],
|
||||
type: string,
|
||||
targetTime: number,
|
||||
force: boolean = false,
|
||||
adjustTime: boolean = true
|
||||
) {
|
||||
for (const aid of aids) {
|
||||
await scheduleSnapshot(client, aid, type, targetTime, force);
|
||||
await scheduleSnapshot(sql, aid, type, targetTime, force, adjustTime);
|
||||
}
|
||||
}
|
||||
|
||||
export async function adjustSnapshotTime(
|
||||
expectedStartTime: Date,
|
||||
allowedCounts: number = 1000,
|
||||
redisClient: Redis,
|
||||
redisClient: Redis
|
||||
): Promise<Date> {
|
||||
const currentWindow = getCurrentWindowIndex();
|
||||
const targetOffset = Math.floor((expectedStartTime.getTime() - Date.now()) / (5 * MINUTE)) - 6;
|
||||
@ -286,25 +281,24 @@ export async function adjustSnapshotTime(
|
||||
return expectedStartTime;
|
||||
}
|
||||
|
||||
export async function getSnapshotsInNextSecond(client: Client) {
|
||||
const query = `
|
||||
SELECT *
|
||||
FROM snapshot_schedule
|
||||
WHERE started_at <= NOW() + INTERVAL '1 seconds' AND status = 'pending' AND type != 'normal'
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN type = 'milestone' THEN 0
|
||||
ELSE 1
|
||||
END,
|
||||
started_at
|
||||
LIMIT 10;
|
||||
export async function getSnapshotsInNextSecond(sql: Psql) {
|
||||
return sql<SnapshotScheduleType[]>`
|
||||
SELECT *
|
||||
FROM snapshot_schedule
|
||||
WHERE started_at <= NOW() + INTERVAL '1 seconds'
|
||||
AND status = 'pending'
|
||||
AND type != 'normal'
|
||||
ORDER BY CASE
|
||||
WHEN type = 'milestone' THEN 0
|
||||
ELSE 1
|
||||
END,
|
||||
started_at
|
||||
LIMIT 10;
|
||||
`;
|
||||
const res = await client.queryObject<SnapshotScheduleType>(query, []);
|
||||
return res.rows;
|
||||
}
|
||||
|
||||
export async function getBulkSnapshotsInNextSecond(client: Client) {
|
||||
const query = `
|
||||
export async function getBulkSnapshotsInNextSecond(sql: Psql) {
|
||||
return sql<SnapshotScheduleType[]>`
|
||||
SELECT *
|
||||
FROM snapshot_schedule
|
||||
WHERE (started_at <= NOW() + INTERVAL '15 seconds')
|
||||
@ -317,42 +311,33 @@ export async function getBulkSnapshotsInNextSecond(client: Client) {
|
||||
started_at
|
||||
LIMIT 1000;
|
||||
`;
|
||||
const res = await client.queryObject<SnapshotScheduleType>(query, []);
|
||||
return res.rows;
|
||||
}
|
||||
|
||||
export async function setSnapshotStatus(client: Client, id: number, status: string) {
|
||||
return await client.queryObject(
|
||||
`UPDATE snapshot_schedule SET status = $2 WHERE id = $1`,
|
||||
[id, status],
|
||||
);
|
||||
export async function setSnapshotStatus(sql: Psql, id: number, status: string) {
|
||||
return sql`
|
||||
UPDATE snapshot_schedule
|
||||
SET status = ${status}
|
||||
WHERE id = ${id}
|
||||
`;
|
||||
}
|
||||
|
||||
export async function bulkSetSnapshotStatus(client: Client, ids: number[], status: string) {
|
||||
return await client.queryObject(
|
||||
`UPDATE snapshot_schedule SET status = $2 WHERE id = ANY($1)`,
|
||||
[ids, status],
|
||||
);
|
||||
export async function bulkSetSnapshotStatus(sql: Psql, ids: number[], status: string) {
|
||||
return sql`
|
||||
UPDATE snapshot_schedule
|
||||
SET status = ${status}
|
||||
WHERE id = ANY (${ids})
|
||||
`;
|
||||
}
|
||||
|
||||
export async function getVideosWithoutActiveSnapshotSchedule(client: Client) {
|
||||
const query: string = `
|
||||
export async function getVideosWithoutActiveSnapshotScheduleByType(sql: Psql, type: string) {
|
||||
const rows = await sql<{ aid: string }[]>`
|
||||
SELECT s.aid
|
||||
FROM songs s
|
||||
LEFT JOIN snapshot_schedule ss ON s.aid = ss.aid AND (ss.status = 'pending' OR ss.status = 'processing')
|
||||
LEFT JOIN snapshot_schedule ss ON
|
||||
s.aid = ss.aid AND
|
||||
(ss.status = 'pending' OR ss.status = 'processing') AND
|
||||
ss.type = ${type}
|
||||
WHERE ss.aid IS NULL
|
||||
`;
|
||||
const res = await client.queryObject<{ aid: number }>(query, []);
|
||||
return res.rows.map((r) => Number(r.aid));
|
||||
}
|
||||
|
||||
export async function getAllVideosWithoutActiveSnapshotSchedule(client: Client) {
|
||||
const query: string = `
|
||||
SELECT s.aid
|
||||
FROM bilibili_metadata s
|
||||
LEFT JOIN snapshot_schedule ss ON s.aid = ss.aid AND (ss.status = 'pending' OR ss.status = 'processing')
|
||||
WHERE ss.aid IS NULL
|
||||
`;
|
||||
const res = await client.queryObject<{ aid: number }>(query, []);
|
||||
return res.rows.map((r) => Number(r.aid));
|
||||
return rows.map((r) => Number(r.aid));
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user