Merge branch 'develop' into feat/schedule-create
This commit is contained in:
commit
4104cbf5b5
81 changed files with 1963 additions and 3529 deletions
192
.config/ci.yml
192
.config/ci.yml
|
@ -1,195 +1,11 @@
|
|||
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
# Firefish configuration
|
||||
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
# ┌─────┐
|
||||
#───┘ URL └─────────────────────────────────────────────────────
|
||||
|
||||
# Final accessible URL seen by a user.
|
||||
url: https://example.tld/
|
||||
|
||||
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||
# URL SETTINGS AFTER THAT!
|
||||
|
||||
# ┌───────────────────────┐
|
||||
#───┘ Port and TLS settings └───────────────────────────────────
|
||||
|
||||
#
|
||||
# Misskey requires a reverse proxy to support HTTPS connections.
|
||||
#
|
||||
# +----- https://example.tld/ ------------+
|
||||
# +------+ |+-------------+ +----------------+|
|
||||
# | User | ---> || Proxy (443) | ---> | Misskey (3000) ||
|
||||
# +------+ |+-------------+ +----------------+|
|
||||
# +---------------------------------------+
|
||||
#
|
||||
# You need to set up a reverse proxy. (e.g. nginx)
|
||||
# An encrypted connection with HTTPS is highly recommended
|
||||
# because tokens may be transferred in GET requests.
|
||||
|
||||
# The port that your Misskey server should listen on.
|
||||
url: http://localhost:3000
|
||||
port: 3000
|
||||
|
||||
# ┌──────────────────────────┐
|
||||
#───┘ PostgreSQL configuration └────────────────────────────────
|
||||
|
||||
db:
|
||||
host: postgres
|
||||
port: 5432
|
||||
|
||||
# Database name
|
||||
db: postgres
|
||||
|
||||
# Auth
|
||||
user: postgres
|
||||
pass: test
|
||||
|
||||
# Whether disable Caching queries
|
||||
#disableCache: true
|
||||
|
||||
# Extra Connection options
|
||||
#extra:
|
||||
# ssl: true
|
||||
|
||||
# ┌─────────────────────┐
|
||||
#───┘ Redis configuration └─────────────────────────────────────
|
||||
|
||||
db: firefish_db
|
||||
user: firefish
|
||||
pass: password
|
||||
redis:
|
||||
host: redis
|
||||
port: 6379
|
||||
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||
#pass: example-pass
|
||||
#prefix: example-prefix
|
||||
#db: 1
|
||||
|
||||
# ┌─────────────────────────────┐
|
||||
#───┘ Elasticsearch configuration └─────────────────────────────
|
||||
|
||||
#elasticsearch:
|
||||
# host: localhost
|
||||
# port: 9200
|
||||
# ssl: false
|
||||
# user:
|
||||
# pass:
|
||||
|
||||
# ┌───────────────┐
|
||||
#───┘ ID generation └───────────────────────────────────────────
|
||||
|
||||
# You can select the ID generation method.
|
||||
# You don't usually need to change this setting, but you can
|
||||
# change it according to your preferences.
|
||||
|
||||
# Available methods:
|
||||
# aid ... Short, Millisecond accuracy
|
||||
# meid ... Similar to ObjectID, Millisecond accuracy
|
||||
# ulid ... Millisecond accuracy
|
||||
# objectid ... This is left for backward compatibility
|
||||
|
||||
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||
# ID SETTINGS AFTER THAT!
|
||||
|
||||
id: 'aid'
|
||||
|
||||
# ┌─────────────────────┐
|
||||
#───┘ Other configuration └─────────────────────────────────────
|
||||
|
||||
# Max note length, should be < 8000.
|
||||
#maxNoteLength: 3000
|
||||
|
||||
# Whether disable HSTS
|
||||
#disableHsts: true
|
||||
|
||||
# Number of worker processes
|
||||
#clusterLimit: 1
|
||||
|
||||
# Job concurrency per worker
|
||||
# deliverJobConcurrency: 128
|
||||
# inboxJobConcurrency: 16
|
||||
|
||||
# Job rate limiter
|
||||
# deliverJobPerSec: 128
|
||||
# inboxJobPerSec: 16
|
||||
|
||||
# Job attempts
|
||||
# deliverJobMaxAttempts: 12
|
||||
# inboxJobMaxAttempts: 8
|
||||
|
||||
# IP address family used for outgoing request (ipv4, ipv6 or dual)
|
||||
#outgoingAddressFamily: ipv4
|
||||
|
||||
# Syslog option
|
||||
#syslog:
|
||||
# host: localhost
|
||||
# port: 514
|
||||
|
||||
# Proxy for HTTP/HTTPS
|
||||
#proxy: http://127.0.0.1:3128
|
||||
|
||||
#proxyBypassHosts: [
|
||||
# 'example.com',
|
||||
# '192.0.2.8'
|
||||
#]
|
||||
|
||||
# Proxy for SMTP/SMTPS
|
||||
#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT
|
||||
#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4
|
||||
#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5
|
||||
|
||||
# Media Proxy
|
||||
#mediaProxy: https://example.com/proxy
|
||||
|
||||
# Proxy remote files (default: false)
|
||||
#proxyRemoteFiles: true
|
||||
|
||||
#allowedPrivateNetworks: [
|
||||
# '127.0.0.1/32'
|
||||
#]
|
||||
|
||||
# Upload or download file size limits (bytes)
|
||||
#maxFileSize: 262144000
|
||||
|
||||
# Managed hosting settings
|
||||
# !!!!!!!!!!
|
||||
# >>>>>> NORMAL SELF-HOSTERS, STAY AWAY! <<<<<<
|
||||
# >>>>>> YOU DON'T NEED THIS! <<<<<<
|
||||
# !!!!!!!!!!
|
||||
# Each category is optional, but if each item in each category is mandatory!
|
||||
# If you mess this up, that's on you, you've been warned...
|
||||
|
||||
#maxUserSignups: 100
|
||||
#isManagedHosting: true
|
||||
#deepl:
|
||||
# managed: true
|
||||
# authKey: ''
|
||||
# isPro: false
|
||||
#
|
||||
#email:
|
||||
# managed: true
|
||||
# address: 'example@email.com'
|
||||
# host: 'email.com'
|
||||
# port: 587
|
||||
# user: 'example@email.com'
|
||||
# pass: ''
|
||||
# useImplicitSslTls: false
|
||||
#
|
||||
#objectStorage:
|
||||
# managed: true
|
||||
# baseUrl: ''
|
||||
# bucket: ''
|
||||
# prefix: ''
|
||||
# endpoint: ''
|
||||
# region: ''
|
||||
# accessKey: ''
|
||||
# secretKey: ''
|
||||
# useSsl: true
|
||||
# connnectOverProxy: false
|
||||
# setPublicReadOnUpload: true
|
||||
# s3ForcePathStyle: true
|
||||
|
||||
# !!!!!!!!!!
|
||||
# >>>>>> AGAIN, NORMAL SELF-HOSTERS, STAY AWAY! <<<<<<
|
||||
# >>>>>> YOU DON'T NEED THIS, ABOVE SETTINGS ARE FOR MANAGED HOSTING ONLY! <<<<<<
|
||||
# !!!!!!!!!!
|
||||
|
||||
# Seriously. Do NOT fill out the above settings if you're self-hosting.
|
||||
# They're much better off being set from the control panel.
|
||||
|
|
|
@ -51,12 +51,11 @@ title.svg
|
|||
/dev
|
||||
/docs
|
||||
/scripts
|
||||
!/scripts/copy-assets.mjs
|
||||
biome.json
|
||||
COPYING
|
||||
CODE_OF_CONDUCT.md
|
||||
CONTRIBUTING.md
|
||||
Dockerfile
|
||||
LICENSE
|
||||
Procfile
|
||||
README.md
|
||||
SECURITY.md
|
||||
|
|
144
.gitlab-ci.yml
Normal file
144
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,144 @@
|
|||
image: docker.io/rust:slim-bookworm
|
||||
|
||||
services:
|
||||
- name: docker.io/groonga/pgroonga:latest-alpine-12-slim
|
||||
alias: postgres
|
||||
- name: docker.io/redis:7-alpine
|
||||
alias: redis
|
||||
|
||||
workflow:
|
||||
rules:
|
||||
- if: $CI_PROJECT_PATH == 'firefish/firefish'
|
||||
when: always
|
||||
- if: $CI_MERGE_REQUEST_PROJECT_PATH == 'firefish/firefish'
|
||||
when: always
|
||||
- when: never
|
||||
|
||||
cache:
|
||||
paths:
|
||||
- node_modules
|
||||
# - /usr/local/cargo/registry/index
|
||||
# - /usr/local/cargo/registry/cache
|
||||
- target/debug/deps
|
||||
- target/debug/build
|
||||
|
||||
stages:
|
||||
- test
|
||||
- build
|
||||
- dependency
|
||||
|
||||
variables:
|
||||
POSTGRES_DB: 'firefish_db'
|
||||
POSTGRES_USER: 'firefish'
|
||||
POSTGRES_PASSWORD: 'password'
|
||||
POSTGRES_HOST_AUTH_METHOD: 'trust'
|
||||
DEBIAN_FRONTEND: 'noninteractive'
|
||||
CARGO_PROFILE_DEV_OPT_LEVEL: '0'
|
||||
CARGO_PROFILE_DEV_LTO: 'off'
|
||||
CARGO_PROFILE_DEV_DEBUG: 'none'
|
||||
|
||||
default:
|
||||
before_script:
|
||||
- apt-get update && apt-get -y upgrade
|
||||
- apt-get -y --no-install-recommends install curl
|
||||
- curl -fsSL 'https://deb.nodesource.com/setup_18.x' | bash -
|
||||
- apt-get install -y --no-install-recommends build-essential clang mold python3 perl nodejs postgresql-client
|
||||
- corepack enable
|
||||
- corepack prepare pnpm@latest --activate
|
||||
- cp .config/ci.yml .config/default.yml
|
||||
- cp ci/cargo/config.toml /usr/local/cargo/config.toml
|
||||
- export PGPASSWORD="${POSTGRES_PASSWORD}"
|
||||
- psql --host postgres --user "${POSTGRES_USER}" --dbname "${POSTGRES_DB}" --command 'CREATE EXTENSION pgroonga'
|
||||
|
||||
build_test:
|
||||
stage: test
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == 'push' || $CI_PIPELINE_SOURCE == 'merge_request_event'
|
||||
changes:
|
||||
paths:
|
||||
- packages/**/*
|
||||
- scripts/**/*
|
||||
- locales/**/*
|
||||
- package.json
|
||||
- pnpm-lock.yaml
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
script:
|
||||
- pnpm install --frozen-lockfile
|
||||
- pnpm run build:debug
|
||||
- pnpm run migrate
|
||||
|
||||
container_image_build:
|
||||
stage: build
|
||||
image: docker.io/debian:bookworm-slim
|
||||
services: []
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == 'develop'
|
||||
changes:
|
||||
paths:
|
||||
- packages/**/*
|
||||
- locales/**/*
|
||||
- scripts/copy-assets.mjs
|
||||
- package.json
|
||||
- pnpm-lock.yaml
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
- Dockerfile
|
||||
- .dockerignore
|
||||
before_script:
|
||||
- apt-get update && apt-get -y upgrade
|
||||
- apt-get install -y --no-install-recommends buildah ca-certificates fuse-overlayfs
|
||||
- buildah login --username "${CI_REGISTRY_USER}" --password "${CI_REGISTRY_PASSWORD}" "${CI_REGISTRY}"
|
||||
- export IMAGE_TAG="${CI_REGISTRY}/${CI_PROJECT_PATH}/develop:not-for-production"
|
||||
script:
|
||||
- buildah build --isolation chroot --device /dev/fuse:rw --security-opt seccomp=unconfined --security-opt apparmor=unconfined --cap-add all --tag "${IMAGE_TAG}" --platform linux/amd64 .
|
||||
- buildah inspect "${IMAGE_TAG}"
|
||||
- buildah push "${IMAGE_TAG}"
|
||||
|
||||
cargo_unit_test:
|
||||
stage: test
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == 'merge_request_event' || $CI_COMMIT_BRANCH == 'develop'
|
||||
changes:
|
||||
paths:
|
||||
- packages/backend-rs/**/*
|
||||
- packages/macro-rs/**/*
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
- if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'main'
|
||||
when: never
|
||||
script:
|
||||
- cargo check --features napi
|
||||
- pnpm install --frozen-lockfile
|
||||
- mkdir packages/backend-rs/built
|
||||
- cp packages/backend-rs/index.js packages/backend-rs/built/index.js
|
||||
- cp packages/backend-rs/index.d.ts packages/backend-rs/built/index.d.ts
|
||||
- pnpm --filter='!backend-rs' run build:debug
|
||||
- cargo test
|
||||
|
||||
cargo_clippy:
|
||||
stage: test
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
|
||||
changes:
|
||||
paths:
|
||||
- packages/backend-rs/**/*
|
||||
- packages/macro-rs/**/*
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
- if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'main'
|
||||
when: never
|
||||
script:
|
||||
- cargo clippy -- -D warnings
|
||||
|
||||
renovate:
|
||||
stage: dependency
|
||||
image:
|
||||
name: docker.io/renovate/renovate:37-slim
|
||||
entrypoint: [""]
|
||||
rules:
|
||||
- if: $RENOVATE && $CI_PIPELINE_SOURCE == 'schedule'
|
||||
services: []
|
||||
before_script: []
|
||||
script:
|
||||
- renovate --platform gitlab --token "${API_TOKEN}" --endpoint "${CI_SERVER_URL}/api/v4" "${CI_PROJECT_PATH}"
|
4
COPYING
4
COPYING
|
@ -26,10 +26,6 @@ RsaSignature2017 implementation by Transmute Industries Inc
|
|||
License: MIT
|
||||
https://github.com/transmute-industries/RsaSignature2017/blob/master/LICENSE
|
||||
|
||||
Machine learning model for sensitive images by Infinite Red, Inc.
|
||||
License: MIT
|
||||
https://github.com/infinitered/nsfwjs/blob/master/LICENSE
|
||||
|
||||
Chiptune2.js by Simon Gündling
|
||||
License: MIT
|
||||
https://github.com/deskjet/chiptune2.js#license
|
||||
|
|
544
Cargo.lock
generated
544
Cargo.lock
generated
|
@ -124,6 +124,17 @@ version = "0.7.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener",
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream"
|
||||
version = "0.3.5"
|
||||
|
@ -207,6 +218,7 @@ dependencies = [
|
|||
"emojis",
|
||||
"idna",
|
||||
"image",
|
||||
"isahc",
|
||||
"macro_rs",
|
||||
"napi",
|
||||
"napi-build",
|
||||
|
@ -218,7 +230,6 @@ dependencies = [
|
|||
"rand",
|
||||
"redis",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"rmp-serde",
|
||||
"sea-orm",
|
||||
"serde",
|
||||
|
@ -445,6 +456,12 @@ version = "1.6.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
|
||||
|
||||
[[package]]
|
||||
name = "castaway"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.95"
|
||||
|
@ -519,6 +536,15 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
|
@ -534,16 +560,6 @@ dependencies = [
|
|||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.6"
|
||||
|
@ -661,6 +677,37 @@ dependencies = [
|
|||
"sha3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curl"
|
||||
version = "0.4.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e2161dd6eba090ff1594084e95fd67aeccf04382ffea77999ea94ed42ec67b6"
|
||||
dependencies = [
|
||||
"curl-sys",
|
||||
"libc",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"socket2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curl-sys"
|
||||
version = "0.4.72+curl-8.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29cbdc8314c447d11e8fd156dcdd031d9e02a7a976163e396b548c03153bc9ea"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"libnghttp2-sys",
|
||||
"libz-sys",
|
||||
"openssl-sys",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.9"
|
||||
|
@ -793,6 +840,15 @@ dependencies = [
|
|||
"zune-inflate",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
|
||||
dependencies = [
|
||||
"instant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.0.2"
|
||||
|
@ -929,6 +985,21 @@ version = "0.3.30"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
|
||||
dependencies = [
|
||||
"fastrand 1.9.0",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"memchr",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
"waker-fn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.30"
|
||||
|
@ -995,25 +1066,6 @@ version = "0.28.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"http",
|
||||
"indexmap",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.4.1"
|
||||
|
@ -1108,100 +1160,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.1.0"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
|
||||
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body-util"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.60"
|
||||
|
@ -1304,6 +1271,15 @@ dependencies = [
|
|||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "interpolate_name"
|
||||
version = "0.2.4"
|
||||
|
@ -1316,10 +1292,31 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.9.0"
|
||||
name = "isahc"
|
||||
version = "1.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
||||
checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"castaway",
|
||||
"crossbeam-utils",
|
||||
"curl",
|
||||
"curl-sys",
|
||||
"encoding_rs",
|
||||
"event-listener",
|
||||
"futures-lite",
|
||||
"http",
|
||||
"log",
|
||||
"mime",
|
||||
"once_cell",
|
||||
"polling",
|
||||
"slab",
|
||||
"sluice",
|
||||
"tracing",
|
||||
"tracing-futures",
|
||||
"url",
|
||||
"waker-fn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
|
@ -1417,6 +1414,16 @@ version = "0.2.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
|
||||
|
||||
[[package]]
|
||||
name = "libnghttp2-sys"
|
||||
version = "0.1.10+1.61.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "959c25552127d2e1fa72f0e52548ec04fc386e827ba71a7bd01db46a447dc135"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.27.0"
|
||||
|
@ -1428,6 +1435,18 @@ dependencies = [
|
|||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libz-sys"
|
||||
version = "1.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.13"
|
||||
|
@ -1591,24 +1610,6 @@ dependencies = [
|
|||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "new_debug_unreachable"
|
||||
version = "1.0.6"
|
||||
|
@ -1881,6 +1882,12 @@ version = "0.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.2"
|
||||
|
@ -2026,6 +2033,22 @@ dependencies = [
|
|||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"bitflags 1.3.2",
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"libc",
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
|
@ -2330,49 +2353,6 @@ dependencies = [
|
|||
"bytecheck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10"
|
||||
dependencies = [
|
||||
"base64 0.22.0",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-tls",
|
||||
"hyper-util",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls-pemfile 2.1.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"winreg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rgb"
|
||||
version = "0.8.37"
|
||||
|
@ -2523,22 +2503,6 @@ dependencies = [
|
|||
"base64 0.21.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
|
||||
dependencies = [
|
||||
"base64 0.22.0",
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.101.7"
|
||||
|
@ -2680,29 +2644,6 @@ version = "4.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.22"
|
||||
|
@ -2749,18 +2690,6 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.9.34+deprecated"
|
||||
|
@ -2876,6 +2805,17 @@ dependencies = [
|
|||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sluice"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.13.2"
|
||||
|
@ -2972,7 +2912,7 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
"rust_decimal",
|
||||
"rustls",
|
||||
"rustls-pemfile 1.0.4",
|
||||
"rustls-pemfile",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
|
@ -3229,33 +3169,6 @@ dependencies = [
|
|||
"syn 2.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration-sys"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "6.2.2"
|
||||
|
@ -3288,7 +3201,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"fastrand 2.0.2",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
@ -3410,16 +3323,6 @@ dependencies = [
|
|||
"syn 2.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.15"
|
||||
|
@ -3431,20 +3334,6 @@ dependencies = [
|
|||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.12"
|
||||
|
@ -3490,34 +3379,6 @@ dependencies = [
|
|||
"winnow 0.6.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"pin-project",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.40"
|
||||
|
@ -3551,6 +3412,16 @@ dependencies = [
|
|||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-futures"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
|
||||
dependencies = [
|
||||
"pin-project",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
|
@ -3576,12 +3447,6 @@ dependencies = [
|
|||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.17.0"
|
||||
|
@ -3695,13 +3560,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.1"
|
||||
name = "waker-fn"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
|
||||
dependencies = [
|
||||
"try-lock",
|
||||
]
|
||||
checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
|
@ -3740,18 +3602,6 @@ dependencies = [
|
|||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.92"
|
||||
|
@ -3781,16 +3631,6 @@ version = "0.2.92"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.25.4"
|
||||
|
@ -4001,16 +3841,6 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wyz"
|
||||
version = "0.5.1"
|
||||
|
|
|
@ -18,6 +18,7 @@ cuid2 = "0.1.2"
|
|||
emojis = "0.6.2"
|
||||
idna = "0.5.0"
|
||||
image = "0.25.1"
|
||||
isahc = "1.7.2"
|
||||
nom-exif = "1.2.0"
|
||||
once_cell = "1.19.0"
|
||||
openssl = "0.10.64"
|
||||
|
@ -27,7 +28,6 @@ quote = "1.0.36"
|
|||
rand = "0.8.5"
|
||||
redis = "0.25.3"
|
||||
regex = "1.10.4"
|
||||
reqwest = "0.12.4"
|
||||
rmp-serde = "1.2.0"
|
||||
sea-orm = "0.12.15"
|
||||
serde = "1.0.198"
|
||||
|
|
|
@ -45,7 +45,7 @@ COPY packages/backend-rs/index.js packages/backend-rs/built/index.js
|
|||
# Copy in the rest of the files to compile
|
||||
COPY . ./
|
||||
RUN NODE_ENV='production' pnpm run --filter firefish-js build
|
||||
RUN NODE_ENV='production' pnpm run --recursive --parallel --filter '!backend-rs' --filter '!firefish-js' build && pnpm run gulp
|
||||
RUN NODE_ENV='production' pnpm run --recursive --parallel --filter '!backend-rs' --filter '!firefish-js' build && pnpm run build:assets
|
||||
|
||||
# Trim down the dependencies to only those for production
|
||||
RUN find . -path '*/node_modules/*' -delete && pnpm install --prod --frozen-lockfile
|
||||
|
|
7
Makefile
7
Makefile
|
@ -3,7 +3,7 @@ export
|
|||
|
||||
|
||||
.PHONY: pre-commit
|
||||
pre-commit: format entities napi-index
|
||||
pre-commit: format entities napi
|
||||
|
||||
.PHONY: format
|
||||
format:
|
||||
|
@ -11,11 +11,12 @@ format:
|
|||
|
||||
.PHONY: entities
|
||||
entities:
|
||||
pnpm --filter=backend run build:debug
|
||||
pnpm run migrate
|
||||
$(MAKE) -C ./packages/backend-rs regenerate-entities
|
||||
|
||||
.PHONY: napi-index
|
||||
napi-index:
|
||||
.PHONY: napi
|
||||
napi:
|
||||
$(MAKE) -C ./packages/backend-rs update-index
|
||||
|
||||
|
||||
|
|
3
ci/cargo/config.toml
Normal file
3
ci/cargo/config.toml
Normal file
|
@ -0,0 +1,3 @@
|
|||
[target.x86_64-unknown-linux-gnu]
|
||||
linker = "/usr/bin/clang"
|
||||
rustflags = ["-C", "link-arg=--ld-path=/usr/bin/mold"]
|
|
@ -7,6 +7,8 @@
|
|||
- Node.js
|
||||
- pnpm
|
||||
- Rust toolchain
|
||||
- Python 3
|
||||
- Perl
|
||||
- FFmpeg
|
||||
- Container runtime
|
||||
- [Docker](https://docs.docker.com/get-docker/)
|
||||
|
@ -31,7 +33,7 @@ You can refer to [local-installation.md](./local-installation.md) to install the
|
|||
1. Copy example config file
|
||||
```sh
|
||||
cp dev/config.example.env dev/config.env
|
||||
# If you use container runtime other than Docker, you need to modify the "COMPOSE" variable
|
||||
# If you use container runtime other than Podman, you need to modify the "COMPOSE" variable
|
||||
# vim dev/config.env
|
||||
```
|
||||
1. Create `.config/default.yml` with the following content
|
||||
|
@ -51,12 +53,7 @@ You can refer to [local-installation.md](./local-installation.md) to install the
|
|||
host: localhost
|
||||
port: 26379
|
||||
|
||||
logLevel: [
|
||||
'error',
|
||||
'success',
|
||||
'warning',
|
||||
'info'
|
||||
]
|
||||
maxlogLevel: 'debug' # or 'trace'
|
||||
```
|
||||
1. Start database containers
|
||||
```sh
|
||||
|
@ -84,6 +81,19 @@ You can refer to [local-installation.md](./local-installation.md) to install the
|
|||
DONE * [core boot] Now listening on port 3000 on http://localhost:3000
|
||||
```
|
||||
|
||||
## Update auto-generated files in `package/backend-rs`
|
||||
|
||||
You need to install `sea-orm-cli` to regenerate database entities.
|
||||
|
||||
```sh
|
||||
cargo install sea-orm-cli
|
||||
```
|
||||
|
||||
```sh
|
||||
make entities
|
||||
make napi
|
||||
```
|
||||
|
||||
## Reset the environment
|
||||
|
||||
You can recreate a fresh local Firefish environment by recreating the database containers:
|
||||
|
|
|
@ -141,12 +141,7 @@ sudo apt install ffmpeg
|
|||
host: localhost
|
||||
port: 6379
|
||||
|
||||
logLevel: [
|
||||
'error',
|
||||
'success',
|
||||
'warning',
|
||||
'info'
|
||||
]
|
||||
maxLogLevel: 'debug' # or 'trace'
|
||||
```
|
||||
|
||||
## 4. Build and start Firefish
|
||||
|
|
|
@ -2,6 +2,14 @@
|
|||
|
||||
Breaking changes are indicated by the :warning: icon.
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Adding `lang` to the response of `i` and the request parameter of `i/update`.
|
||||
|
||||
## v20240504
|
||||
|
||||
- :warning: Removed `release` endpoint.
|
||||
|
||||
## v20240424
|
||||
|
||||
- Added `antennaLimit` field to the response of `meta` and `admin/meta`, and the request of `admin/update-meta` (optional).
|
||||
|
|
|
@ -5,6 +5,10 @@ Critical security updates are indicated by the :warning: icon.
|
|||
- Server administrators should check [notice-for-admins.md](./notice-for-admins.md) as well.
|
||||
- Third-party client/bot developers may want to check [api-change.md](./api-change.md) as well.
|
||||
|
||||
## [v20240504](https://firefish.dev/firefish/firefish/-/merge_requests/10790/commits)
|
||||
|
||||
- Fix bugs
|
||||
|
||||
## :warning: [v20240430](https://firefish.dev/firefish/firefish/-/merge_requests/10781/commits)
|
||||
|
||||
- Add ability to group similar notifications
|
||||
|
|
|
@ -2,6 +2,8 @@ BEGIN;
|
|||
|
||||
DELETE FROM "migrations" WHERE name IN (
|
||||
'CreateScheduledNoteCreation1714728200194',
|
||||
'AddUserProfileLanguage1714888400293',
|
||||
'DropUnusedIndexes1714643926317',
|
||||
'AlterAkaType1714099399879',
|
||||
'AddDriveFileUsage1713451569342',
|
||||
'ConvertCwVarcharToText1713225866247',
|
||||
|
@ -29,6 +31,22 @@ DELETE FROM "migrations" WHERE name IN (
|
|||
-- create-scheduled-note-creation
|
||||
DROP TABLE "scheduled_note_creation";
|
||||
|
||||
-- drop-unused-indexes
|
||||
CREATE INDEX "IDX_01f4581f114e0ebd2bbb876f0b" ON "note_reaction" ("createdAt");
|
||||
CREATE INDEX "IDX_0610ebcfcfb4a18441a9bcdab2" ON "poll" ("userId");
|
||||
CREATE INDEX "IDX_25dfc71b0369b003a4cd434d0b" ON "note" ("attachedFileTypes");
|
||||
CREATE INDEX "IDX_2710a55f826ee236ea1a62698f" ON "hashtag" ("mentionedUsersCount");
|
||||
CREATE INDEX "IDX_4c02d38a976c3ae132228c6fce" ON "hashtag" ("mentionedRemoteUsersCount");
|
||||
CREATE INDEX "IDX_51c063b6a133a9cb87145450f5" ON "note" ("fileIds");
|
||||
CREATE INDEX "IDX_54ebcb6d27222913b908d56fd8" ON "note" ("mentions");
|
||||
CREATE INDEX "IDX_7fa20a12319c7f6dc3aed98c0a" ON "poll" ("userHost");
|
||||
CREATE INDEX "IDX_88937d94d7443d9a99a76fa5c0" ON "note" ("tags");
|
||||
CREATE INDEX "IDX_b11a5e627c41d4dc3170f1d370" ON "notification" ("createdAt");
|
||||
CREATE INDEX "IDX_c8dfad3b72196dd1d6b5db168a" ON "drive_file" ("createdAt");
|
||||
CREATE INDEX "IDX_d57f9030cd3af7f63ffb1c267c" ON "hashtag" ("attachedUsersCount");
|
||||
CREATE INDEX "IDX_e5848eac4940934e23dbc17581" ON "drive_file" ("uri");
|
||||
CREATE INDEX "IDX_fa99d777623947a5b05f394cae" ON "user" ("tags");
|
||||
|
||||
-- alter-aka-type
|
||||
ALTER TABLE "user" RENAME COLUMN "alsoKnownAs" TO "alsoKnownAsOld";
|
||||
ALTER TABLE "user" ADD COLUMN "alsoKnownAs" text;
|
||||
|
@ -751,9 +769,6 @@ CREATE SEQUENCE public.__chart_day__users_id_seq
|
|||
CACHE 1;
|
||||
ALTER SEQUENCE public.__chart_day__users_id_seq OWNED BY public.__chart_day__users.id;
|
||||
|
||||
-- drop-user-profile-language
|
||||
ALTER TABLE "user_profile" ADD COLUMN "lang" character varying(32);
|
||||
|
||||
-- emoji-moderator
|
||||
ALTER TABLE "user" DROP COLUMN "emojiModPerm";
|
||||
DROP TYPE "public"."user_emojimodperm_enum";
|
||||
|
|
|
@ -24,6 +24,7 @@ Firefish depends on the following software.
|
|||
- `build-essential` on Debian/Ubuntu Linux
|
||||
- `base-devel` on Arch Linux
|
||||
- [Python 3](https://www.python.org/)
|
||||
- [Perl](https://www.perl.org/)
|
||||
|
||||
This document shows an example procedure for installing these dependencies and Firefish on Debian 12. Note that there is much room for customizing the server setup; this document merely demonstrates a simple installation.
|
||||
|
||||
|
@ -325,7 +326,7 @@ cd ~/firefish
|
|||
- To add custom locales, place them in the `./custom/locales/` directory. If you name your custom locale the same as an existing locale, it will overwrite it. If you give it a unique name, it will be added to the list. Also make sure that the first part of the filename matches the locale you're basing it on. (Example: `en-FOO.yml`)
|
||||
- To add custom error images, place them in the `./custom/assets/badges` directory, replacing the files already there.
|
||||
- To add custom sounds, place only mp3 files in the `./custom/assets/sounds` directory.
|
||||
- To update custom assets without rebuilding, just run `pnpm run gulp`.
|
||||
- To update custom assets without rebuilding, just run `pnpm run build:assets`.
|
||||
- To block ChatGPT, CommonCrawl, or other crawlers from indexing your instance, uncomment the respective rules in `./custom/robots.txt`.
|
||||
|
||||
## Tips & Tricks
|
||||
|
|
|
@ -10,23 +10,21 @@ You can control the verbosity of the server log by adding `maxLogLevel` in `.con
|
|||
|
||||
### For systemd/pm2 users
|
||||
|
||||
Not only Firefish but also Node.js has recently fixed a few security issues:
|
||||
|
||||
- You need to install Perl to build Firefish. Since Git depends on Perl in many packaging systems, you probably already have Perl installed on your system. You can check the Perl version by this command:
|
||||
```sh
|
||||
perl --version
|
||||
```
|
||||
- Not only Firefish but also Node.js has recently fixed a few security issues:
|
||||
- https://nodejs.org/en/blog/vulnerability/april-2024-security-releases
|
||||
- https://nodejs.org/en/blog/vulnerability/april-2024-security-releases-2
|
||||
|
||||
So, it is highly recommended that you upgrade your Node.js version as well. The new versions are
|
||||
|
||||
- Node v18.20.2 (v18.x LTS)
|
||||
- Node v20.12.2 (v20.x LTS)
|
||||
- Node v21.7.3 (v21.x)
|
||||
|
||||
You can check your Node.js version by this command:
|
||||
|
||||
- You can check your Node.js version by this command:
|
||||
```sh
|
||||
node --version
|
||||
```
|
||||
|
||||
[Node v22](https://nodejs.org/en/blog/announcements/v22-release-announce) was also released several days ago, but we have not yet tested Firefish with this version.
|
||||
|
||||
## v20240413
|
||||
|
|
101
gulpfile.js
101
gulpfile.js
|
@ -1,101 +0,0 @@
|
|||
/**
|
||||
* Gulp tasks
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const gulp = require("gulp");
|
||||
const replace = require("gulp-replace");
|
||||
const terser = require("gulp-terser");
|
||||
const cssnano = require("gulp-cssnano");
|
||||
|
||||
const meta = require("./package.json");
|
||||
|
||||
gulp.task("copy:backend:views", () =>
|
||||
gulp
|
||||
.src("./packages/backend/src/server/web/views/**/*")
|
||||
.pipe(gulp.dest("./packages/backend/built/server/web/views")),
|
||||
);
|
||||
|
||||
gulp.task("copy:backend:custom", () =>
|
||||
gulp
|
||||
.src("./custom/assets/**/*")
|
||||
.pipe(gulp.dest("./packages/backend/assets/")),
|
||||
);
|
||||
|
||||
gulp.task("copy:client:fonts", () =>
|
||||
gulp
|
||||
.src("./packages/client/node_modules/three/examples/fonts/**/*")
|
||||
.pipe(gulp.dest("./built/_client_dist_/fonts/")),
|
||||
);
|
||||
|
||||
gulp.task("copy:client:locales", async (cb) => {
|
||||
fs.mkdirSync("./built/_client_dist_/locales", { recursive: true });
|
||||
const { default: locales } = await import("./locales/index.mjs");
|
||||
|
||||
const v = { _version_: meta.version };
|
||||
|
||||
for (const [lang, locale] of Object.entries(locales)) {
|
||||
fs.writeFileSync(
|
||||
`./built/_client_dist_/locales/${lang}.${meta.version}.json`,
|
||||
JSON.stringify({ ...locale, ...v }),
|
||||
"utf-8",
|
||||
);
|
||||
}
|
||||
|
||||
cb();
|
||||
});
|
||||
|
||||
gulp.task("build:backend:script", async () => {
|
||||
const { default: locales } = await import("./locales/index.mjs");
|
||||
|
||||
return gulp
|
||||
.src([
|
||||
"./packages/backend/src/server/web/boot.js",
|
||||
"./packages/backend/src/server/web/bios.js",
|
||||
"./packages/backend/src/server/web/cli.js",
|
||||
])
|
||||
.pipe(replace("SUPPORTED_LANGS", JSON.stringify(Object.keys(locales))))
|
||||
.pipe(
|
||||
terser({
|
||||
toplevel: true,
|
||||
}),
|
||||
)
|
||||
.pipe(gulp.dest("./packages/backend/built/server/web/"));
|
||||
});
|
||||
|
||||
gulp.task("build:backend:style", () => {
|
||||
return gulp
|
||||
.src([
|
||||
"./packages/backend/src/server/web/style.css",
|
||||
"./packages/backend/src/server/web/bios.css",
|
||||
"./packages/backend/src/server/web/cli.css",
|
||||
])
|
||||
.pipe(
|
||||
cssnano({
|
||||
zindex: false,
|
||||
}),
|
||||
)
|
||||
.pipe(gulp.dest("./packages/backend/built/server/web/"));
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
"build",
|
||||
gulp.parallel(
|
||||
"copy:client:locales",
|
||||
"copy:backend:views",
|
||||
"copy:backend:custom",
|
||||
"build:backend:script",
|
||||
"build:backend:style",
|
||||
"copy:client:fonts",
|
||||
),
|
||||
);
|
||||
|
||||
gulp.task("default", gulp.task("build"));
|
||||
|
||||
gulp.task("watch", () => {
|
||||
gulp.watch(
|
||||
["./packages/*/src/**/*"],
|
||||
{ ignoreInitial: false },
|
||||
gulp.task("build"),
|
||||
);
|
||||
});
|
|
@ -766,6 +766,9 @@ confirmToUnclipAlreadyClippedNote: "This post is already part of the \"{name}\"
|
|||
public: "Public"
|
||||
i18nInfo: "Firefish is being translated into various languages by volunteers. You
|
||||
can help at {link}."
|
||||
i18nServerInfo: "New clients will be in {language} by default."
|
||||
i18nServerChange: "Use {language} instead."
|
||||
i18nServerSet: "Use {language} for new clients."
|
||||
manageAccessTokens: "Manage access tokens"
|
||||
accountInfo: "Account Info"
|
||||
notesCount: "Number of posts"
|
||||
|
|
|
@ -685,6 +685,9 @@ unclip: "クリップ解除"
|
|||
confirmToUnclipAlreadyClippedNote: "この投稿はすでにクリップ「{name}」に含まれています。投稿をこのクリップから除外しますか?"
|
||||
public: "公開"
|
||||
i18nInfo: "Firefishは有志によって様々な言語に翻訳されています。{link}で翻訳に協力できます。"
|
||||
i18nServerInfo: "新しい端末では{language}が既定の言語になります。"
|
||||
i18nServerChange: "{language}に変更する。"
|
||||
i18nServerSet: "新しい端末での表示言語を{language}にします。"
|
||||
manageAccessTokens: "アクセストークンの管理"
|
||||
accountInfo: "アカウント情報"
|
||||
notesCount: "投稿の数"
|
||||
|
|
|
@ -667,6 +667,9 @@ unclip: "移除便签"
|
|||
confirmToUnclipAlreadyClippedNote: "本帖已包含在便签 \"{name}\" 里。您想要将本帖从该便签中移除吗?"
|
||||
public: "公开"
|
||||
i18nInfo: "Firefish 已经被志愿者们翻译成了各种语言。如果您也有兴趣,可以通过 {link} 帮助翻译。"
|
||||
i18nServerInfo: "新客户端将默认使用 {language}。"
|
||||
i18nServerChange: "改为 {language}。"
|
||||
i18nServerSet: "设定新客户端使用 {language}。"
|
||||
manageAccessTokens: "管理访问令牌"
|
||||
accountInfo: "账号信息"
|
||||
notesCount: "帖子数量"
|
||||
|
|
|
@ -661,6 +661,9 @@ unclip: "解除摘錄"
|
|||
confirmToUnclipAlreadyClippedNote: "此貼文已包含在摘錄「{name}」中。 你想將貼文從這個摘錄中排除嗎?"
|
||||
public: "公開"
|
||||
i18nInfo: "Firefish已經被志願者們翻譯成各種語言版本,如果想要幫忙的話,可以進入{link}幫助翻譯。"
|
||||
i18nServerInfo: "新客戶端將默認使用 {language}。"
|
||||
i18nServerChange: "改為 {language}。"
|
||||
i18nServerSet: "設定新客戶端使用 {language}。"
|
||||
manageAccessTokens: "管理存取權杖"
|
||||
accountInfo: "帳戶資訊"
|
||||
notesCount: "貼文數量"
|
||||
|
|
14
package.json
14
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "firefish",
|
||||
"version": "20240430",
|
||||
"version": "20240504",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://firefish.dev/firefish/firefish.git"
|
||||
|
@ -9,14 +9,15 @@
|
|||
"private": true,
|
||||
"scripts": {
|
||||
"rebuild": "pnpm run clean && pnpm run build",
|
||||
"build": "pnpm node ./scripts/build.mjs && pnpm run gulp",
|
||||
"build": "pnpm node ./scripts/build.mjs && pnpm run build:assets",
|
||||
"build:assets": "pnpm node ./scripts/copy-assets.mjs",
|
||||
"build:debug": "pnpm run clean && pnpm node ./scripts/dev-build.mjs && pnpm run build:assets",
|
||||
"start": "pnpm --filter backend run start",
|
||||
"start:container": "pnpm run gulp && pnpm run migrate && pnpm run start",
|
||||
"start:container": "pnpm run build:assets && pnpm run migrate && pnpm run start",
|
||||
"start:test": "pnpm --filter backend run start:test",
|
||||
"init": "pnpm run migrate",
|
||||
"migrate": "pnpm --filter backend run migration:run",
|
||||
"revertmigration": "pnpm --filter backend run migration:revert",
|
||||
"gulp": "gulp build",
|
||||
"watch": "pnpm run dev",
|
||||
"dev": "pnpm node ./scripts/dev.mjs",
|
||||
"dev:staging": "NODE_OPTIONS=--max_old_space_size=3072 NODE_ENV=development pnpm run build && pnpm run start",
|
||||
|
@ -24,7 +25,6 @@
|
|||
"lint:ts": "pnpm --filter !firefish-js -r --parallel run lint",
|
||||
"lint:rs": "cargo clippy --fix --allow-dirty --allow-staged && cargo fmt --all --",
|
||||
"debug": "pnpm run build:debug && pnpm run start",
|
||||
"build:debug": "pnpm run clean && pnpm node ./scripts/dev-build.mjs && pnpm run gulp",
|
||||
"mocha": "pnpm --filter backend run mocha",
|
||||
"test": "pnpm run test:ts && pnpm run test:rs",
|
||||
"test:ts": "pnpm run mocha",
|
||||
|
@ -38,10 +38,6 @@
|
|||
"clean-all": "pnpm run clean && pnpm run clean-cargo && pnpm run clean-npm"
|
||||
},
|
||||
"dependencies": {
|
||||
"gulp": "4.0.2",
|
||||
"gulp-cssnano": "2.1.3",
|
||||
"gulp-replace": "1.1.4",
|
||||
"gulp-terser": "2.1.0",
|
||||
"js-yaml": "4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -7,6 +7,7 @@ rust-version = "1.74"
|
|||
[features]
|
||||
default = []
|
||||
napi = ["dep:napi", "dep:napi-derive"]
|
||||
ci = []
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
@ -25,13 +26,13 @@ cuid2 = { workspace = true }
|
|||
emojis = { workspace = true }
|
||||
idna = { workspace = true }
|
||||
image = { workspace = true }
|
||||
isahc = { workspace = true }
|
||||
nom-exif = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
openssl = { workspace = true, features = ["vendored"] }
|
||||
rand = { workspace = true }
|
||||
redis = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["blocking"] }
|
||||
rmp-serde = { workspace = true }
|
||||
sea-orm = { workspace = true, features = ["sqlx-postgres", "runtime-tokio-rustls"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
|
122
packages/backend-rs/index.d.ts
vendored
122
packages/backend-rs/index.d.ts
vendored
|
@ -214,18 +214,25 @@ export function stringToAcct(acct: string): Acct
|
|||
export function acctToString(acct: Acct): string
|
||||
export function addNoteToAntenna(antennaId: string, note: Note): void
|
||||
/**
|
||||
* @param host punycoded instance host
|
||||
* @returns whether the given host should be blocked
|
||||
* Checks if a server is blocked.
|
||||
*
|
||||
* ## Argument
|
||||
* `host` - punycoded instance host
|
||||
*/
|
||||
export function isBlockedServer(host: string): Promise<boolean>
|
||||
/**
|
||||
* @param host punycoded instance host
|
||||
* @returns whether the given host should be limited
|
||||
* Checks if a server is silenced.
|
||||
*
|
||||
* ## Argument
|
||||
* `host` - punycoded instance host
|
||||
*/
|
||||
export function isSilencedServer(host: string): Promise<boolean>
|
||||
/**
|
||||
* @param host punycoded instance host
|
||||
* @returns whether the given host is allowlisted (this is always true if private mode is disabled)
|
||||
* Checks if a server is allowlisted.
|
||||
* Returns `Ok(true)` if private mode is disabled.
|
||||
*
|
||||
* ## Argument
|
||||
* `host` - punycoded instance host
|
||||
*/
|
||||
export function isAllowedServer(host: string): Promise<boolean>
|
||||
/** TODO: handle name collisions better */
|
||||
|
@ -261,6 +268,8 @@ export interface NoteLikeForGetNoteSummary {
|
|||
hasPoll: boolean
|
||||
}
|
||||
export function getNoteSummary(note: NoteLikeForGetNoteSummary): string
|
||||
export function isSafeUrl(url: string): boolean
|
||||
export function latestVersion(): Promise<string>
|
||||
export function toMastodonId(firefishId: string): string | null
|
||||
export function fromMastodonId(mastodonId: string): string | null
|
||||
export function fetchMeta(useCache: boolean): Promise<Meta>
|
||||
|
@ -1121,6 +1130,7 @@ export interface UserProfile {
|
|||
preventAiLearning: boolean
|
||||
isIndexable: boolean
|
||||
mutedPatterns: Array<string>
|
||||
lang: string | null
|
||||
}
|
||||
export interface UserPublickey {
|
||||
userId: string
|
||||
|
@ -1147,6 +1157,106 @@ export interface Webhook {
|
|||
latestStatus: number | null
|
||||
}
|
||||
export function initializeRustLogger(): void
|
||||
export function fetchNodeinfo(host: string): Promise<Nodeinfo>
|
||||
export function nodeinfo_2_1(): Promise<any>
|
||||
export function nodeinfo_2_0(): Promise<any>
|
||||
/** NodeInfo schema version 2.0. https://nodeinfo.diaspora.software/docson/index.html#/ns/schema/2.0 */
|
||||
export interface Nodeinfo {
|
||||
/** The schema version, must be 2.0. */
|
||||
version: string
|
||||
/** Metadata about server software in use. */
|
||||
software: Software20
|
||||
/** The protocols supported on this server. */
|
||||
protocols: Array<Protocol>
|
||||
/** The third party sites this server can connect to via their application API. */
|
||||
services: Services
|
||||
/** Whether this server allows open self-registration. */
|
||||
openRegistrations: boolean
|
||||
/** Usage statistics for this server. */
|
||||
usage: Usage
|
||||
/** Free form key value pairs for software specific values. Clients should not rely on any specific key present. */
|
||||
metadata: Record<string, any>
|
||||
}
|
||||
/** Metadata about server software in use (version 2.0). */
|
||||
export interface Software20 {
|
||||
/** The canonical name of this server software. */
|
||||
name: string
|
||||
/** The version of this server software. */
|
||||
version: string
|
||||
}
|
||||
export enum Protocol {
|
||||
Activitypub = 'activitypub',
|
||||
Buddycloud = 'buddycloud',
|
||||
Dfrn = 'dfrn',
|
||||
Diaspora = 'diaspora',
|
||||
Libertree = 'libertree',
|
||||
Ostatus = 'ostatus',
|
||||
Pumpio = 'pumpio',
|
||||
Tent = 'tent',
|
||||
Xmpp = 'xmpp',
|
||||
Zot = 'zot'
|
||||
}
|
||||
/** The third party sites this server can connect to via their application API. */
|
||||
export interface Services {
|
||||
/** The third party sites this server can retrieve messages from for combined display with regular traffic. */
|
||||
inbound: Array<Inbound>
|
||||
/** The third party sites this server can publish messages to on the behalf of a user. */
|
||||
outbound: Array<Outbound>
|
||||
}
|
||||
/** The third party sites this server can retrieve messages from for combined display with regular traffic. */
|
||||
export enum Inbound {
|
||||
Atom1 = 'atom1',
|
||||
Gnusocial = 'gnusocial',
|
||||
Imap = 'imap',
|
||||
Pnut = 'pnut',
|
||||
Pop3 = 'pop3',
|
||||
Pumpio = 'pumpio',
|
||||
Rss2 = 'rss2',
|
||||
Twitter = 'twitter'
|
||||
}
|
||||
/** The third party sites this server can publish messages to on the behalf of a user. */
|
||||
export enum Outbound {
|
||||
Atom1 = 'atom1',
|
||||
Blogger = 'blogger',
|
||||
Buddycloud = 'buddycloud',
|
||||
Diaspora = 'diaspora',
|
||||
Dreamwidth = 'dreamwidth',
|
||||
Drupal = 'drupal',
|
||||
Facebook = 'facebook',
|
||||
Friendica = 'friendica',
|
||||
Gnusocial = 'gnusocial',
|
||||
Google = 'google',
|
||||
Insanejournal = 'insanejournal',
|
||||
Libertree = 'libertree',
|
||||
Linkedin = 'linkedin',
|
||||
Livejournal = 'livejournal',
|
||||
Mediagoblin = 'mediagoblin',
|
||||
Myspace = 'myspace',
|
||||
Pinterest = 'pinterest',
|
||||
Pnut = 'pnut',
|
||||
Posterous = 'posterous',
|
||||
Pumpio = 'pumpio',
|
||||
Redmatrix = 'redmatrix',
|
||||
Rss2 = 'rss2',
|
||||
Smtp = 'smtp',
|
||||
Tent = 'tent',
|
||||
Tumblr = 'tumblr',
|
||||
Twitter = 'twitter',
|
||||
Wordpress = 'wordpress',
|
||||
Xmpp = 'xmpp'
|
||||
}
|
||||
/** Usage statistics for this server. */
|
||||
export interface Usage {
|
||||
users: Users
|
||||
localPosts: number | null
|
||||
localComments: number | null
|
||||
}
|
||||
/** statistics about the users of this server. */
|
||||
export interface Users {
|
||||
total: number | null
|
||||
activeHalfyear: number | null
|
||||
activeMonth: number | null
|
||||
}
|
||||
export function watchNote(watcherId: string, noteAuthorId: string, noteId: string): Promise<void>
|
||||
export function unwatchNote(watcherId: string, noteId: string): Promise<void>
|
||||
export function publishToChannelStream(channelId: string, userId: string): void
|
||||
|
|
|
@ -310,7 +310,7 @@ if (!nativeBinding) {
|
|||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
const { SECOND, MINUTE, HOUR, DAY, USER_ONLINE_THRESHOLD, USER_ACTIVE_THRESHOLD, FILE_TYPE_BROWSERSAFE, loadEnv, loadConfig, stringToAcct, acctToString, addNoteToAntenna, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initializeRustLogger, watchNote, unwatchNote, publishToChannelStream, ChatEvent, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, getTimestamp, genId, genIdAt, secureRndstr } = nativeBinding
|
||||
const { SECOND, MINUTE, HOUR, DAY, USER_ONLINE_THRESHOLD, USER_ACTIVE_THRESHOLD, FILE_TYPE_BROWSERSAFE, loadEnv, loadConfig, stringToAcct, acctToString, addNoteToAntenna, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, isSafeUrl, latestVersion, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initializeRustLogger, fetchNodeinfo, nodeinfo_2_1, nodeinfo_2_0, Protocol, Inbound, Outbound, watchNote, unwatchNote, publishToChannelStream, ChatEvent, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, getTimestamp, genId, genIdAt, secureRndstr } = nativeBinding
|
||||
|
||||
module.exports.SECOND = SECOND
|
||||
module.exports.MINUTE = MINUTE
|
||||
|
@ -339,6 +339,8 @@ module.exports.safeForSql = safeForSql
|
|||
module.exports.formatMilliseconds = formatMilliseconds
|
||||
module.exports.getImageSizeFromUrl = getImageSizeFromUrl
|
||||
module.exports.getNoteSummary = getNoteSummary
|
||||
module.exports.isSafeUrl = isSafeUrl
|
||||
module.exports.latestVersion = latestVersion
|
||||
module.exports.toMastodonId = toMastodonId
|
||||
module.exports.fromMastodonId = fromMastodonId
|
||||
module.exports.fetchMeta = fetchMeta
|
||||
|
@ -363,6 +365,12 @@ module.exports.UserEmojimodpermEnum = UserEmojimodpermEnum
|
|||
module.exports.UserProfileFfvisibilityEnum = UserProfileFfvisibilityEnum
|
||||
module.exports.UserProfileMutingnotificationtypesEnum = UserProfileMutingnotificationtypesEnum
|
||||
module.exports.initializeRustLogger = initializeRustLogger
|
||||
module.exports.fetchNodeinfo = fetchNodeinfo
|
||||
module.exports.nodeinfo_2_1 = nodeinfo_2_1
|
||||
module.exports.nodeinfo_2_0 = nodeinfo_2_0
|
||||
module.exports.Protocol = Protocol
|
||||
module.exports.Inbound = Inbound
|
||||
module.exports.Outbound = Outbound
|
||||
module.exports.watchNote = watchNote
|
||||
module.exports.unwatchNote = unwatchNote
|
||||
module.exports.publishToChannelStream = publishToChannelStream
|
||||
|
|
290
packages/backend-rs/src/database/cache.rs
Normal file
290
packages/backend-rs/src/database/cache.rs
Normal file
|
@ -0,0 +1,290 @@
|
|||
use crate::database::{redis_conn, redis_key};
|
||||
use redis::{Commands, RedisError};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(strum::Display, Debug)]
|
||||
pub enum Category {
|
||||
#[strum(serialize = "fetchUrl")]
|
||||
FetchUrl,
|
||||
#[cfg(test)]
|
||||
#[strum(serialize = "usedOnlyForTesting")]
|
||||
Test,
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Redis error: {0}")]
|
||||
RedisError(#[from] RedisError),
|
||||
#[error("Data serialization error: {0}")]
|
||||
SerializeError(#[from] rmp_serde::encode::Error),
|
||||
#[error("Data deserialization error: {0}")]
|
||||
DeserializeError(#[from] rmp_serde::decode::Error),
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn prefix_key(key: &str) -> String {
|
||||
redis_key(format!("cache:{}", key))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn categorize(category: Category, key: &str) -> String {
|
||||
format!("{}:{}", category, key)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn wildcard(category: Category) -> String {
|
||||
prefix_key(&categorize(category, "*"))
|
||||
}
|
||||
|
||||
/// Sets a Redis cache.
|
||||
///
|
||||
/// This overwrites the exsisting cache with the same key.
|
||||
///
|
||||
/// ## Arguments
|
||||
///
|
||||
/// * `key` - key (will be prefixed automatically)
|
||||
/// * `value` - (de)serializable value
|
||||
/// * `expire_seconds` - TTL
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// use backend_rs::database::cache;
|
||||
/// let key = "apple";
|
||||
/// let data = "I want to cache this string".to_string();
|
||||
///
|
||||
/// // caches the data for 10 seconds
|
||||
/// cache::set(key, &data, 10);
|
||||
///
|
||||
/// // get the cache
|
||||
/// let cached_data = cache::get::<String>(key).unwrap();
|
||||
/// assert_eq!(data, cached_data.unwrap());
|
||||
/// ```
|
||||
pub fn set<V: for<'a> Deserialize<'a> + Serialize>(
|
||||
key: &str,
|
||||
value: &V,
|
||||
expire_seconds: u64,
|
||||
) -> Result<(), Error> {
|
||||
redis_conn()?.set_ex(
|
||||
prefix_key(key),
|
||||
rmp_serde::encode::to_vec(&value)?,
|
||||
expire_seconds,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets a Redis cache.
|
||||
///
|
||||
/// If the Redis connection is fine, this returns `Ok(data)` where `data`
|
||||
/// is the cached value. Returns `Ok(None)` if there is no value corresponding to `key`.
|
||||
///
|
||||
/// ## Arguments
|
||||
///
|
||||
/// * `key` - key (will be prefixed automatically)
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// use backend_rs::database::cache;
|
||||
///
|
||||
/// let key = "banana";
|
||||
/// let data = "I want to cache this string".to_string();
|
||||
///
|
||||
/// // set cache
|
||||
/// cache::set(key, &data, 10).unwrap();
|
||||
///
|
||||
/// // get cache
|
||||
/// let cached_data = cache::get::<String>(key).unwrap();
|
||||
/// assert_eq!(data, cached_data.unwrap());
|
||||
///
|
||||
/// // get nonexistent (or expired) cache
|
||||
/// let no_cache = cache::get::<String>("nonexistent").unwrap();
|
||||
/// assert!(no_cache.is_none());
|
||||
/// ```
|
||||
pub fn get<V: for<'a> Deserialize<'a> + Serialize>(key: &str) -> Result<Option<V>, Error> {
|
||||
let serialized_value: Option<Vec<u8>> = redis_conn()?.get(prefix_key(key))?;
|
||||
Ok(match serialized_value {
|
||||
Some(v) => Some(rmp_serde::from_slice::<V>(v.as_ref())?),
|
||||
None => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Deletes a Redis cache.
|
||||
///
|
||||
/// If the Redis connection is fine, this returns `Ok(())`
|
||||
/// regardless of whether the cache exists.
|
||||
///
|
||||
/// ## Arguments
|
||||
///
|
||||
/// * `key` - key (will be prefixed automatically)
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// use backend_rs::database::cache::{set, get, delete};
|
||||
///
|
||||
/// let key = "chocolate";
|
||||
/// let value = "I want to cache this string".to_string();
|
||||
///
|
||||
/// // set cache
|
||||
/// set(key, &value, 10).unwrap();
|
||||
///
|
||||
/// // delete the cache
|
||||
/// delete("foo").unwrap();
|
||||
/// delete("nonexistent").unwrap(); // this is okay
|
||||
///
|
||||
/// // the cache is gone
|
||||
/// let cached_value = get::<String>("foo").unwrap();
|
||||
/// assert!(cached_value.is_none());
|
||||
/// ```
|
||||
pub fn delete(key: &str) -> Result<(), Error> {
|
||||
Ok(redis_conn()?.del(prefix_key(key))?)
|
||||
}
|
||||
|
||||
/// Sets a Redis cache under a `category`.
|
||||
///
|
||||
/// The usage is the same as [set], except that you need to
|
||||
/// use [get_one] and [delete_one] to get/delete the cache.
|
||||
///
|
||||
/// ## Arguments
|
||||
///
|
||||
/// * `category` - one of [Category]
|
||||
/// * `key` - key (will be prefixed automatically)
|
||||
/// * `value` - (de)serializable value
|
||||
/// * `expire_seconds` - TTL
|
||||
pub fn set_one<V: for<'a> Deserialize<'a> + Serialize>(
|
||||
category: Category,
|
||||
key: &str,
|
||||
value: &V,
|
||||
expire_seconds: u64,
|
||||
) -> Result<(), Error> {
|
||||
set(&categorize(category, key), value, expire_seconds)
|
||||
}
|
||||
|
||||
/// Gets a Redis cache under a `category`.
|
||||
///
|
||||
/// The usage is basically the same as [get].
|
||||
///
|
||||
/// ## Arguments
|
||||
///
|
||||
/// * `category` - one of [Category]
|
||||
/// * `key` - key (will be prefixed automatically)
|
||||
pub fn get_one<V: for<'a> Deserialize<'a> + Serialize>(
|
||||
category: Category,
|
||||
key: &str,
|
||||
) -> Result<Option<V>, Error> {
|
||||
get(&categorize(category, key))
|
||||
}
|
||||
|
||||
/// Deletes a Redis cache under a `category`.
|
||||
///
|
||||
/// The usage is basically the same as [delete].
|
||||
///
|
||||
/// ## Arguments
|
||||
///
|
||||
/// * `category` - one of [Category]
|
||||
/// * `key` - key (will be prefixed automatically)
|
||||
pub fn delete_one(category: Category, key: &str) -> Result<(), Error> {
|
||||
delete(&categorize(category, key))
|
||||
}
|
||||
|
||||
/// Deletes all Redis caches under a `category`.
|
||||
///
|
||||
/// ## Arguments
|
||||
///
|
||||
/// * `category` - one of [Category]
|
||||
pub fn delete_all(category: Category) -> Result<(), Error> {
|
||||
let mut redis = redis_conn()?;
|
||||
let keys: Vec<Vec<u8>> = redis.keys(wildcard(category))?;
|
||||
|
||||
if !keys.is_empty() {
|
||||
redis.del(keys)?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: set_all(), get_all()
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use crate::database::cache::delete_one;
|
||||
|
||||
use super::{delete_all, get, get_one, set, set_one, Category::Test};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn set_get_expire() {
|
||||
#[derive(serde::Deserialize, serde::Serialize, PartialEq, Debug)]
|
||||
struct Data {
|
||||
id: u32,
|
||||
kind: String,
|
||||
}
|
||||
|
||||
let key_1 = "CARGO_TEST_CACHE_KEY_1";
|
||||
let value_1: Vec<i32> = vec![1, 2, 3, 4, 5];
|
||||
|
||||
let key_2 = "CARGO_TEST_CACHE_KEY_2";
|
||||
let value_2 = "Hello fedizens".to_string();
|
||||
|
||||
let key_3 = "CARGO_TEST_CACHE_KEY_3";
|
||||
let value_3 = Data {
|
||||
id: 1000000007,
|
||||
kind: "prime number".to_string(),
|
||||
};
|
||||
|
||||
set(key_1, &value_1, 1).unwrap();
|
||||
set(key_2, &value_2, 1).unwrap();
|
||||
set(key_3, &value_3, 1).unwrap();
|
||||
|
||||
let cached_value_1: Vec<i32> = get(key_1).unwrap().unwrap();
|
||||
let cached_value_2: String = get(key_2).unwrap().unwrap();
|
||||
let cached_value_3: Data = get(key_3).unwrap().unwrap();
|
||||
|
||||
assert_eq!(value_1, cached_value_1);
|
||||
assert_eq!(value_2, cached_value_2);
|
||||
assert_eq!(value_3, cached_value_3);
|
||||
|
||||
// wait for the cache to expire
|
||||
std::thread::sleep(std::time::Duration::from_millis(1100));
|
||||
|
||||
let expired_value_1: Option<Vec<i32>> = get(key_1).unwrap();
|
||||
let expired_value_2: Option<Vec<i32>> = get(key_2).unwrap();
|
||||
let expired_value_3: Option<Vec<i32>> = get(key_3).unwrap();
|
||||
|
||||
assert!(expired_value_1.is_none());
|
||||
assert!(expired_value_2.is_none());
|
||||
assert!(expired_value_3.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn use_category() {
|
||||
let key_1 = "fire";
|
||||
let key_2 = "fish";
|
||||
let key_3 = "awawa";
|
||||
|
||||
let value_1 = "hello".to_string();
|
||||
let value_2 = 998244353u32;
|
||||
let value_3 = 'あ';
|
||||
|
||||
set_one(Test, key_1, &value_1, 5 * 60).unwrap();
|
||||
set_one(Test, key_2, &value_2, 5 * 60).unwrap();
|
||||
set_one(Test, key_3, &value_3, 5 * 60).unwrap();
|
||||
|
||||
assert_eq!(get_one::<String>(Test, key_1).unwrap().unwrap(), value_1);
|
||||
assert_eq!(get_one::<u32>(Test, key_2).unwrap().unwrap(), value_2);
|
||||
assert_eq!(get_one::<char>(Test, key_3).unwrap().unwrap(), value_3);
|
||||
|
||||
delete_one(Test, key_1).unwrap();
|
||||
|
||||
assert!(get_one::<String>(Test, key_1).unwrap().is_none());
|
||||
assert!(get_one::<u32>(Test, key_2).unwrap().is_some());
|
||||
assert!(get_one::<char>(Test, key_3).unwrap().is_some());
|
||||
|
||||
delete_all(Test).unwrap();
|
||||
|
||||
assert!(get_one::<String>(Test, key_1).unwrap().is_none());
|
||||
assert!(get_one::<u32>(Test, key_2).unwrap().is_none());
|
||||
assert!(get_one::<char>(Test, key_3).unwrap().is_none());
|
||||
}
|
||||
}
|
|
@ -2,5 +2,6 @@ pub use postgresql::db_conn;
|
|||
pub use redis::key as redis_key;
|
||||
pub use redis::redis_conn;
|
||||
|
||||
pub mod cache;
|
||||
pub mod postgresql;
|
||||
pub mod redis;
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
use crate::misc::meta::fetch_meta;
|
||||
use sea_orm::DbErr;
|
||||
|
||||
/**
|
||||
* @param host punycoded instance host
|
||||
* @returns whether the given host should be blocked
|
||||
*/
|
||||
/// Checks if a server is blocked.
|
||||
///
|
||||
/// ## Argument
|
||||
/// `host` - punycoded instance host
|
||||
#[crate::export]
|
||||
pub async fn is_blocked_server(host: &str) -> Result<bool, DbErr> {
|
||||
Ok(fetch_meta(true)
|
||||
|
@ -16,10 +16,10 @@ pub async fn is_blocked_server(host: &str) -> Result<bool, DbErr> {
|
|||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param host punycoded instance host
|
||||
* @returns whether the given host should be limited
|
||||
*/
|
||||
/// Checks if a server is silenced.
|
||||
///
|
||||
/// ## Argument
|
||||
/// `host` - punycoded instance host
|
||||
#[crate::export]
|
||||
pub async fn is_silenced_server(host: &str) -> Result<bool, DbErr> {
|
||||
Ok(fetch_meta(true)
|
||||
|
@ -31,10 +31,11 @@ pub async fn is_silenced_server(host: &str) -> Result<bool, DbErr> {
|
|||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param host punycoded instance host
|
||||
* @returns whether the given host is allowlisted (this is always true if private mode is disabled)
|
||||
*/
|
||||
/// Checks if a server is allowlisted.
|
||||
/// Returns `Ok(true)` if private mode is disabled.
|
||||
///
|
||||
/// ## Argument
|
||||
/// `host` - punycoded instance host
|
||||
#[crate::export]
|
||||
pub async fn is_allowed_server(host: &str) -> Result<bool, DbErr> {
|
||||
let meta = fetch_meta(true).await?;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::misc::redis_cache::{get_cache, set_cache, CacheError};
|
||||
use crate::database::cache;
|
||||
use crate::util::http_client;
|
||||
use image::{io::Reader, ImageError, ImageFormat};
|
||||
use isahc::ReadResponseExt;
|
||||
use nom_exif::{parse_jpeg_exif, EntryValue, ExifTag};
|
||||
use std::io::Cursor;
|
||||
use tokio::sync::Mutex;
|
||||
|
@ -8,9 +9,13 @@ use tokio::sync::Mutex;
|
|||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Redis cache error: {0}")]
|
||||
CacheErr(#[from] CacheError),
|
||||
#[error("Reqwest error: {0}")]
|
||||
ReqwestErr(#[from] reqwest::Error),
|
||||
CacheErr(#[from] cache::Error),
|
||||
#[error("HTTP client aquisition error: {0}")]
|
||||
HttpClientErr(#[from] http_client::Error),
|
||||
#[error("Isahc error: {0}")]
|
||||
IsahcErr(#[from] isahc::Error),
|
||||
#[error("HTTP error: {0}")]
|
||||
HttpErr(String),
|
||||
#[error("Image decoding error: {0}")]
|
||||
ImageErr(#[from] ImageError),
|
||||
#[error("Image decoding error: {0}")]
|
||||
|
@ -50,11 +55,10 @@ pub async fn get_image_size_from_url(url: &str) -> Result<ImageSize, Error> {
|
|||
{
|
||||
let _ = MTX_GUARD.lock().await;
|
||||
|
||||
let key = format!("fetchImage:{}", url);
|
||||
attempted = get_cache::<bool>(&key)?.is_some();
|
||||
attempted = cache::get_one::<bool>(cache::Category::FetchUrl, url)?.is_some();
|
||||
|
||||
if !attempted {
|
||||
set_cache(&key, &true, 10 * 60)?;
|
||||
cache::set_one(cache::Category::FetchUrl, url, &true, 10 * 60)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,7 +69,16 @@ pub async fn get_image_size_from_url(url: &str) -> Result<ImageSize, Error> {
|
|||
|
||||
tracing::info!("retrieving image size from {}", url);
|
||||
|
||||
let image_bytes = http_client()?.get(url).send().await?.bytes().await?;
|
||||
let mut response = http_client::client()?.get(url)?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
tracing::info!("status: {}", response.status());
|
||||
tracing::debug!("response body: {:#?}", response.body());
|
||||
return Err(Error::HttpErr(format!("Failed to get image from {}", url)));
|
||||
}
|
||||
|
||||
let image_bytes = response.bytes()?;
|
||||
|
||||
let reader = Reader::new(Cursor::new(&image_bytes)).with_guessed_format()?;
|
||||
|
||||
let format = reader.format();
|
||||
|
@ -109,7 +122,7 @@ pub async fn get_image_size_from_url(url: &str) -> Result<ImageSize, Error> {
|
|||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::{get_image_size_from_url, ImageSize};
|
||||
use crate::misc::redis_cache::delete_cache;
|
||||
use crate::database::cache;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -124,17 +137,8 @@ mod unit_test {
|
|||
let gif_url = "https://firefish.dev/firefish/firefish/-/raw/b9c3dfbd3d473cb2cee20c467eeae780bc401271/packages/backend/test/resources/anime.gif";
|
||||
let mp3_url = "https://firefish.dev/firefish/firefish/-/blob/5891a90f71a8b9d5ea99c683ade7e485c685d642/packages/backend/assets/sounds/aisha/1.mp3";
|
||||
|
||||
// Delete caches in case you run this test multiple times
|
||||
// (should be disabled in CI tasks)
|
||||
delete_cache(&format!("fetchImage:{}", png_url_1)).unwrap();
|
||||
delete_cache(&format!("fetchImage:{}", png_url_2)).unwrap();
|
||||
delete_cache(&format!("fetchImage:{}", png_url_3)).unwrap();
|
||||
delete_cache(&format!("fetchImage:{}", rotated_jpeg_url)).unwrap();
|
||||
delete_cache(&format!("fetchImage:{}", webp_url_1)).unwrap();
|
||||
delete_cache(&format!("fetchImage:{}", webp_url_2)).unwrap();
|
||||
delete_cache(&format!("fetchImage:{}", ico_url)).unwrap();
|
||||
delete_cache(&format!("fetchImage:{}", gif_url)).unwrap();
|
||||
delete_cache(&format!("fetchImage:{}", mp3_url)).unwrap();
|
||||
// delete caches in case you run this test multiple times
|
||||
cache::delete_all(cache::Category::FetchUrl).unwrap();
|
||||
|
||||
let png_size_1 = ImageSize {
|
||||
width: 1024,
|
||||
|
@ -197,4 +201,15 @@ mod unit_test {
|
|||
assert_eq!(gif_size, get_image_size_from_url(gif_url).await.unwrap());
|
||||
assert!(get_image_size_from_url(mp3_url).await.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn too_many_attempts() {
|
||||
let url = "https://firefish.dev/firefish/firefish/-/raw/5891a90f71a8b9d5ea99c683ade7e485c685d642/packages/backend/assets/splash.png";
|
||||
|
||||
// delete caches in case you run this test multiple times
|
||||
cache::delete_one(cache::Category::FetchUrl, url).unwrap();
|
||||
|
||||
assert!(get_image_size_from_url(url).await.is_ok());
|
||||
assert!(get_image_size_from_url(url).await.is_err());
|
||||
}
|
||||
}
|
||||
|
|
34
packages/backend-rs/src/misc/is_safe_url.rs
Normal file
34
packages/backend-rs/src/misc/is_safe_url.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
#[crate::export]
|
||||
pub fn is_safe_url(url: &str) -> bool {
|
||||
if let Ok(url) = url.parse::<url::Url>() {
|
||||
if url.host_str().unwrap_or_default() == "unix"
|
||||
|| !["http", "https"].contains(&url.scheme())
|
||||
|| ![None, Some(80), Some(443)].contains(&url.port())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::is_safe_url;
|
||||
|
||||
#[test]
|
||||
fn safe_url() {
|
||||
assert!(is_safe_url("http://firefish.dev/firefish/firefish"));
|
||||
assert!(is_safe_url("https://firefish.dev/firefish/firefish"));
|
||||
assert!(is_safe_url("http://firefish.dev:80/firefish/firefish"));
|
||||
assert!(is_safe_url("https://firefish.dev:80/firefish/firefish"));
|
||||
assert!(is_safe_url("http://firefish.dev:443/firefish/firefish"));
|
||||
assert!(is_safe_url("https://firefish.dev:443/firefish/firefish"));
|
||||
assert!(!is_safe_url("https://unix/firefish/firefish"));
|
||||
assert!(!is_safe_url("https://firefish.dev:35/firefish/firefish"));
|
||||
assert!(!is_safe_url("ftp://firefish.dev/firefish/firefish"));
|
||||
assert!(!is_safe_url("nyaa"));
|
||||
assert!(!is_safe_url(""));
|
||||
}
|
||||
}
|
108
packages/backend-rs/src/misc/latest_version.rs
Normal file
108
packages/backend-rs/src/misc/latest_version.rs
Normal file
|
@ -0,0 +1,108 @@
|
|||
use crate::database::cache;
|
||||
use crate::util::http_client;
|
||||
use isahc::ReadResponseExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Cache error: {0}")]
|
||||
CacheErr(#[from] cache::Error),
|
||||
#[error("Isahc error: {0}")]
|
||||
IsahcErr(#[from] isahc::Error),
|
||||
#[error("HTTP client aquisition error: {0}")]
|
||||
HttpClientErr(#[from] http_client::Error),
|
||||
#[error("HTTP error: {0}")]
|
||||
HttpErr(String),
|
||||
#[error("Response parsing error: {0}")]
|
||||
IoErr(#[from] std::io::Error),
|
||||
#[error("Failed to deserialize JSON: {0}")]
|
||||
JsonErr(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
const UPSTREAM_PACKAGE_JSON_URL: &str =
|
||||
"https://firefish.dev/firefish/firefish/-/raw/main/package.json";
|
||||
|
||||
async fn get_latest_version() -> Result<String, Error> {
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct Response {
|
||||
version: String,
|
||||
}
|
||||
|
||||
let mut response = http_client::client()?.get(UPSTREAM_PACKAGE_JSON_URL)?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
tracing::info!("status: {}", response.status());
|
||||
tracing::debug!("response body: {:#?}", response.body());
|
||||
return Err(Error::HttpErr(
|
||||
"Failed to fetch version from Firefish GitLab".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let res_parsed: Response = serde_json::from_str(&response.text()?)?;
|
||||
|
||||
Ok(res_parsed.version)
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub async fn latest_version() -> Result<String, Error> {
|
||||
let version: Option<String> =
|
||||
cache::get_one(cache::Category::FetchUrl, UPSTREAM_PACKAGE_JSON_URL)?;
|
||||
|
||||
if let Some(v) = version {
|
||||
tracing::trace!("use cached value: {}", v);
|
||||
Ok(v)
|
||||
} else {
|
||||
tracing::trace!("cache is expired, fetching the latest version");
|
||||
let fetched_version = get_latest_version().await?;
|
||||
tracing::trace!("fetched value: {}", fetched_version);
|
||||
|
||||
cache::set_one(
|
||||
cache::Category::FetchUrl,
|
||||
UPSTREAM_PACKAGE_JSON_URL,
|
||||
&fetched_version,
|
||||
3 * 60 * 60,
|
||||
)?;
|
||||
Ok(fetched_version)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::{latest_version, UPSTREAM_PACKAGE_JSON_URL};
|
||||
use crate::database::cache;
|
||||
|
||||
fn validate_version(version: String) {
|
||||
// version: YYYYMMDD or YYYYMMDD-X
|
||||
assert!(version.len() >= 8);
|
||||
assert!(version[..8].chars().all(|c| c.is_ascii_digit()));
|
||||
|
||||
// YYYY
|
||||
assert!(&version[..4] >= "2024");
|
||||
|
||||
// MM
|
||||
assert!(&version[4..6] >= "01");
|
||||
assert!(&version[4..6] <= "12");
|
||||
|
||||
// DD
|
||||
assert!(&version[6..8] >= "01");
|
||||
assert!(&version[6..8] <= "31");
|
||||
|
||||
// -X
|
||||
if version.len() > 8 {
|
||||
assert!(version.chars().nth(8).unwrap() == '-');
|
||||
assert!(version[9..].chars().all(|c| c.is_ascii_digit()));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn check_version() {
|
||||
// delete caches in case you run this test multiple times
|
||||
cache::delete_one(cache::Category::FetchUrl, UPSTREAM_PACKAGE_JSON_URL).unwrap();
|
||||
|
||||
// fetch from firefish.dev
|
||||
validate_version(latest_version().await.unwrap());
|
||||
|
||||
// use cache
|
||||
validate_version(latest_version().await.unwrap());
|
||||
}
|
||||
}
|
|
@ -8,10 +8,11 @@ pub mod escape_sql;
|
|||
pub mod format_milliseconds;
|
||||
pub mod get_image_size;
|
||||
pub mod get_note_summary;
|
||||
pub mod is_safe_url;
|
||||
pub mod latest_version;
|
||||
pub mod mastodon_id;
|
||||
pub mod meta;
|
||||
pub mod nyaify;
|
||||
pub mod password;
|
||||
pub mod reaction;
|
||||
pub mod redis_cache;
|
||||
pub mod remove_old_attestation_challenges;
|
||||
|
|
|
@ -1,94 +0,0 @@
|
|||
use crate::database::{redis_conn, redis_key};
|
||||
use redis::{Commands, RedisError};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum CacheError {
|
||||
#[error("Redis error: {0}")]
|
||||
RedisError(#[from] RedisError),
|
||||
#[error("Data serialization error: {0}")]
|
||||
SerializeError(#[from] rmp_serde::encode::Error),
|
||||
#[error("Data deserialization error: {0}")]
|
||||
DeserializeError(#[from] rmp_serde::decode::Error),
|
||||
}
|
||||
|
||||
fn prefix_key(key: &str) -> String {
|
||||
redis_key(format!("cache:{}", key))
|
||||
}
|
||||
|
||||
pub fn set_cache<V: for<'a> Deserialize<'a> + Serialize>(
|
||||
key: &str,
|
||||
value: &V,
|
||||
expire_seconds: u64,
|
||||
) -> Result<(), CacheError> {
|
||||
redis_conn()?.set_ex(
|
||||
prefix_key(key),
|
||||
rmp_serde::encode::to_vec(&value)?,
|
||||
expire_seconds,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_cache<V: for<'a> Deserialize<'a> + Serialize>(
|
||||
key: &str,
|
||||
) -> Result<Option<V>, CacheError> {
|
||||
let serialized_value: Option<Vec<u8>> = redis_conn()?.get(prefix_key(key))?;
|
||||
Ok(match serialized_value {
|
||||
Some(v) => Some(rmp_serde::from_slice::<V>(v.as_ref())?),
|
||||
None => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_cache(key: &str) -> Result<(), CacheError> {
|
||||
Ok(redis_conn()?.del(prefix_key(key))?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::{get_cache, set_cache};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn set_get_expire() {
|
||||
#[derive(serde::Deserialize, serde::Serialize, PartialEq, Debug)]
|
||||
struct Data {
|
||||
id: u32,
|
||||
kind: String,
|
||||
}
|
||||
|
||||
let key_1 = "CARGO_TEST_CACHE_KEY_1";
|
||||
let value_1: Vec<i32> = vec![1, 2, 3, 4, 5];
|
||||
|
||||
let key_2 = "CARGO_TEST_CACHE_KEY_2";
|
||||
let value_2 = "Hello fedizens".to_string();
|
||||
|
||||
let key_3 = "CARGO_TEST_CACHE_KEY_3";
|
||||
let value_3 = Data {
|
||||
id: 1000000007,
|
||||
kind: "prime number".to_string(),
|
||||
};
|
||||
|
||||
set_cache(key_1, &value_1, 1).unwrap();
|
||||
set_cache(key_2, &value_2, 1).unwrap();
|
||||
set_cache(key_3, &value_3, 1).unwrap();
|
||||
|
||||
let cached_value_1: Vec<i32> = get_cache(key_1).unwrap().unwrap();
|
||||
let cached_value_2: String = get_cache(key_2).unwrap().unwrap();
|
||||
let cached_value_3: Data = get_cache(key_3).unwrap().unwrap();
|
||||
|
||||
assert_eq!(value_1, cached_value_1);
|
||||
assert_eq!(value_2, cached_value_2);
|
||||
assert_eq!(value_3, cached_value_3);
|
||||
|
||||
// wait for the cache to expire
|
||||
std::thread::sleep(std::time::Duration::from_millis(1100));
|
||||
|
||||
let expired_value_1: Option<Vec<i32>> = get_cache(key_1).unwrap();
|
||||
let expired_value_2: Option<Vec<i32>> = get_cache(key_2).unwrap();
|
||||
let expired_value_3: Option<Vec<i32>> = get_cache(key_3).unwrap();
|
||||
|
||||
assert!(expired_value_1.is_none());
|
||||
assert!(expired_value_2.is_none());
|
||||
assert!(expired_value_3.is_none());
|
||||
}
|
||||
}
|
|
@ -78,6 +78,7 @@ pub struct Model {
|
|||
pub is_indexable: bool,
|
||||
#[sea_orm(column_name = "mutedPatterns")]
|
||||
pub muted_patterns: Vec<String>,
|
||||
pub lang: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
pub mod log;
|
||||
pub mod nodeinfo;
|
||||
pub mod note;
|
||||
pub mod stream;
|
||||
|
|
161
packages/backend-rs/src/service/nodeinfo/fetch.rs
Normal file
161
packages/backend-rs/src/service/nodeinfo/fetch.rs
Normal file
|
@ -0,0 +1,161 @@
|
|||
use crate::service::nodeinfo::schema::*;
|
||||
use crate::util::http_client;
|
||||
use isahc::AsyncReadResponseExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Http client aquisition error: {0}")]
|
||||
HttpClientErr(#[from] http_client::Error),
|
||||
#[error("Http error: {0}")]
|
||||
HttpErr(#[from] isahc::Error),
|
||||
#[error("Bad status: {0}")]
|
||||
BadStatus(String),
|
||||
#[error("Failed to parse response body as text: {0}")]
|
||||
ResponseErr(#[from] std::io::Error),
|
||||
#[error("Failed to parse response body as json: {0}")]
|
||||
JsonErr(#[from] serde_json::Error),
|
||||
#[error("No nodeinfo provided")]
|
||||
MissingNodeinfo,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct NodeinfoLinks {
|
||||
links: Vec<NodeinfoLink>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct NodeinfoLink {
|
||||
rel: String,
|
||||
href: String,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn wellknown_nodeinfo_url(host: &str) -> String {
|
||||
format!("https://{}/.well-known/nodeinfo", host)
|
||||
}
|
||||
|
||||
async fn fetch_nodeinfo_links(host: &str) -> Result<NodeinfoLinks, Error> {
|
||||
let client = http_client::client()?;
|
||||
let wellknown_url = wellknown_nodeinfo_url(host);
|
||||
let mut wellknown_response = client.get_async(&wellknown_url).await?;
|
||||
|
||||
if !wellknown_response.status().is_success() {
|
||||
tracing::debug!("{:#?}", wellknown_response.body());
|
||||
return Err(Error::BadStatus(format!(
|
||||
"{} returned {}",
|
||||
wellknown_url,
|
||||
wellknown_response.status()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(serde_json::from_str(&wellknown_response.text().await?)?)
|
||||
}
|
||||
|
||||
fn check_nodeinfo_link(links: NodeinfoLinks) -> Result<String, Error> {
|
||||
for link in links.links {
|
||||
if link.rel == "http://nodeinfo.diaspora.software/ns/schema/2.1"
|
||||
|| link.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0"
|
||||
{
|
||||
return Ok(link.href);
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::MissingNodeinfo)
|
||||
}
|
||||
|
||||
async fn fetch_nodeinfo_impl(nodeinfo_link: &str) -> Result<Nodeinfo20, Error> {
|
||||
let client = http_client::client()?;
|
||||
let mut response = client.get_async(nodeinfo_link).await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
tracing::debug!("{:#?}", response.body());
|
||||
return Err(Error::BadStatus(format!(
|
||||
"{} returned {}",
|
||||
nodeinfo_link,
|
||||
response.status()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(serde_json::from_str(&response.text().await?)?)
|
||||
}
|
||||
|
||||
// for napi export
|
||||
type Nodeinfo = Nodeinfo20;
|
||||
|
||||
#[crate::export]
|
||||
pub async fn fetch_nodeinfo(host: &str) -> Result<Nodeinfo, Error> {
|
||||
tracing::info!("fetching from {}", host);
|
||||
let links = fetch_nodeinfo_links(host).await?;
|
||||
let nodeinfo_link = check_nodeinfo_link(links)?;
|
||||
fetch_nodeinfo_impl(&nodeinfo_link).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::{check_nodeinfo_link, fetch_nodeinfo, NodeinfoLink, NodeinfoLinks};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_check_nodeinfo_link() {
|
||||
let links_1 = NodeinfoLinks {
|
||||
links: vec![
|
||||
NodeinfoLink {
|
||||
rel: "https://example.com/incorrect/schema/2.0".to_string(),
|
||||
href: "https://example.com/dummy".to_string(),
|
||||
},
|
||||
NodeinfoLink {
|
||||
rel: "http://nodeinfo.diaspora.software/ns/schema/2.0".to_string(),
|
||||
href: "https://example.com/real".to_string(),
|
||||
},
|
||||
],
|
||||
};
|
||||
assert_eq!(
|
||||
check_nodeinfo_link(links_1).unwrap(),
|
||||
"https://example.com/real"
|
||||
);
|
||||
|
||||
let links_2 = NodeinfoLinks {
|
||||
links: vec![
|
||||
NodeinfoLink {
|
||||
rel: "https://example.com/incorrect/schema/2.0".to_string(),
|
||||
href: "https://example.com/dummy".to_string(),
|
||||
},
|
||||
NodeinfoLink {
|
||||
rel: "http://nodeinfo.diaspora.software/ns/schema/2.1".to_string(),
|
||||
href: "https://example.com/real".to_string(),
|
||||
},
|
||||
],
|
||||
};
|
||||
assert_eq!(
|
||||
check_nodeinfo_link(links_2).unwrap(),
|
||||
"https://example.com/real"
|
||||
);
|
||||
|
||||
let links_3 = NodeinfoLinks {
|
||||
links: vec![
|
||||
NodeinfoLink {
|
||||
rel: "https://example.com/incorrect/schema/2.0".to_string(),
|
||||
href: "https://example.com/dummy/2.0".to_string(),
|
||||
},
|
||||
NodeinfoLink {
|
||||
rel: "https://example.com/incorrect/schema/2.1".to_string(),
|
||||
href: "https://example.com/dummy/2.1".to_string(),
|
||||
},
|
||||
],
|
||||
};
|
||||
check_nodeinfo_link(links_3).expect_err("No nodeinfo");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fetch_nodeinfo() {
|
||||
assert_eq!(
|
||||
fetch_nodeinfo("info.firefish.dev")
|
||||
.await
|
||||
.unwrap()
|
||||
.software
|
||||
.name,
|
||||
"firefish"
|
||||
);
|
||||
}
|
||||
}
|
142
packages/backend-rs/src/service/nodeinfo/generate.rs
Normal file
142
packages/backend-rs/src/service/nodeinfo/generate.rs
Normal file
|
@ -0,0 +1,142 @@
|
|||
use crate::config::CONFIG;
|
||||
use crate::database::cache;
|
||||
use crate::database::db_conn;
|
||||
use crate::misc::meta::fetch_meta;
|
||||
use crate::model::entity::{note, user};
|
||||
use crate::service::nodeinfo::schema::*;
|
||||
use sea_orm::{ColumnTrait, DbErr, EntityTrait, PaginatorTrait, QueryFilter};
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Database error: {0}")]
|
||||
DbErr(#[from] DbErr),
|
||||
#[error("Cache error: {0}")]
|
||||
CacheErr(#[from] cache::Error),
|
||||
#[error("Failed to serialize nodeinfo to JSON: {0}")]
|
||||
JsonErr(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
async fn statistics() -> Result<(u64, u64, u64, u64), DbErr> {
|
||||
let db = db_conn().await?;
|
||||
|
||||
let now = chrono::Local::now().naive_local();
|
||||
const MONTH: chrono::TimeDelta = chrono::Duration::seconds(2592000000);
|
||||
const HALF_YEAR: chrono::TimeDelta = chrono::Duration::seconds(15552000000);
|
||||
|
||||
let local_users = user::Entity::find()
|
||||
.filter(user::Column::Host.is_null())
|
||||
.count(db);
|
||||
let local_active_halfyear = user::Entity::find()
|
||||
.filter(user::Column::Host.is_null())
|
||||
.filter(user::Column::LastActiveDate.gt(now - HALF_YEAR))
|
||||
.count(db);
|
||||
let local_active_month = user::Entity::find()
|
||||
.filter(user::Column::Host.is_null())
|
||||
.filter(user::Column::LastActiveDate.gt(now - MONTH))
|
||||
.count(db);
|
||||
let local_posts = note::Entity::find()
|
||||
.filter(note::Column::UserHost.is_null())
|
||||
.count(db);
|
||||
|
||||
tokio::try_join!(
|
||||
local_users,
|
||||
local_active_halfyear,
|
||||
local_active_month,
|
||||
local_posts
|
||||
)
|
||||
}
|
||||
|
||||
async fn generate_nodeinfo_2_1() -> Result<Nodeinfo21, Error> {
|
||||
let (local_users, local_active_halfyear, local_active_month, local_posts) =
|
||||
statistics().await?;
|
||||
let meta = fetch_meta(true).await?;
|
||||
let metadata = HashMap::from([
|
||||
(
|
||||
"nodeName".to_string(),
|
||||
json!(meta.name.unwrap_or(CONFIG.host.clone())),
|
||||
),
|
||||
("nodeDescription".to_string(), json!(meta.description)),
|
||||
("repositoryUrl".to_string(), json!(meta.repository_url)),
|
||||
(
|
||||
"enableLocalTimeline".to_string(),
|
||||
json!(!meta.disable_local_timeline),
|
||||
),
|
||||
(
|
||||
"enableRecommendedTimeline".to_string(),
|
||||
json!(!meta.disable_recommended_timeline),
|
||||
),
|
||||
(
|
||||
"enableGlobalTimeline".to_string(),
|
||||
json!(!meta.disable_global_timeline),
|
||||
),
|
||||
(
|
||||
"enableGuestTimeline".to_string(),
|
||||
json!(meta.enable_guest_timeline),
|
||||
),
|
||||
(
|
||||
"maintainer".to_string(),
|
||||
json!({"name":meta.maintainer_name,"email":meta.maintainer_email}),
|
||||
),
|
||||
("proxyAccountName".to_string(), json!(meta.proxy_account_id)),
|
||||
(
|
||||
"themeColor".to_string(),
|
||||
json!(meta.theme_color.unwrap_or("#31748f".to_string())),
|
||||
),
|
||||
]);
|
||||
|
||||
Ok(Nodeinfo21 {
|
||||
version: "2.1".to_string(),
|
||||
software: Software21 {
|
||||
name: "firefish".to_string(),
|
||||
version: CONFIG.version.clone(),
|
||||
repository: Some(meta.repository_url),
|
||||
homepage: Some("https://firefish.dev/firefish/firefish".to_string()),
|
||||
},
|
||||
protocols: vec![Protocol::Activitypub],
|
||||
services: Services {
|
||||
inbound: vec![],
|
||||
outbound: vec![Outbound::Atom1, Outbound::Rss2],
|
||||
},
|
||||
open_registrations: !meta.disable_registration,
|
||||
usage: Usage {
|
||||
users: Users {
|
||||
total: Some(local_users as u32),
|
||||
active_halfyear: Some(local_active_halfyear as u32),
|
||||
active_month: Some(local_active_month as u32),
|
||||
},
|
||||
local_posts: Some(local_posts as u32),
|
||||
local_comments: None,
|
||||
},
|
||||
metadata,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn nodeinfo_2_1() -> Result<Nodeinfo21, Error> {
|
||||
const NODEINFO_2_1_CACHE_KEY: &str = "nodeinfo_2_1";
|
||||
|
||||
let cached = cache::get::<Nodeinfo21>(NODEINFO_2_1_CACHE_KEY)?;
|
||||
|
||||
if let Some(nodeinfo) = cached {
|
||||
Ok(nodeinfo)
|
||||
} else {
|
||||
let nodeinfo = generate_nodeinfo_2_1().await?;
|
||||
cache::set(NODEINFO_2_1_CACHE_KEY, &nodeinfo, 60 * 60)?;
|
||||
Ok(nodeinfo)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn nodeinfo_2_0() -> Result<Nodeinfo20, Error> {
|
||||
Ok(nodeinfo_2_1().await?.into())
|
||||
}
|
||||
|
||||
#[crate::export(js_name = "nodeinfo_2_1")]
|
||||
pub async fn nodeinfo_2_1_as_json() -> Result<serde_json::Value, Error> {
|
||||
Ok(serde_json::to_value(nodeinfo_2_1().await?)?)
|
||||
}
|
||||
|
||||
#[crate::export(js_name = "nodeinfo_2_0")]
|
||||
pub async fn nodeinfo_2_0_as_json() -> Result<serde_json::Value, Error> {
|
||||
Ok(serde_json::to_value(nodeinfo_2_0().await?)?)
|
||||
}
|
3
packages/backend-rs/src/service/nodeinfo/mod.rs
Normal file
3
packages/backend-rs/src/service/nodeinfo/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod fetch;
|
||||
pub mod generate;
|
||||
pub mod schema;
|
263
packages/backend-rs/src/service/nodeinfo/schema.rs
Normal file
263
packages/backend-rs/src/service/nodeinfo/schema.rs
Normal file
|
@ -0,0 +1,263 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
// TODO: I want to use these macros but they don't work with rmp_serde
|
||||
// - #[serde(skip_serializing_if = "Option::is_none")] (https://github.com/3Hren/msgpack-rust/issues/86)
|
||||
// - #[serde(tag = "version", rename = "2.1")] (https://github.com/3Hren/msgpack-rust/issues/318)
|
||||
|
||||
/// NodeInfo schema version 2.1. https://nodeinfo.diaspora.software/docson/index.html#/ns/schema/2.1
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Nodeinfo21 {
|
||||
/// The schema version, must be 2.1.
|
||||
pub version: String,
|
||||
/// Metadata about server software in use.
|
||||
pub software: Software21,
|
||||
/// The protocols supported on this server.
|
||||
pub protocols: Vec<Protocol>,
|
||||
/// The third party sites this server can connect to via their application API.
|
||||
pub services: Services,
|
||||
/// Whether this server allows open self-registration.
|
||||
pub open_registrations: bool,
|
||||
/// Usage statistics for this server.
|
||||
pub usage: Usage,
|
||||
/// Free form key value pairs for software specific values. Clients should not rely on any specific key present.
|
||||
pub metadata: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
/// NodeInfo schema version 2.0. https://nodeinfo.diaspora.software/docson/index.html#/ns/schema/2.0
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[crate::export(object, js_name = "Nodeinfo")]
|
||||
pub struct Nodeinfo20 {
|
||||
/// The schema version, must be 2.0.
|
||||
pub version: String,
|
||||
/// Metadata about server software in use.
|
||||
pub software: Software20,
|
||||
/// The protocols supported on this server.
|
||||
pub protocols: Vec<Protocol>,
|
||||
/// The third party sites this server can connect to via their application API.
|
||||
pub services: Services,
|
||||
/// Whether this server allows open self-registration.
|
||||
pub open_registrations: bool,
|
||||
/// Usage statistics for this server.
|
||||
pub usage: Usage,
|
||||
/// Free form key value pairs for software specific values. Clients should not rely on any specific key present.
|
||||
pub metadata: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Metadata about server software in use (version 2.1).
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Software21 {
|
||||
/// The canonical name of this server software.
|
||||
pub name: String,
|
||||
/// The version of this server software.
|
||||
pub version: String,
|
||||
/// The url of the source code repository of this server software.
|
||||
pub repository: Option<String>,
|
||||
/// The url of the homepage of this server software.
|
||||
pub homepage: Option<String>,
|
||||
}
|
||||
|
||||
/// Metadata about server software in use (version 2.0).
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[crate::export(object)]
|
||||
pub struct Software20 {
|
||||
/// The canonical name of this server software.
|
||||
pub name: String,
|
||||
/// The version of this server software.
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[crate::export(string_enum = "lowercase")]
|
||||
pub enum Protocol {
|
||||
Activitypub,
|
||||
Buddycloud,
|
||||
Dfrn,
|
||||
Diaspora,
|
||||
Libertree,
|
||||
Ostatus,
|
||||
Pumpio,
|
||||
Tent,
|
||||
Xmpp,
|
||||
Zot,
|
||||
}
|
||||
|
||||
/// The third party sites this server can connect to via their application API.
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[crate::export(object)]
|
||||
pub struct Services {
|
||||
/// The third party sites this server can retrieve messages from for combined display with regular traffic.
|
||||
pub inbound: Vec<Inbound>,
|
||||
/// The third party sites this server can publish messages to on the behalf of a user.
|
||||
pub outbound: Vec<Outbound>,
|
||||
}
|
||||
|
||||
/// The third party sites this server can retrieve messages from for combined display with regular traffic.
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[crate::export(string_enum = "lowercase")]
|
||||
pub enum Inbound {
|
||||
#[serde(rename = "atom1.0")]
|
||||
Atom1,
|
||||
Gnusocial,
|
||||
Imap,
|
||||
Pnut,
|
||||
#[serde(rename = "pop3")]
|
||||
Pop3,
|
||||
Pumpio,
|
||||
#[serde(rename = "rss2.0")]
|
||||
Rss2,
|
||||
Twitter,
|
||||
}
|
||||
|
||||
/// The third party sites this server can publish messages to on the behalf of a user.
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[crate::export(string_enum = "lowercase")]
|
||||
pub enum Outbound {
|
||||
#[serde(rename = "atom1.0")]
|
||||
Atom1,
|
||||
Blogger,
|
||||
Buddycloud,
|
||||
Diaspora,
|
||||
Dreamwidth,
|
||||
Drupal,
|
||||
Facebook,
|
||||
Friendica,
|
||||
Gnusocial,
|
||||
Google,
|
||||
Insanejournal,
|
||||
Libertree,
|
||||
Linkedin,
|
||||
Livejournal,
|
||||
Mediagoblin,
|
||||
Myspace,
|
||||
Pinterest,
|
||||
Pnut,
|
||||
Posterous,
|
||||
Pumpio,
|
||||
Redmatrix,
|
||||
#[serde(rename = "rss2.0")]
|
||||
Rss2,
|
||||
Smtp,
|
||||
Tent,
|
||||
Tumblr,
|
||||
Twitter,
|
||||
Wordpress,
|
||||
Xmpp,
|
||||
}
|
||||
|
||||
/// Usage statistics for this server.
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[crate::export(object)]
|
||||
pub struct Usage {
|
||||
pub users: Users,
|
||||
pub local_posts: Option<u32>,
|
||||
pub local_comments: Option<u32>,
|
||||
}
|
||||
|
||||
/// statistics about the users of this server.
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[crate::export(object)]
|
||||
pub struct Users {
|
||||
pub total: Option<u32>,
|
||||
pub active_halfyear: Option<u32>,
|
||||
pub active_month: Option<u32>,
|
||||
}
|
||||
|
||||
impl From<Software21> for Software20 {
|
||||
fn from(software: Software21) -> Self {
|
||||
Self {
|
||||
name: software.name,
|
||||
version: software.version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Nodeinfo21> for Nodeinfo20 {
|
||||
fn from(nodeinfo: Nodeinfo21) -> Self {
|
||||
Self {
|
||||
version: "2.0".to_string(),
|
||||
software: nodeinfo.software.into(),
|
||||
protocols: nodeinfo.protocols,
|
||||
services: nodeinfo.services,
|
||||
open_registrations: nodeinfo.open_registrations,
|
||||
usage: nodeinfo.usage,
|
||||
metadata: nodeinfo.metadata,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::{Nodeinfo20, Nodeinfo21};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn parse_nodeinfo_2_0() {
|
||||
let json_str_1 = r#"{"version":"2.0","software":{"name":"mastodon","version":"4.3.0-nightly.2024-04-30"},"protocols":["activitypub"],"services":{"outbound":[],"inbound":[]},"usage":{"users":{"total":1935016,"activeMonth":238223,"activeHalfyear":618795},"localPosts":90175135},"openRegistrations":true,"metadata":{"nodeName":"Mastodon","nodeDescription":"The original server operated by the Mastodon gGmbH non-profit"}}"#;
|
||||
let parsed_1: Nodeinfo20 = serde_json::from_str(json_str_1).unwrap();
|
||||
let serialized_1 = serde_json::to_string(&parsed_1).unwrap();
|
||||
let reparsed_1: Nodeinfo20 = serde_json::from_str(&serialized_1).unwrap();
|
||||
|
||||
assert_eq!(parsed_1, reparsed_1);
|
||||
assert_eq!(parsed_1.software.name, "mastodon");
|
||||
assert_eq!(parsed_1.software.version, "4.3.0-nightly.2024-04-30");
|
||||
|
||||
let json_str_2 = r#"{"version":"2.0","software":{"name":"peertube","version":"5.0.0"},"protocols":["activitypub"],"services":{"inbound":[],"outbound":["atom1.0","rss2.0"]},"openRegistrations":false,"usage":{"users":{"total":5,"activeMonth":0,"activeHalfyear":2},"localPosts":1018,"localComments":1},"metadata":{"taxonomy":{"postsName":"Videos"},"nodeName":"Blender Video","nodeDescription":"Blender Foundation PeerTube instance.","nodeConfig":{"search":{"remoteUri":{"users":true,"anonymous":false}},"plugin":{"registered":[]},"theme":{"registered":[],"default":"default"},"email":{"enabled":false},"contactForm":{"enabled":true},"transcoding":{"hls":{"enabled":true},"webtorrent":{"enabled":true},"enabledResolutions":[1080]},"live":{"enabled":false,"transcoding":{"enabled":true,"enabledResolutions":[]}},"import":{"videos":{"http":{"enabled":true},"torrent":{"enabled":false}}},"autoBlacklist":{"videos":{"ofUsers":{"enabled":false}}},"avatar":{"file":{"size":{"max":4194304},"extensions":[".png",".jpeg",".jpg",".gif",".webp"]}},"video":{"image":{"extensions":[".png",".jpg",".jpeg",".webp"],"size":{"max":4194304}},"file":{"extensions":[".webm",".ogv",".ogg",".mp4",".mkv",".mov",".qt",".mqv",".m4v",".flv",".f4v",".wmv",".avi",".3gp",".3gpp",".3g2",".3gpp2",".nut",".mts",".m2ts",".mpv",".m2v",".m1v",".mpg",".mpe",".mpeg",".vob",".mxf",".mp3",".wma",".wav",".flac",".aac",".m4a",".ac3"]}},"videoCaption":{"file":{"size":{"max":20971520},"extensions":[".vtt",".srt"]}},"user":{"videoQuota":5368709120,"videoQuotaDaily":-1},"trending":{"videos":{"intervalDays":7}},"tracker":{"enabled":true}}}}"#;
|
||||
let parsed_2: Nodeinfo20 = serde_json::from_str(json_str_2).unwrap();
|
||||
let serialized_2 = serde_json::to_string(&parsed_2).unwrap();
|
||||
let reparsed_2: Nodeinfo20 = serde_json::from_str(&serialized_2).unwrap();
|
||||
|
||||
assert_eq!(parsed_2, reparsed_2);
|
||||
assert_eq!(parsed_2.software.name, "peertube");
|
||||
assert_eq!(parsed_2.software.version, "5.0.0");
|
||||
|
||||
let json_str_3 = r#"{"metadata":{"nodeName":"pixelfed","software":{"homepage":"https://pixelfed.org","repo":"https://github.com/pixelfed/pixelfed"},"config":{"features":{"timelines":{"local":true,"network":true},"mobile_apis":true,"stories":true,"video":true,"import":{"instagram":false,"mastodon":false,"pixelfed":false},"label":{"covid":{"enabled":false,"org":"visit the WHO website","url":"https://www.who.int/emergencies/diseases/novel-coronavirus-2019/advice-for-public"}},"hls":{"enabled":false}}}},"protocols":["activitypub"],"services":{"inbound":[],"outbound":[]},"software":{"name":"pixelfed","version":"0.12.0"},"usage":{"localPosts":24059868,"localComments":0,"users":{"total":112832,"activeHalfyear":24366,"activeMonth":8921}},"version":"2.0","openRegistrations":true}"#;
|
||||
let parsed_3: Nodeinfo20 = serde_json::from_str(json_str_3).unwrap();
|
||||
let serialized_3 = serde_json::to_string(&parsed_3).unwrap();
|
||||
let reparsed_3: Nodeinfo20 = serde_json::from_str(&serialized_3).unwrap();
|
||||
|
||||
assert_eq!(parsed_3, reparsed_3);
|
||||
assert_eq!(parsed_3.software.name, "pixelfed");
|
||||
assert_eq!(parsed_3.software.version, "0.12.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_nodeinfo_2_1() {
|
||||
let json_str_1 = r##"{"version":"2.1","software":{"name":"catodon","version":"24.04-dev.2","repository":"https://codeberg.org/catodon/catodon","homepage":"https://codeberg.org/catodon/catodon"},"protocols":["activitypub"],"services":{"inbound":[],"outbound":["atom1.0","rss2.0"]},"openRegistrations":true,"usage":{"users":{"total":294,"activeHalfyear":292,"activeMonth":139},"localPosts":22616,"localComments":0},"metadata":{"nodeName":"Catodon Social","nodeDescription":"🌎 Home of Catodon, a new platform for fedi communities, initially based on Iceshrimp/Firefish/Misskey. Be aware that our first release is not out yet, so things are still experimental.","maintainer":{"name":"admin","email":"redacted@example.com"},"langs":[],"tosUrl":"https://example.com/redacted","repositoryUrl":"https://codeberg.org/catodon/catodon","feedbackUrl":"https://codeberg.org/catodon/catodon/issues","disableRegistration":false,"disableLocalTimeline":false,"disableRecommendedTimeline":true,"disableGlobalTimeline":false,"emailRequiredForSignup":true,"postEditing":true,"postImports":false,"enableHcaptcha":true,"enableRecaptcha":false,"maxNoteTextLength":8000,"maxCaptionTextLength":1500,"enableGithubIntegration":false,"enableDiscordIntegration":false,"enableEmail":true,"themeColor":"#31748f"}}"##;
|
||||
let parsed_1: Nodeinfo21 = serde_json::from_str(json_str_1).unwrap();
|
||||
let serialized_1 = serde_json::to_string(&parsed_1).unwrap();
|
||||
let reparsed_1: Nodeinfo21 = serde_json::from_str(&serialized_1).unwrap();
|
||||
|
||||
assert_eq!(parsed_1, reparsed_1);
|
||||
assert_eq!(parsed_1.software.name, "catodon");
|
||||
assert_eq!(parsed_1.software.version, "24.04-dev.2");
|
||||
|
||||
let json_str_2 = r#"{"version":"2.1","software":{"name":"meisskey","version":"10.102.699-m544","repository":"https://github.com/mei23/misskey"},"protocols":["activitypub"],"services":{"inbound":[],"outbound":["atom1.0","rss2.0"]},"openRegistrations":true,"usage":{"users":{"total":1123,"activeHalfyear":305,"activeMonth":89},"localPosts":268739,"localComments":0},"metadata":{"nodeName":"meisskey.one","nodeDescription":"ローカルタイムラインのないインスタンスなのだわ\n\n\n[通報・報告 (Report)](https://example.com/redacted)","name":"meisskey.one","description":"ローカルタイムラインのないインスタンスなのだわ\n\n\n[通報・報告 (Report)](https://example.com/redacted)","maintainer":{"name":"redacted","email":"redacted"},"langs":[],"announcements":[{"title":"問題・要望など","text":"問題・要望などは <a href=\"https://example.com/redacted\">#meisskeyone要望</a> で投稿してなのだわ"}],"relayActor":"https://example.com/redacted","relays":[],"disableRegistration":false,"disableLocalTimeline":true,"enableRecaptcha":true,"maxNoteTextLength":5000,"enableTwitterIntegration":false,"enableGithubIntegration":false,"enableDiscordIntegration":false,"enableServiceWorker":true,"proxyAccountName":"ghost"}}"#;
|
||||
let parsed_2: Nodeinfo21 = serde_json::from_str(json_str_2).unwrap();
|
||||
let serialized_2 = serde_json::to_string(&parsed_2).unwrap();
|
||||
let reparsed_2: Nodeinfo21 = serde_json::from_str(&serialized_2).unwrap();
|
||||
|
||||
assert_eq!(parsed_2, reparsed_2);
|
||||
assert_eq!(parsed_2.software.name, "meisskey");
|
||||
assert_eq!(parsed_2.software.version, "10.102.699-m544");
|
||||
|
||||
let json_str_3 = r##"{"metadata":{"enableGlobalTimeline":true,"enableGuestTimeline":false,"enableLocalTimeline":true,"enableRecommendedTimeline":false,"maintainer":{"name":"Firefish dev team"},"nodeDescription":"","nodeName":"Firefish","repositoryUrl":"https://firefish.dev/firefish/firefish","themeColor":"#F25A85"},"openRegistrations":false,"protocols":["activitypub"],"services":{"inbound":[],"outbound":["atom1.0","rss2.0"]},"software":{"homepage":"https://firefish.dev/firefish/firefish","name":"firefish","repository":"https://firefish.dev/firefish/firefish","version":"20240504"},"usage":{"localPosts":23857,"users":{"activeHalfyear":7,"activeMonth":7,"total":9}},"version":"2.1"}"##;
|
||||
let parsed_3: Nodeinfo20 = serde_json::from_str(json_str_3).unwrap();
|
||||
let serialized_3 = serde_json::to_string(&parsed_3).unwrap();
|
||||
let reparsed_3: Nodeinfo20 = serde_json::from_str(&serialized_3).unwrap();
|
||||
|
||||
assert_eq!(parsed_3, reparsed_3);
|
||||
assert_eq!(parsed_3.software.name, "firefish");
|
||||
assert_eq!(parsed_3.software.version, "20240504");
|
||||
}
|
||||
}
|
|
@ -1,24 +1,34 @@
|
|||
use crate::config::CONFIG;
|
||||
use isahc::{config::*, HttpClient};
|
||||
use once_cell::sync::OnceCell;
|
||||
use reqwest::{Client, Error, NoProxy, Proxy};
|
||||
use std::time::Duration;
|
||||
|
||||
static CLIENT: OnceCell<Client> = OnceCell::new();
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Isahc error: {0}")]
|
||||
IsahcErr(#[from] isahc::Error),
|
||||
#[error("Url parse error: {0}")]
|
||||
UrlParseErr(#[from] isahc::http::uri::InvalidUri),
|
||||
}
|
||||
|
||||
pub fn http_client() -> Result<Client, Error> {
|
||||
static CLIENT: OnceCell<HttpClient> = OnceCell::new();
|
||||
|
||||
pub fn client() -> Result<HttpClient, Error> {
|
||||
CLIENT
|
||||
.get_or_try_init(|| {
|
||||
let mut builder = Client::builder().timeout(Duration::from_secs(5));
|
||||
let mut builder = HttpClient::builder()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.default_header("user-agent", &CONFIG.user_agent)
|
||||
.dns_cache(DnsCache::Timeout(Duration::from_secs(60 * 60)));
|
||||
|
||||
if let Some(proxy_url) = &CONFIG.proxy {
|
||||
let mut proxy = Proxy::all(proxy_url)?;
|
||||
builder = builder.proxy(Some(proxy_url.parse()?));
|
||||
if let Some(proxy_bypass_hosts) = &CONFIG.proxy_bypass_hosts {
|
||||
proxy = proxy.no_proxy(NoProxy::from_string(&proxy_bypass_hosts.join(",")));
|
||||
builder = builder.proxy_blacklist(proxy_bypass_hosts);
|
||||
}
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
|
||||
builder.build()
|
||||
Ok(builder.build()?)
|
||||
})
|
||||
.cloned()
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
pub use http_client::http_client;
|
||||
|
||||
pub mod http_client;
|
||||
pub mod id;
|
||||
pub mod random;
|
||||
|
|
|
@ -19,6 +19,13 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
|
|||
return appendChildren(childNodes, background).join("").trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* We only exclude text containing asterisks, since the other marks can almost be considered intentionally used.
|
||||
*/
|
||||
function escapeAmbiguousMfmMarks(text: string) {
|
||||
return text.includes("*") ? `<plain>${text}</plain>` : text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get only the text, ignoring all formatting inside
|
||||
* @param node
|
||||
|
@ -62,7 +69,7 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
|
|||
background = "",
|
||||
): (string | string[])[] {
|
||||
if (treeAdapter.isTextNode(node)) {
|
||||
return [node.value];
|
||||
return [escapeAmbiguousMfmMarks(node.value)];
|
||||
}
|
||||
|
||||
// Skip comment or document type node
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
import type { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class DropUnusedIndexes1714643926317 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP INDEX "IDX_01f4581f114e0ebd2bbb876f0b"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_0610ebcfcfb4a18441a9bcdab2"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_25dfc71b0369b003a4cd434d0b"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_2710a55f826ee236ea1a62698f"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_4c02d38a976c3ae132228c6fce"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_51c063b6a133a9cb87145450f5"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_54ebcb6d27222913b908d56fd8"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_7fa20a12319c7f6dc3aed98c0a"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_88937d94d7443d9a99a76fa5c0"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_b11a5e627c41d4dc3170f1d370"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_c8dfad3b72196dd1d6b5db168a"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_d57f9030cd3af7f63ffb1c267c"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_e5848eac4940934e23dbc17581"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_fa99d777623947a5b05f394cae"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_01f4581f114e0ebd2bbb876f0b" ON "note_reaction" ("createdAt")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_0610ebcfcfb4a18441a9bcdab2" ON "poll" ("userId")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_25dfc71b0369b003a4cd434d0b" ON "note" ("attachedFileTypes")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_2710a55f826ee236ea1a62698f" ON "hashtag" ("mentionedUsersCount")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_4c02d38a976c3ae132228c6fce" ON "hashtag" ("mentionedRemoteUsersCount")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_51c063b6a133a9cb87145450f5" ON "note" ("fileIds")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_54ebcb6d27222913b908d56fd8" ON "note" ("mentions")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_7fa20a12319c7f6dc3aed98c0a" ON "poll" ("userHost")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_88937d94d7443d9a99a76fa5c0" ON "note" ("tags")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_b11a5e627c41d4dc3170f1d370" ON "notification" ("createdAt")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_c8dfad3b72196dd1d6b5db168a" ON "drive_file" ("createdAt")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_d57f9030cd3af7f63ffb1c267c" ON "hashtag" ("attachedUsersCount")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_e5848eac4940934e23dbc17581" ON "drive_file" ("uri")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_fa99d777623947a5b05f394cae" ON "user" ("tags")`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import type { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddUserProfileLanguage1714888400293 implements MigrationInterface {
|
||||
async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_profile" ADD COLUMN "lang" character varying(32)`,
|
||||
);
|
||||
}
|
||||
|
||||
async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "lang"`);
|
||||
}
|
||||
}
|
|
@ -7,10 +7,10 @@ import chalk from "chalk";
|
|||
import Logger from "@/services/logger.js";
|
||||
import IPCIDR from "ip-cidr";
|
||||
import PrivateIp from "private-ip";
|
||||
import { isValidUrl } from "./is-valid-url.js";
|
||||
import { isSafeUrl } from "backend-rs";
|
||||
|
||||
export async function downloadUrl(url: string, path: string): Promise<void> {
|
||||
if (!isValidUrl(url)) {
|
||||
if (!isSafeUrl(url)) {
|
||||
throw new StatusError("Invalid URL", 400);
|
||||
}
|
||||
|
||||
|
@ -43,8 +43,8 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
|
|||
limit: 0,
|
||||
},
|
||||
})
|
||||
.on("redirect", (res: Got.Response, opts: Got.NormalizedOptions) => {
|
||||
if (!isValidUrl(opts.url)) {
|
||||
.on("redirect", (_res: Got.Response, opts: Got.NormalizedOptions) => {
|
||||
if (!isSafeUrl(opts.url)) {
|
||||
downloadLogger.warn(`Invalid URL: ${opts.url}`);
|
||||
req.destroy();
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import CacheableLookup from "cacheable-lookup";
|
|||
import fetch, { type RequestRedirect } from "node-fetch";
|
||||
import { HttpProxyAgent, HttpsProxyAgent } from "hpagent";
|
||||
import { config } from "@/config.js";
|
||||
import { isValidUrl } from "./is-valid-url.js";
|
||||
import { isSafeUrl } from "backend-rs";
|
||||
|
||||
export async function getJson(
|
||||
url: string,
|
||||
|
@ -60,7 +60,7 @@ export async function getResponse(args: {
|
|||
size?: number;
|
||||
redirect?: RequestRedirect;
|
||||
}) {
|
||||
if (!isValidUrl(args.url)) {
|
||||
if (!isSafeUrl(args.url)) {
|
||||
throw new StatusError("Invalid URL", 400);
|
||||
}
|
||||
|
||||
|
@ -83,7 +83,7 @@ export async function getResponse(args: {
|
|||
});
|
||||
|
||||
if (args.redirect === "manual" && [301, 302, 307, 308].includes(res.status)) {
|
||||
if (!isValidUrl(res.url)) {
|
||||
if (!isSafeUrl(res.url)) {
|
||||
throw new StatusError("Invalid URL", 400);
|
||||
}
|
||||
return res;
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
export function isValidUrl(url: string | URL | undefined): boolean {
|
||||
if (process.env.NODE_ENV !== "production") return true;
|
||||
|
||||
try {
|
||||
if (url == null) return false;
|
||||
|
||||
const u = typeof url === "string" ? new URL(url) : url;
|
||||
if (!u.protocol.match(/^https?:$/) || u.hostname === "unix") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (u.port !== "" && !["80", "443"].includes(u.port)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -23,7 +23,6 @@ export class DriveFile {
|
|||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column("timestamp without time zone", {
|
||||
comment: "The created date of the DriveFile.",
|
||||
})
|
||||
|
@ -147,7 +146,6 @@ export class DriveFile {
|
|||
})
|
||||
public webpublicAccessKey: string | null;
|
||||
|
||||
@Index()
|
||||
@Column("varchar", {
|
||||
length: 512,
|
||||
nullable: true,
|
||||
|
|
|
@ -19,7 +19,6 @@ export class Hashtag {
|
|||
})
|
||||
public mentionedUserIds: User["id"][];
|
||||
|
||||
@Index()
|
||||
@Column("integer", {
|
||||
default: 0,
|
||||
})
|
||||
|
@ -43,7 +42,6 @@ export class Hashtag {
|
|||
})
|
||||
public mentionedRemoteUserIds: User["id"][];
|
||||
|
||||
@Index()
|
||||
@Column("integer", {
|
||||
default: 0,
|
||||
})
|
||||
|
@ -55,7 +53,6 @@ export class Hashtag {
|
|||
})
|
||||
public attachedUserIds: User["id"][];
|
||||
|
||||
@Index()
|
||||
@Column("integer", {
|
||||
default: 0,
|
||||
})
|
||||
|
|
|
@ -17,7 +17,6 @@ export class NoteReaction {
|
|||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column("timestamp without time zone", {
|
||||
comment: "The created date of the NoteReaction.",
|
||||
})
|
||||
|
|
|
@ -139,7 +139,6 @@ export class Note {
|
|||
|
||||
// FIXME: file id is not removed from this array even if the file is deleted
|
||||
// TODO: drop this column and use note_files
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
array: true,
|
||||
|
@ -147,7 +146,6 @@ export class Note {
|
|||
})
|
||||
public fileIds: DriveFile["id"][];
|
||||
|
||||
@Index()
|
||||
@Column("varchar", {
|
||||
length: 256,
|
||||
array: true,
|
||||
|
@ -163,7 +161,6 @@ export class Note {
|
|||
})
|
||||
public visibleUserIds: User["id"][];
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
array: true,
|
||||
|
@ -184,7 +181,6 @@ export class Note {
|
|||
})
|
||||
public emojis: string[];
|
||||
|
||||
@Index()
|
||||
@Column("varchar", {
|
||||
length: 128,
|
||||
array: true,
|
||||
|
|
|
@ -20,7 +20,6 @@ export class Notification {
|
|||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column("timestamp without time zone", {
|
||||
comment: "The created date of the Notification.",
|
||||
})
|
||||
|
|
|
@ -44,14 +44,12 @@ export class Poll {
|
|||
})
|
||||
public noteVisibility: (typeof noteVisibilities)[number];
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
comment: "[Denormalized]",
|
||||
})
|
||||
public userId: User["id"];
|
||||
|
||||
@Index()
|
||||
@Column("varchar", {
|
||||
length: 512,
|
||||
nullable: true,
|
||||
|
|
|
@ -50,6 +50,12 @@ export class UserProfile {
|
|||
verified?: boolean;
|
||||
}[];
|
||||
|
||||
@Column("varchar", {
|
||||
length: 32,
|
||||
nullable: true,
|
||||
})
|
||||
public lang: string | null;
|
||||
|
||||
@Column("varchar", {
|
||||
length: 512,
|
||||
nullable: true,
|
||||
|
|
|
@ -116,7 +116,6 @@ export class User {
|
|||
})
|
||||
public bannerId: DriveFile["id"] | null;
|
||||
|
||||
@Index()
|
||||
@Column("varchar", {
|
||||
length: 128,
|
||||
array: true,
|
||||
|
|
|
@ -512,6 +512,7 @@ export const UserRepository = db.getRepository(User).extend({
|
|||
description: profile!.description,
|
||||
location: profile!.location,
|
||||
birthday: profile!.birthday,
|
||||
lang: profile!.lang,
|
||||
fields: profile!.fields,
|
||||
followersCount: followersCount ?? null,
|
||||
followingCount: followingCount ?? null,
|
||||
|
|
|
@ -204,6 +204,12 @@ export const packedUserDetailedNotMeOnlySchema = {
|
|||
optional: false,
|
||||
example: "2018-03-12",
|
||||
},
|
||||
lang: {
|
||||
type: "string",
|
||||
nullable: true,
|
||||
optional: false,
|
||||
example: "ja-JP",
|
||||
},
|
||||
fields: {
|
||||
type: "array",
|
||||
nullable: false,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// https://gist.github.com/tkrotoff/a6baf96eb6b61b445a9142e5555511a0
|
||||
import type { Primitive } from "type-fest";
|
||||
|
||||
type NullToUndefined<T> = T extends null
|
||||
export type NullToUndefined<T> = T extends null
|
||||
? undefined
|
||||
: T extends Primitive | Function | Date | RegExp
|
||||
? T
|
||||
|
@ -15,7 +15,7 @@ type NullToUndefined<T> = T extends null
|
|||
? { [K in keyof T]: NullToUndefined<T[K]> }
|
||||
: unknown;
|
||||
|
||||
type UndefinedToNull<T> = T extends undefined
|
||||
export type UndefinedToNull<T> = T extends undefined
|
||||
? null
|
||||
: T extends Primitive | Function | Date | RegExp
|
||||
? T
|
||||
|
@ -47,6 +47,16 @@ function _nullToUndefined<T>(obj: T): NullToUndefined<T> {
|
|||
return obj as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively converts all null values to undefined.
|
||||
*
|
||||
* @param obj object to convert
|
||||
* @returns a copy of the object with all its null values converted to undefined
|
||||
*/
|
||||
export function fromRustObject<T>(obj: T) {
|
||||
return _nullToUndefined(structuredClone(obj));
|
||||
}
|
||||
|
||||
function _undefinedToNull<T>(obj: T): UndefinedToNull<T> {
|
||||
if (obj === undefined) {
|
||||
return null as any;
|
||||
|
@ -71,6 +81,6 @@ function _undefinedToNull<T>(obj: T): UndefinedToNull<T> {
|
|||
* @param obj object to convert
|
||||
* @returns a copy of the object with all its undefined values converted to null
|
||||
*/
|
||||
export function undefinedToNull<T>(obj: T) {
|
||||
export function toRustObject<T>(obj: T) {
|
||||
return _undefinedToNull(structuredClone(obj));
|
||||
}
|
||||
|
|
|
@ -5,8 +5,8 @@ import { StatusError, getResponse } from "@/misc/fetch.js";
|
|||
import { createSignedPost, createSignedGet } from "./ap-request.js";
|
||||
import type { Response } from "node-fetch";
|
||||
import type { IObject } from "./type.js";
|
||||
import { isValidUrl } from "@/misc/is-valid-url.js";
|
||||
import { apLogger } from "@/remote/activitypub/logger.js";
|
||||
import { isSafeUrl } from "backend-rs";
|
||||
|
||||
export default async (user: { id: User["id"] }, url: string, object: any) => {
|
||||
const body = JSON.stringify(object);
|
||||
|
@ -44,7 +44,7 @@ export async function apGet(
|
|||
user?: ILocalUser,
|
||||
redirects: boolean = true,
|
||||
): Promise<{ finalUrl: string; content: IObject }> {
|
||||
if (!isValidUrl(url)) {
|
||||
if (!isSafeUrl(url)) {
|
||||
throw new StatusError("Invalid URL", 400);
|
||||
}
|
||||
|
||||
|
|
|
@ -286,7 +286,6 @@ import * as ep___pinnedUsers from "./endpoints/pinned-users.js";
|
|||
import * as ep___customMotd from "./endpoints/custom-motd.js";
|
||||
import * as ep___customSplashIcons from "./endpoints/custom-splash-icons.js";
|
||||
import * as ep___latestVersion from "./endpoints/latest-version.js";
|
||||
import * as ep___release from "./endpoints/release.js";
|
||||
import * as ep___promo_read from "./endpoints/promo/read.js";
|
||||
import * as ep___requestResetPassword from "./endpoints/request-reset-password.js";
|
||||
import * as ep___resetPassword from "./endpoints/reset-password.js";
|
||||
|
@ -635,7 +634,6 @@ const eps = [
|
|||
["custom-motd", ep___customMotd],
|
||||
["custom-splash-icons", ep___customSplashIcons],
|
||||
["latest-version", ep___latestVersion],
|
||||
["release", ep___release],
|
||||
["promo/read", ep___promo_read],
|
||||
["request-reset-password", ep___requestResetPassword],
|
||||
["reset-password", ep___resetPassword],
|
||||
|
|
|
@ -87,6 +87,7 @@ export const paramDef = {
|
|||
description: { ...Users.descriptionSchema, nullable: true },
|
||||
location: { ...Users.locationSchema, nullable: true },
|
||||
birthday: { ...Users.birthdaySchema, nullable: true },
|
||||
lang: { type: "string", nullable: true },
|
||||
avatarId: { type: "string", format: "misskey:id", nullable: true },
|
||||
bannerId: { type: "string", format: "misskey:id", nullable: true },
|
||||
fields: {
|
||||
|
@ -154,6 +155,7 @@ export default define(meta, paramDef, async (ps, _user, token) => {
|
|||
|
||||
if (ps.name !== undefined) updates.name = ps.name;
|
||||
if (ps.description !== undefined) profileUpdates.description = ps.description;
|
||||
if (typeof ps.lang === "string") profileUpdates.lang = ps.lang;
|
||||
if (ps.location !== undefined) profileUpdates.location = ps.location;
|
||||
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
|
||||
if (ps.ffVisibility !== undefined)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import define from "@/server/api/define.js";
|
||||
import { latestVersion } from "backend-rs";
|
||||
|
||||
export const meta = {
|
||||
tags: ["meta"],
|
||||
|
@ -14,14 +15,7 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
export default define(meta, paramDef, async () => {
|
||||
let latest_version;
|
||||
await fetch("https://firefish.dev/firefish/firefish/-/raw/main/package.json")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
latest_version = data.version;
|
||||
});
|
||||
|
||||
return {
|
||||
latest_version,
|
||||
latest_version: await latestVersion(),
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
import define from "@/server/api/define.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["meta"],
|
||||
description: "Get release notes from Codeberg",
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: false,
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
export default define(meta, paramDef, async () => {
|
||||
let release;
|
||||
|
||||
await fetch(
|
||||
"https://firefish.dev/firefish/firefish/-/raw/develop/release.json",
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
release = data;
|
||||
});
|
||||
return release;
|
||||
});
|
|
@ -1,9 +1,7 @@
|
|||
import Router from "@koa/router";
|
||||
import { config } from "@/config.js";
|
||||
import { fetchMeta } from "backend-rs";
|
||||
import { Users, Notes } from "@/models/index.js";
|
||||
import { IsNull, MoreThan } from "typeorm";
|
||||
import { Cache } from "@/misc/cache.js";
|
||||
import { nodeinfo_2_0, nodeinfo_2_1 } from "backend-rs";
|
||||
import { fromRustObject } from "@/prelude/undefined-to-null.js";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
|
@ -22,101 +20,14 @@ export const links = [
|
|||
},
|
||||
];
|
||||
|
||||
const nodeinfo2 = async () => {
|
||||
const now = Date.now();
|
||||
const [meta, total, activeHalfyear, activeMonth, localPosts] =
|
||||
await Promise.all([
|
||||
fetchMeta(false),
|
||||
Users.count({ where: { host: IsNull() } }),
|
||||
Users.count({
|
||||
where: {
|
||||
host: IsNull(),
|
||||
lastActiveDate: MoreThan(new Date(now - 15552000000)),
|
||||
},
|
||||
}),
|
||||
Users.count({
|
||||
where: {
|
||||
host: IsNull(),
|
||||
lastActiveDate: MoreThan(new Date(now - 2592000000)),
|
||||
},
|
||||
}),
|
||||
Notes.count({ where: { userHost: IsNull() } }),
|
||||
]);
|
||||
|
||||
const proxyAccount = meta.proxyAccountId
|
||||
? await Users.pack(meta.proxyAccountId).catch(() => null)
|
||||
: null;
|
||||
|
||||
return {
|
||||
software: {
|
||||
name: "firefish",
|
||||
version: config.version,
|
||||
repository: meta.repositoryUrl,
|
||||
homepage: "https://firefish.dev/firefish/firefish",
|
||||
},
|
||||
protocols: ["activitypub"],
|
||||
services: {
|
||||
inbound: [] as string[],
|
||||
outbound: ["atom1.0", "rss2.0"],
|
||||
},
|
||||
openRegistrations: !meta.disableRegistration,
|
||||
usage: {
|
||||
users: { total, activeHalfyear, activeMonth },
|
||||
localPosts,
|
||||
localComments: 0,
|
||||
},
|
||||
metadata: {
|
||||
nodeName: meta.name,
|
||||
nodeDescription: meta.description,
|
||||
maintainer: {
|
||||
name: meta.maintainerName,
|
||||
email: meta.maintainerEmail,
|
||||
},
|
||||
langs: meta.langs,
|
||||
tosUrl: meta.tosUrl,
|
||||
repositoryUrl: meta.repositoryUrl,
|
||||
feedbackUrl: meta.feedbackUrl,
|
||||
disableRegistration: meta.disableRegistration,
|
||||
disableLocalTimeline: meta.disableLocalTimeline,
|
||||
disableRecommendedTimeline: meta.disableRecommendedTimeline,
|
||||
disableGlobalTimeline: meta.disableGlobalTimeline,
|
||||
emailRequiredForSignup: meta.emailRequiredForSignup,
|
||||
postEditing: true,
|
||||
postImports: meta.experimentalFeatures?.postImports || false,
|
||||
enableHcaptcha: meta.enableHcaptcha,
|
||||
enableRecaptcha: meta.enableRecaptcha,
|
||||
maxNoteTextLength: config.maxNoteLength,
|
||||
maxCaptionTextLength: config.maxCaptionLength,
|
||||
enableEmail: meta.enableEmail,
|
||||
enableServiceWorker: meta.enableServiceWorker,
|
||||
proxyAccountName: proxyAccount ? proxyAccount.username : null,
|
||||
themeColor: meta.themeColor || "#31748f",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const cache = new Cache<Awaited<ReturnType<typeof nodeinfo2>>>(
|
||||
"nodeinfo",
|
||||
60 * 10,
|
||||
);
|
||||
|
||||
router.get(nodeinfo2_1path, async (ctx) => {
|
||||
const base = await cache.fetch(null, () => nodeinfo2());
|
||||
|
||||
ctx.body = { version: "2.1", ...base };
|
||||
ctx.set("Cache-Control", "public, max-age=600");
|
||||
ctx.body = fromRustObject(await nodeinfo_2_1());
|
||||
ctx.set("Cache-Control", "public, max-age=3600");
|
||||
});
|
||||
|
||||
router.get(nodeinfo2_0path, async (ctx) => {
|
||||
const base = await cache.fetch(null, () => nodeinfo2());
|
||||
|
||||
// @ts-ignore
|
||||
base.software.repository = undefined;
|
||||
// @ts-ignore
|
||||
base.software.homepage = undefined;
|
||||
|
||||
ctx.body = { version: "2.0", ...base };
|
||||
ctx.set("Cache-Control", "public, max-age=600");
|
||||
ctx.body = fromRustObject(await nodeinfo_2_0());
|
||||
ctx.set("Cache-Control", "public, max-age=3600");
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
import { Instances } from "@/models/index.js";
|
||||
import { getFetchInstanceMetadataLock } from "@/misc/app-lock.js";
|
||||
import Logger from "@/services/logger.js";
|
||||
import { type Nodeinfo, fetchNodeinfo } from "backend-rs";
|
||||
import { inspect } from "node:util";
|
||||
|
||||
const logger = new Logger("metadata", "cyan");
|
||||
|
@ -36,7 +37,7 @@ export async function fetchInstanceMetadata(
|
|||
|
||||
try {
|
||||
const [info, dom, manifest] = await Promise.all([
|
||||
fetchNodeinfo(instance).catch(() => null),
|
||||
fetchNodeinfo(instance.host).catch(() => null),
|
||||
fetchDom(instance).catch(() => null),
|
||||
fetchManifest(instance).catch(() => null),
|
||||
]);
|
||||
|
@ -57,30 +58,26 @@ export async function fetchInstanceMetadata(
|
|||
|
||||
if (info) {
|
||||
updates.softwareName =
|
||||
info.software?.name
|
||||
?.toLowerCase()
|
||||
info.software.name
|
||||
.toLowerCase()
|
||||
.substring(0, MAX_LENGTH_INSTANCE.softwareName) || null;
|
||||
updates.softwareVersion =
|
||||
info.software?.version?.substring(
|
||||
info.software.version.substring(
|
||||
0,
|
||||
MAX_LENGTH_INSTANCE.softwareVersion,
|
||||
) || null;
|
||||
updates.openRegistrations = info.openRegistrations;
|
||||
updates.maintainerName = info.metadata
|
||||
? info.metadata.maintainer
|
||||
updates.maintainerName = info.metadata.maintainer
|
||||
? info.metadata.maintainer.name?.substring(
|
||||
0,
|
||||
MAX_LENGTH_INSTANCE.maintainerName,
|
||||
) || null
|
||||
: null
|
||||
: null;
|
||||
updates.maintainerEmail = info.metadata
|
||||
? info.metadata.maintainer
|
||||
updates.maintainerEmail = info.metadata.maintainer
|
||||
? info.metadata.maintainer.email?.substring(
|
||||
0,
|
||||
MAX_LENGTH_INSTANCE.maintainerEmail,
|
||||
) || null
|
||||
: null
|
||||
: null;
|
||||
}
|
||||
|
||||
|
@ -115,75 +112,6 @@ export async function fetchInstanceMetadata(
|
|||
}
|
||||
}
|
||||
|
||||
type NodeInfo = {
|
||||
openRegistrations?: boolean;
|
||||
software?: {
|
||||
name?: string;
|
||||
version?: string;
|
||||
};
|
||||
metadata?: {
|
||||
name?: string;
|
||||
nodeName?: string;
|
||||
nodeDescription?: string;
|
||||
description?: string;
|
||||
maintainer?: {
|
||||
name?: string;
|
||||
email?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
async function fetchNodeinfo(instance: Instance): Promise<NodeInfo> {
|
||||
logger.info(`Fetching nodeinfo of ${instance.host} ...`);
|
||||
|
||||
try {
|
||||
const wellknown = (await getJson(
|
||||
`https://${instance.host}/.well-known/nodeinfo`,
|
||||
).catch((e) => {
|
||||
if (e.statusCode === 404) {
|
||||
throw new Error("No nodeinfo provided");
|
||||
} else {
|
||||
throw new Error(inspect(e));
|
||||
}
|
||||
})) as Record<string, unknown>;
|
||||
|
||||
if (wellknown.links == null || !Array.isArray(wellknown.links)) {
|
||||
throw new Error("No wellknown links");
|
||||
}
|
||||
|
||||
const links = wellknown.links as any[];
|
||||
|
||||
const lnik1_0 = links.find(
|
||||
(link) => link.rel === "http://nodeinfo.diaspora.software/ns/schema/1.0",
|
||||
);
|
||||
const lnik2_0 = links.find(
|
||||
(link) => link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
||||
);
|
||||
const lnik2_1 = links.find(
|
||||
(link) => link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.1",
|
||||
);
|
||||
const link = lnik2_1 || lnik2_0 || lnik1_0;
|
||||
|
||||
if (link == null) {
|
||||
throw new Error("No nodeinfo link provided");
|
||||
}
|
||||
|
||||
const info = await getJson(link.href).catch((e) => {
|
||||
throw new Error(inspect(e));
|
||||
});
|
||||
|
||||
logger.info(`Successfuly fetched nodeinfo of ${instance.host}`);
|
||||
|
||||
return info as NodeInfo;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Failed to fetch nodeinfo of ${instance.host}:\n${inspect(e)}`,
|
||||
);
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDom(instance: Instance): Promise<Window["document"]> {
|
||||
logger.info(`Fetching HTML of ${instance.host} ...`);
|
||||
|
||||
|
@ -272,7 +200,7 @@ async function fetchIconUrl(
|
|||
}
|
||||
|
||||
async function getThemeColor(
|
||||
info: NodeInfo | null,
|
||||
info: Nodeinfo | null,
|
||||
doc: Window["document"] | null,
|
||||
manifest: Record<string, any> | null,
|
||||
): Promise<string | null> {
|
||||
|
@ -290,7 +218,7 @@ async function getThemeColor(
|
|||
}
|
||||
|
||||
async function getSiteName(
|
||||
info: NodeInfo | null,
|
||||
info: Nodeinfo | null,
|
||||
doc: Window["document"] | null,
|
||||
manifest: Record<string, any> | null,
|
||||
): Promise<string | undefined | null> {
|
||||
|
@ -318,7 +246,7 @@ async function getSiteName(
|
|||
}
|
||||
|
||||
async function getDescription(
|
||||
info: NodeInfo | null,
|
||||
info: Nodeinfo | null,
|
||||
doc: Window["document"] | null,
|
||||
manifest: Record<string, any> | null,
|
||||
): Promise<string | null> {
|
||||
|
|
|
@ -66,7 +66,7 @@ import { Mutex } from "redis-semaphore";
|
|||
import { langmap } from "@/misc/langmap.js";
|
||||
import Logger from "@/services/logger.js";
|
||||
import { inspect } from "node:util";
|
||||
import { undefinedToNull } from "@/prelude/undefined-to-null.js";
|
||||
import { toRustObject } from "@/prelude/undefined-to-null.js";
|
||||
|
||||
const logger = new Logger("create-note");
|
||||
|
||||
|
@ -407,7 +407,7 @@ export default async (
|
|||
checkHitAntenna(antenna, note, user).then((hit) => {
|
||||
if (hit) {
|
||||
// TODO: do this more sanely
|
||||
addNoteToAntenna(antenna.id, undefinedToNull(note) as Note);
|
||||
addNoteToAntenna(antenna.id, toRustObject(note));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -21,8 +21,6 @@
|
|||
"@syuilo/aiscript": "0.17.0",
|
||||
"@types/autosize": "^4.0.3",
|
||||
"@types/glob": "8.1.0",
|
||||
"@types/gulp": "4.0.17",
|
||||
"@types/gulp-rename": "2.0.6",
|
||||
"@types/insert-text-at-cursor": "^0.3.2",
|
||||
"@types/katex": "0.16.7",
|
||||
"@types/matter-js": "0.19.6",
|
||||
|
|
|
@ -132,6 +132,9 @@ export async function signIn(token: Account["token"], redirect?: string) {
|
|||
if (_DEV_) console.log("logging as token ", token);
|
||||
const newAccount = await fetchAccount(token);
|
||||
localStorage.setItem("account", JSON.stringify(newAccount));
|
||||
if (newAccount.lang) {
|
||||
localStorage.setItem("lang", newAccount.lang);
|
||||
}
|
||||
document.cookie = `token=${token}; path=/; max-age=31536000`; // bull dashboardの認証とかで使う
|
||||
await addAccount(newAccount.id, token);
|
||||
|
||||
|
|
|
@ -130,7 +130,7 @@
|
|||
v-else-if="item.type === 'parent'"
|
||||
class="_button item parent"
|
||||
:class="{ childShowing: childShowingItem === item }"
|
||||
@mouseenter="showChildren(item, $event)"
|
||||
@mouseenter.passive="showChildren(item, $event)"
|
||||
@click.stop="showChildren(item, $event)"
|
||||
>
|
||||
<i
|
||||
|
@ -318,6 +318,7 @@ function onItemMouseLeave(_item) {
|
|||
|
||||
async function showChildren(item: MenuParent, ev: MouseEvent) {
|
||||
if (props.asDrawer) {
|
||||
if (ev.type === "mouseenter") return;
|
||||
os.popupMenu(item.children, (ev.currentTarget ?? ev.target) as HTMLElement);
|
||||
close();
|
||||
} else {
|
||||
|
|
|
@ -71,7 +71,7 @@ import { foldNotifications } from "@/scripts/fold";
|
|||
import { defaultStore } from "@/store";
|
||||
|
||||
const props = defineProps<{
|
||||
includeTypes?: (typeof notificationTypes)[number][];
|
||||
includeTypes?: (typeof notificationTypes)[number][] | null;
|
||||
unreadOnly?: boolean;
|
||||
}>();
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ const FIRE_THRESHOLD = defaultStore.state.pullToRefreshThreshold;
|
|||
const RELEASE_TRANSITION_DURATION = 200;
|
||||
const PULL_BRAKE_BASE = 1.5;
|
||||
const PULL_BRAKE_FACTOR = 170;
|
||||
const MAX_PULL_TAN_ANGLE = Math.tan((1 / 6) * Math.PI); // 30°
|
||||
|
||||
const pullStarted = ref(false);
|
||||
const pullEnded = ref(false);
|
||||
|
@ -53,6 +54,7 @@ const pullDistance = ref(0);
|
|||
let disabled = false;
|
||||
const supportPointerDesktop = false;
|
||||
let startScreenY: number | null = null;
|
||||
let startScreenX: number | null = null;
|
||||
|
||||
const rootEl = shallowRef<HTMLDivElement>();
|
||||
let scrollEl: HTMLElement | null = null;
|
||||
|
@ -72,11 +74,16 @@ function getScreenY(event) {
|
|||
if (supportPointerDesktop) return event.screenY;
|
||||
return event.touches[0].screenY;
|
||||
}
|
||||
function getScreenX(event) {
|
||||
if (supportPointerDesktop) return event.screenX;
|
||||
return event.touches[0].screenX;
|
||||
}
|
||||
|
||||
function moveStart(event) {
|
||||
if (!pullStarted.value && !isRefreshing.value && !disabled) {
|
||||
pullStarted.value = true;
|
||||
startScreenY = getScreenY(event);
|
||||
startScreenX = getScreenX(event);
|
||||
pullDistance.value = 0;
|
||||
}
|
||||
}
|
||||
|
@ -117,6 +124,7 @@ async function closeContent() {
|
|||
function moveEnd() {
|
||||
if (pullStarted.value && !isRefreshing.value) {
|
||||
startScreenY = null;
|
||||
startScreenX = null;
|
||||
if (pullEnded.value) {
|
||||
pullEnded.value = false;
|
||||
isRefreshing.value = true;
|
||||
|
@ -146,11 +154,17 @@ function moving(event: TouchEvent | PointerEvent) {
|
|||
moveEnd();
|
||||
return;
|
||||
}
|
||||
if (startScreenY === null) {
|
||||
startScreenY = getScreenY(event);
|
||||
}
|
||||
startScreenX ??= getScreenX(event);
|
||||
startScreenY ??= getScreenY(event);
|
||||
const moveScreenY = getScreenY(event);
|
||||
const moveScreenX = getScreenX(event);
|
||||
const moveHeight = moveScreenY - startScreenY!;
|
||||
const moveWidth = moveScreenX - startScreenX!;
|
||||
if (Math.abs(moveWidth / moveHeight) > MAX_PULL_TAN_ANGLE) {
|
||||
if (Math.abs(moveWidth) > 30) pullStarted.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
pullDistance.value = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE);
|
||||
|
||||
if (pullDistance.value > 0) {
|
||||
|
|
|
@ -10,17 +10,6 @@
|
|||
<MkSparkle>{{ i18n.ts.misskeyUpdated }}</MkSparkle>
|
||||
</div>
|
||||
<div :class="$style.version">✨ {{ version }} 🚀</div>
|
||||
<div v-if="newRelease" :class="$style.releaseNotes">
|
||||
<Mfm :text="data.notes" />
|
||||
<div v-if="data.screenshots.length > 0" style="max-width: 500">
|
||||
<img
|
||||
v-for="i in data.screenshots"
|
||||
:key="i"
|
||||
:src="i"
|
||||
alt="screenshot"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<MkButton
|
||||
:class="$style.gotIt"
|
||||
primary
|
||||
|
@ -33,28 +22,14 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, shallowRef } from "vue";
|
||||
import { shallowRef } from "vue";
|
||||
import MkModal from "@/components/MkModal.vue";
|
||||
import MkSparkle from "@/components/MkSparkle.vue";
|
||||
import MkButton from "@/components/MkButton.vue";
|
||||
import { version } from "@/config";
|
||||
import { i18n } from "@/i18n";
|
||||
import * as os from "@/os";
|
||||
|
||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||
|
||||
const newRelease = ref(false);
|
||||
const data = ref(Object);
|
||||
|
||||
os.api("release").then((res) => {
|
||||
data.value = res;
|
||||
newRelease.value = version === data.value?.version;
|
||||
});
|
||||
|
||||
console.log(`Version: ${version}`);
|
||||
console.log(`Data version: ${data.value.version}`);
|
||||
console.log(newRelease.value);
|
||||
console.log(data.value);
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
@ -81,10 +56,4 @@ console.log(data.value);
|
|||
.gotIt {
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
|
||||
.releaseNotes {
|
||||
> img {
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -283,44 +283,44 @@ export default defineComponent({
|
|||
? "perspective(128px) rotateY"
|
||||
: "rotate";
|
||||
const degrees = Number.parseFloat(
|
||||
token.props.args.deg.toString() ?? "90",
|
||||
(token.props.args.deg ?? "90").toString(),
|
||||
);
|
||||
style = `transform: ${rotate}(${degrees}deg); transform-origin: center center;`;
|
||||
break;
|
||||
}
|
||||
case "position": {
|
||||
const x = Number.parseFloat(
|
||||
token.props.args.x.toString() ?? "0",
|
||||
(token.props.args.x ?? "0").toString(),
|
||||
);
|
||||
const y = Number.parseFloat(
|
||||
token.props.args.y.toString() ?? "0",
|
||||
(token.props.args.y ?? "0").toString(),
|
||||
);
|
||||
style = `transform: translateX(${x}em) translateY(${y}em);`;
|
||||
break;
|
||||
}
|
||||
case "crop": {
|
||||
const top = Number.parseFloat(
|
||||
token.props.args.top.toString() ?? "0",
|
||||
(token.props.args.top ?? "0").toString(),
|
||||
);
|
||||
const right = Number.parseFloat(
|
||||
token.props.args.right.toString() ?? "0",
|
||||
(token.props.args.right ?? "0").toString(),
|
||||
);
|
||||
const bottom = Number.parseFloat(
|
||||
token.props.args.bottom.toString() ?? "0",
|
||||
(token.props.args.bottom ?? "0").toString(),
|
||||
);
|
||||
const left = Number.parseFloat(
|
||||
token.props.args.left.toString() ?? "0",
|
||||
(token.props.args.left ?? "0").toString(),
|
||||
);
|
||||
style = `clip-path: inset(${top}% ${right}% ${bottom}% ${left}%);`;
|
||||
break;
|
||||
}
|
||||
case "scale": {
|
||||
const x = Math.min(
|
||||
Number.parseFloat(token.props.args.x.toString() ?? "1"),
|
||||
Number.parseFloat((token.props.args.x ?? "1").toString()),
|
||||
5,
|
||||
);
|
||||
const y = Math.min(
|
||||
Number.parseFloat(token.props.args.y.toString() ?? "1"),
|
||||
Number.parseFloat((token.props.args.y ?? "1").toString()),
|
||||
5,
|
||||
);
|
||||
style = `transform: scale(${x}, ${y});`;
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
>
|
||||
<swiper-slide>
|
||||
<XNotifications
|
||||
:key="'tab1'"
|
||||
class="notifications"
|
||||
:include-types="includeTypes"
|
||||
:unread-only="false"
|
||||
|
@ -34,16 +35,18 @@
|
|||
</swiper-slide>
|
||||
<swiper-slide>
|
||||
<XNotifications
|
||||
v-if="tab === 'reactions'"
|
||||
:key="'tab2'"
|
||||
class="notifications"
|
||||
:include-types="['reaction']"
|
||||
:unread-only="false"
|
||||
/>
|
||||
</swiper-slide>
|
||||
<swiper-slide>
|
||||
<XNotes :pagination="mentionsPagination" />
|
||||
<XNotes v-if="tab === 'mentions'" :key="'tab3'" :pagination="mentionsPagination" />
|
||||
</swiper-slide>
|
||||
<swiper-slide>
|
||||
<XNotes :pagination="directNotesPagination" />
|
||||
<XNotes v-if="tab === 'directNotes'" :key="'tab4'" :pagination="directNotesPagination" />
|
||||
</swiper-slide>
|
||||
</swiper>
|
||||
</MkSpacer>
|
||||
|
@ -54,6 +57,7 @@
|
|||
import { computed, ref, watch } from "vue";
|
||||
import { Virtual } from "swiper/modules";
|
||||
import { Swiper, SwiperSlide } from "swiper/vue";
|
||||
import type { Swiper as SwiperType } from "swiper/types";
|
||||
import { notificationTypes } from "firefish-js";
|
||||
import XNotifications from "@/components/MkNotifications.vue";
|
||||
import XNotes from "@/components/MkNotes.vue";
|
||||
|
@ -70,7 +74,7 @@ const tabs = ["all", "reactions", "mentions", "directNotes"];
|
|||
const tab = ref(tabs[0]);
|
||||
watch(tab, () => syncSlide(tabs.indexOf(tab.value)));
|
||||
|
||||
const includeTypes = ref<string[] | null>(null);
|
||||
const includeTypes = ref<(typeof notificationTypes)[number][] | null>(null);
|
||||
os.api("notifications/mark-all-as-read");
|
||||
|
||||
const MOBILE_THRESHOLD = 500;
|
||||
|
@ -98,7 +102,7 @@ const directNotesPagination = {
|
|||
function setFilter(ev) {
|
||||
const typeItems = notificationTypes.map((t) => ({
|
||||
text: i18n.t(`_notification._types.${t}`),
|
||||
active: includeTypes.value && includeTypes.value.includes(t),
|
||||
active: includeTypes.value?.includes(t),
|
||||
action: () => {
|
||||
includeTypes.value = [t];
|
||||
},
|
||||
|
@ -121,25 +125,23 @@ function setFilter(ev) {
|
|||
}
|
||||
|
||||
const headerActions = computed(() =>
|
||||
[
|
||||
tab.value === "all"
|
||||
? {
|
||||
? [
|
||||
{
|
||||
text: i18n.ts.filter,
|
||||
icon: `${icon("ph-funnel")}`,
|
||||
highlighted: includeTypes.value != null,
|
||||
handler: setFilter,
|
||||
}
|
||||
: undefined,
|
||||
tab.value === "all"
|
||||
? {
|
||||
},
|
||||
{
|
||||
text: i18n.ts.markAllAsRead,
|
||||
icon: `${icon("ph-check")}`,
|
||||
handler: () => {
|
||||
os.apiWithDialog("notifications/mark-all-as-read");
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
].filter((x) => x !== undefined),
|
||||
},
|
||||
]
|
||||
: [],
|
||||
);
|
||||
|
||||
const headerTabs = computed(() => [
|
||||
|
@ -172,18 +174,19 @@ definePageMetadata(
|
|||
})),
|
||||
);
|
||||
|
||||
let swiperRef = null;
|
||||
let swiperRef: SwiperType | null = null;
|
||||
|
||||
function setSwiperRef(swiper) {
|
||||
function setSwiperRef(swiper: SwiperType) {
|
||||
swiperRef = swiper;
|
||||
syncSlide(tabs.indexOf(tab.value));
|
||||
}
|
||||
|
||||
function onSlideChange() {
|
||||
tab.value = tabs[swiperRef.activeIndex];
|
||||
if (tab.value !== tabs[swiperRef!.activeIndex])
|
||||
tab.value = tabs[swiperRef!.activeIndex];
|
||||
}
|
||||
|
||||
function syncSlide(index) {
|
||||
swiperRef.slideTo(index);
|
||||
function syncSlide(index: number) {
|
||||
if (index !== swiperRef!.activeIndex) swiperRef!.slideTo(index);
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -14,6 +14,12 @@
|
|||
>
|
||||
</template>
|
||||
</I18n>
|
||||
<I18n :src="i18n.ts.i18nServerInfo" v-if="serverLang" tag="span">
|
||||
<template #language><strong>{{ langs.find(a => a[0] === serverLang)?.[1] ?? serverLang }}</strong></template>
|
||||
</I18n>
|
||||
<button class="_textButton" @click="updateServerLang" v-if="lang && lang !== serverLang">
|
||||
{{i18n.t(serverLang ? "i18nServerChange" : "i18nServerSet", { language: langs.find(a => a[0] === lang)?.[1] ?? lang })}}
|
||||
</button>
|
||||
</template>
|
||||
</FormSelect>
|
||||
|
||||
|
@ -404,6 +410,7 @@ import { deviceKind } from "@/scripts/device-kind";
|
|||
import icon from "@/scripts/icon";
|
||||
|
||||
const lang = ref(localStorage.getItem("lang"));
|
||||
const serverLang = ref(me?.lang);
|
||||
const translateLang = ref(localStorage.getItem("translateLang"));
|
||||
const fontSize = ref(localStorage.getItem("fontSize"));
|
||||
const useSystemFont = ref(localStorage.getItem("useSystemFont") !== "f");
|
||||
|
@ -559,6 +566,14 @@ const foldNotification = computed(
|
|||
// });
|
||||
// }
|
||||
|
||||
function updateServerLang() {
|
||||
os.api("i/update", {
|
||||
lang: lang.value,
|
||||
}).then((i) => {
|
||||
serverLang.value = i.lang;
|
||||
});
|
||||
}
|
||||
|
||||
watch(swipeOnDesktop, () => {
|
||||
defaultStore.set("swipeOnMobile", true);
|
||||
});
|
||||
|
|
|
@ -265,14 +265,18 @@ export function getUserMenu(user, router: Router = mainRouter) {
|
|||
icon: "ph-qr-code ph-bold ph-lg",
|
||||
text: i18n.ts.getQrCode,
|
||||
action: () => {
|
||||
os.displayQrCode(`https://${host}/follow-me?acct=${user.username}`);
|
||||
os.displayQrCode(
|
||||
`https://${host}/follow-me?acct=${acct.toString(user)}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: `${icon("ph-hand-waving")}`,
|
||||
text: i18n.ts.copyRemoteFollowUrl,
|
||||
action: () => {
|
||||
copyToClipboard(`https://${host}/follow-me?acct=${user.username}`);
|
||||
copyToClipboard(
|
||||
`https://${host}/follow-me?acct=${acct.toString(user)}`,
|
||||
);
|
||||
os.success();
|
||||
},
|
||||
},
|
||||
|
@ -321,7 +325,7 @@ export function getUserMenu(user, router: Router = mainRouter) {
|
|||
icon: `${icon("ph-hand-waving")}`,
|
||||
text: i18n.ts.remoteFollow,
|
||||
action: () => {
|
||||
router.push(`/follow-me?acct=${user.username}`);
|
||||
router.push(`/follow-me?acct=${acct.toString(user)}`);
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
|
|
2366
pnpm-lock.yaml
2366
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
15
renovate.json
Normal file
15
renovate.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:base"],
|
||||
"rangeStrategy": "bump",
|
||||
"branchConcurrentLimit": 5,
|
||||
"enabledManagers": ["npm", "cargo"],
|
||||
"baseBranches": ["develop"],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true,
|
||||
"recreateWhen": "always",
|
||||
"rebaseStalePrs": true,
|
||||
"branchTopic": "lock-file-maintenance",
|
||||
"commitMessageAction": "Lock file maintenance"
|
||||
}
|
||||
}
|
41
scripts/copy-assets.mjs
Normal file
41
scripts/copy-assets.mjs
Normal file
|
@ -0,0 +1,41 @@
|
|||
import fs from "node:fs/promises";
|
||||
import path, { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repositoryRootDir = join(path.dirname(fileURLToPath(import.meta.url)), "../");
|
||||
const file = (relativePath) => join(repositoryRootDir, relativePath);
|
||||
|
||||
await (async () => {
|
||||
await fs.rm(file("built/_client_dist_/locales"), { recursive: true, force: true });
|
||||
await Promise.all([
|
||||
fs.cp(file("packages/backend/src/server/web"), file("packages/backend/built/server/web"), { recursive: true }),
|
||||
fs.cp(file("custom/assets"), file("packages/backend/assets"), { recursive: true }),
|
||||
fs.cp(file("packages/client/node_modules/three/examples/fonts"), file("built/_client_dist_/fonts"), { recursive: true }),
|
||||
fs.mkdir(file("built/_client_dist_/locales"), { recursive: true }),
|
||||
]);
|
||||
|
||||
const locales = (await import("../locales/index.mjs")).default;
|
||||
const meta = (await import("../built/meta.json", { assert: { type: "json" } })).default;
|
||||
|
||||
for await (const [lang, locale] of Object.entries(locales)) {
|
||||
await fs.writeFile(
|
||||
file(`built/_client_dist_/locales/${lang}.${meta.version}.json`),
|
||||
JSON.stringify({ ...locale, _version_: meta.version }),
|
||||
"utf-8",
|
||||
);
|
||||
}
|
||||
|
||||
const js_assets = [
|
||||
file("packages/backend/built/server/web/boot.js"),
|
||||
file("packages/backend/built/server/web/bios.js"),
|
||||
file("packages/backend/built/server/web/cli.js"),
|
||||
];
|
||||
|
||||
for await (const js_file of js_assets) {
|
||||
const content = (await fs.readFile(js_file, "utf-8"))
|
||||
.replace("SUPPORTED_LANGS", JSON.stringify(Object.keys(locales)));
|
||||
await fs.writeFile(js_file, content, "utf-8");
|
||||
}
|
||||
|
||||
// TODO?: minify packages/backend/built/server/web/*.css
|
||||
})();
|
|
@ -1,6 +1,7 @@
|
|||
import path, { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { execa } from "execa";
|
||||
import fs from "node:fs";
|
||||
|
||||
(async () => {
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
@ -32,4 +33,18 @@ import { execa } from "execa";
|
|||
stdio: "inherit",
|
||||
}
|
||||
);
|
||||
|
||||
if (!fs.existsSync(join(__dirname, "/../packages/backend-rs/built/index.js"))) {
|
||||
fs.copyFileSync(
|
||||
join(__dirname, "/../packages/backend-rs/index.js"),
|
||||
join(__dirname, "/../packages/backend-rs/built/index.js"),
|
||||
);
|
||||
console.warn("backend-rs/built/index.js has not been updated (https://github.com/napi-rs/napi-rs/issues/1768)");
|
||||
}
|
||||
if (!fs.existsSync(join(__dirname, "/../packages/backend-rs/built/index.d.ts"))) {
|
||||
fs.copyFileSync(
|
||||
join(__dirname, "/../packages/backend-rs/index.d.ts"),
|
||||
join(__dirname, "/../packages/backend-rs/built/index.d.ts"),
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -11,12 +11,6 @@ import { execa } from "execa";
|
|||
stderr: process.stderr,
|
||||
});
|
||||
|
||||
execa("pnpm", ["dlx", "gulp", "watch"], {
|
||||
cwd: join(__dirname, "/../"),
|
||||
stdout: process.stdout,
|
||||
stderr: process.stderr,
|
||||
});
|
||||
|
||||
execa("pnpm", ["--filter", "backend", "watch"], {
|
||||
cwd: join(__dirname, "/../"),
|
||||
stdout: process.stdout,
|
||||
|
|
Loading…
Reference in a new issue