Merge branch 'develop' into refactor/scheduled-posts

This commit is contained in:
naskya 2024-06-06 09:55:57 +09:00
commit 167d89e03e
No known key found for this signature in database
GPG key ID: 712D413B3A9FED5C
195 changed files with 2323 additions and 2270 deletions

View file

@ -6,5 +6,9 @@ indent_size = 2
charset = utf-8
insert_final_newline = true
[*.rs]
indent_style = space
indent_size = 4
[*.yml]
indent_style = space

1
.gitignore vendored
View file

@ -15,6 +15,7 @@ node_modules
report.*.json
# Cargo
/.cargo
/target
# Cypress

View file

@ -25,10 +25,11 @@ workflow:
- when: never
stages:
- dependency
- test
- doc
- build
- dependency
- clean
variables:
POSTGRES_DB: 'firefish_db'
@ -199,6 +200,8 @@ build:container:
STORAGE_DRIVER: overlay
before_script:
- apt-get update && apt-get -y upgrade
- |-
sed -i -r 's/"version": "([-0-9]+)",/"version": "\1-dev",/' package.json
- apt-get install -y --no-install-recommends ca-certificates fuse-overlayfs buildah
- echo "${CI_REGISTRY_PASSWORD}" | buildah login --username "${CI_REGISTRY_USER}" --password-stdin "${CI_REGISTRY}"
- export IMAGE_TAG="${CI_REGISTRY}/${CI_PROJECT_PATH}/develop:not-for-production"
@ -285,12 +288,12 @@ cargo:doc:
- apt-get update
- apt-get install -y --no-install-recommends build-essential clang mold nodejs npm
- cp ci/cargo/config.toml /usr/local/cargo/config.toml
- npm install --global netlify-cli
script:
- cargo doc --no-deps --document-private-items
- cargo doc --document-private-items
- printf "window.ALL_CRATES = ['backend_rs', 'macro_rs'];" > target/doc/crates.js
- printf '<meta http-equiv="refresh" content="0; url=%s">' 'backend_rs' > target/doc/index.html
- cd target/doc
- netlify deploy --prod --site="${CARGO_DOC_SITE_ID}" --dir=.
- npx --yes netlify-cli deploy --prod --site="${CARGO_DOC_SITE_ID}" --dir=.
renovate:
stage: dependency
@ -303,3 +306,19 @@ renovate:
before_script: []
script:
- renovate --platform gitlab --token "${API_TOKEN}" --endpoint "${CI_SERVER_URL}/api/v4" "${CI_PROJECT_PATH}"
clean:
stage: clean
rules:
- if: $CLEAN && $CI_PIPELINE_SOURCE == 'schedule'
services: []
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 nodejs
- corepack enable
- corepack prepare pnpm@latest --activate
- pnpm install --frozen-lockfile
script:
- pnpm run clean-all

393
Cargo.lock generated
View file

@ -17,17 +17,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
dependencies = [
"getrandom",
"once_cell",
"version_check",
]
[[package]]
name = "ahash"
version = "0.8.11"
@ -309,7 +298,6 @@ dependencies = [
"async-trait",
"futures-channel",
"futures-util",
"parking_lot",
"tokio",
]
@ -323,18 +311,6 @@ dependencies = [
"blowfish",
"getrandom",
"subtle",
"zeroize",
]
[[package]]
name = "bigdecimal"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6773ddc0eafc0e509fb60e48dff7f450f8e674a0686ae8605e8d9901bd5eefa"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
@ -343,12 +319,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e0d60973d9320722cb1206f412740e162a33b8547ea8d6be75d7cff237c7a85"
[[package]]
name = "bit_field"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
[[package]]
name = "bitflags"
version = "1.3.2"
@ -370,18 +340,6 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c12d1856e42f0d817a835fe55853957c85c8c8a470114029143d3f12671446e"
[[package]]
name = "bitvec"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
dependencies = [
"funty",
"radium",
"tap",
"wyz",
]
[[package]]
name = "blake2"
version = "0.10.6"
@ -410,30 +368,6 @@ dependencies = [
"cipher",
]
[[package]]
name = "borsh"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbe5b10e214954177fb1dc9fbd20a1a2608fe99e6c832033bdc7cea287a20d77"
dependencies = [
"borsh-derive",
"cfg_aliases",
]
[[package]]
name = "borsh-derive"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7a8646f94ab393e43e8b35a2558b1624bed28b97ee09c5d15456e3c9463f46d"
dependencies = [
"once_cell",
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.66",
"syn_derive",
]
[[package]]
name = "built"
version = "0.7.3"
@ -446,28 +380,6 @@ version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "bytecheck"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2"
dependencies = [
"bytecheck_derive",
"ptr_meta",
"simdutf8",
]
[[package]]
name = "bytecheck_derive"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "bytemuck"
version = "1.16.0"
@ -525,12 +437,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "chrono"
version = "0.4.38"
@ -690,12 +596,6 @@ version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]]
name = "crunchy"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "crypto-bigint"
version = "0.5.5"
@ -995,22 +895,6 @@ version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "exr"
version = "1.72.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4"
dependencies = [
"bit_field",
"flume",
"half",
"lebe",
"miniz_oxide",
"rayon-core",
"smallvec",
"zune-inflate",
]
[[package]]
name = "fastrand"
version = "1.9.0"
@ -1102,12 +986,6 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "futures"
version = "0.3.30"
@ -1261,32 +1139,13 @@ dependencies = [
"subtle",
]
[[package]]
name = "half"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888"
dependencies = [
"cfg-if",
"crunchy",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash 0.7.8",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash 0.8.11",
"ahash",
"allocator-api2",
]
@ -1296,7 +1155,7 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
dependencies = [
"hashbrown 0.14.5",
"hashbrown",
]
[[package]]
@ -1430,14 +1289,11 @@ dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"exr",
"gif",
"image-webp",
"num-traits",
"png",
"qoi",
"ravif",
"rayon",
"rgb",
"tiff",
"zune-core",
@ -1467,7 +1323,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
dependencies = [
"equivalent",
"hashbrown 0.14.5",
"hashbrown",
]
[[package]]
@ -1634,12 +1490,6 @@ dependencies = [
"spin 0.5.2",
]
[[package]]
name = "lebe"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "libc"
version = "0.2.155"
@ -1743,6 +1593,7 @@ version = "0.0.0"
dependencies = [
"convert_case",
"napi",
"napi-derive",
"proc-macro2",
"quote",
"serde",
@ -2420,15 +2271,6 @@ dependencies = [
"elliptic-curve",
]
[[package]]
name = "proc-macro-crate"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284"
dependencies = [
"toml_edit 0.21.1",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
@ -2455,9 +2297,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.84"
version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6"
checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23"
dependencies = [
"unicode-ident",
]
@ -2481,35 +2323,6 @@ dependencies = [
"syn 2.0.66",
]
[[package]]
name = "ptr_meta"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1"
dependencies = [
"ptr_meta_derive",
]
[[package]]
name = "ptr_meta_derive"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "qoi"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
dependencies = [
"bytemuck",
]
[[package]]
name = "quick-error"
version = "2.0.1"
@ -2525,12 +2338,6 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "radium"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rand"
version = "0.8.5"
@ -2633,9 +2440,9 @@ dependencies = [
[[package]]
name = "redis"
version = "0.25.3"
version = "0.25.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6472825949c09872e8f2c50bde59fcefc17748b6be5c90fd67cd8b4daca73bfd"
checksum = "e0d7a6955c7511f60f3ba9e86c6d02b3c3f144f8c24b288d1f4e18074ab8bbec"
dependencies = [
"async-trait",
"bytes",
@ -2697,15 +2504,6 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
[[package]]
name = "rend"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c"
dependencies = [
"bytecheck",
]
[[package]]
name = "rfc6979"
version = "0.4.0"
@ -2740,35 +2538,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rkyv"
version = "0.7.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cba464629b3394fc4dbc6f940ff8f5b4ff5c7aef40f29166fd4ad12acbc99c0"
dependencies = [
"bitvec",
"bytecheck",
"bytes",
"hashbrown 0.12.3",
"ptr_meta",
"rend",
"rkyv_derive",
"seahash",
"tinyvec",
"uuid",
]
[[package]]
name = "rkyv_derive"
version = "0.7.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7dddfff8de25e6f62b9d64e6e432bf1c6736c57d20323e15ee10435fbda7c65"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "rmp"
version = "0.8.14"
@ -2832,22 +2601,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rust_decimal"
version = "1.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1790d1c4c0ca81211399e0e0af16333276f375209e71a37b67698a373db5b47a"
dependencies = [
"arrayvec",
"borsh",
"bytes",
"num-traits",
"rand",
"rkyv",
"serde",
"serde_json",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
@ -2955,12 +2708,10 @@ checksum = "c8814e37dc25de54398ee62228323657520b7f29713b8e238649385dbe473ee0"
dependencies = [
"async-stream",
"async-trait",
"bigdecimal",
"chrono",
"futures",
"log",
"ouroboros",
"rust_decimal",
"sea-orm-macros",
"sea-query",
"sea-query-binder",
@ -2995,15 +2746,11 @@ version = "0.30.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4166a1e072292d46dc91f31617c2a1cdaf55a8be4b5c9f4bf2ba248e3ac4999b"
dependencies = [
"bigdecimal",
"chrono",
"derivative",
"inherent",
"ordered-float",
"rust_decimal",
"serde_json",
"time",
"uuid",
]
[[package]]
@ -3012,22 +2759,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36bbb68df92e820e4d5aeb17b4acd5cc8b5d18b2c36a4dd6f4626aabfa7ab1b9"
dependencies = [
"bigdecimal",
"chrono",
"rust_decimal",
"sea-query",
"serde_json",
"sqlx",
"time",
"uuid",
]
[[package]]
name = "seahash"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "sec1"
version = "0.7.3"
@ -3197,12 +2934,6 @@ dependencies = [
"quote",
]
[[package]]
name = "simdutf8"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a"
[[package]]
name = "siphasher"
version = "0.3.11"
@ -3310,9 +3041,8 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6"
dependencies = [
"ahash 0.8.11",
"ahash",
"atoi",
"bigdecimal",
"byteorder",
"bytes",
"chrono",
@ -3333,7 +3063,6 @@ dependencies = [
"once_cell",
"paste",
"percent-encoding",
"rust_decimal",
"rustls",
"rustls-pemfile",
"serde",
@ -3342,12 +3071,10 @@ dependencies = [
"smallvec",
"sqlformat",
"thiserror",
"time",
"tokio",
"tokio-stream",
"tracing",
"url",
"uuid",
"webpki-roots",
]
@ -3398,7 +3125,6 @@ checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418"
dependencies = [
"atoi",
"base64 0.21.7",
"bigdecimal",
"bitflags 2.5.0",
"byteorder",
"bytes",
@ -3423,7 +3149,6 @@ dependencies = [
"percent-encoding",
"rand",
"rsa 0.9.6",
"rust_decimal",
"serde",
"sha1",
"sha2",
@ -3431,9 +3156,7 @@ dependencies = [
"sqlx-core",
"stringprep",
"thiserror",
"time",
"tracing",
"uuid",
"whoami",
]
@ -3445,7 +3168,6 @@ checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e"
dependencies = [
"atoi",
"base64 0.21.7",
"bigdecimal",
"bitflags 2.5.0",
"byteorder",
"chrono",
@ -3464,10 +3186,8 @@ dependencies = [
"log",
"md-5",
"memchr",
"num-bigint",
"once_cell",
"rand",
"rust_decimal",
"serde",
"serde_json",
"sha2",
@ -3475,9 +3195,7 @@ dependencies = [
"sqlx-core",
"stringprep",
"thiserror",
"time",
"tracing",
"uuid",
"whoami",
]
@ -3500,11 +3218,9 @@ dependencies = [
"percent-encoding",
"serde",
"sqlx-core",
"time",
"tracing",
"url",
"urlencoding",
"uuid",
]
[[package]]
@ -3580,18 +3296,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "syn_derive"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b"
dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.66",
]
[[package]]
name = "synstructure"
version = "0.12.6"
@ -3615,7 +3319,6 @@ dependencies = [
"libc",
"ntapi",
"once_cell",
"rayon",
"windows",
]
@ -3632,12 +3335,6 @@ dependencies = [
"version-compare",
]
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "target-lexicon"
version = "0.12.14"
@ -3704,7 +3401,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
@ -3745,16 +3441,15 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.37.0"
version = "1.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"num_cpus",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
@ -3764,9 +3459,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
dependencies = [
"proc-macro2",
"quote",
@ -3819,7 +3514,7 @@ dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit 0.22.13",
"toml_edit",
]
[[package]]
@ -3831,17 +3526,6 @@ dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
dependencies = [
"indexmap",
"toml_datetime",
"winnow 0.5.40",
]
[[package]]
name = "toml_edit"
version = "0.22.13"
@ -3852,7 +3536,7 @@ dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"winnow 0.6.8",
"winnow",
]
[[package]]
@ -3885,7 +3569,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
@ -3898,17 +3581,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.18"
@ -3917,10 +3589,8 @@ checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
dependencies = [
"nu-ansi-term",
"sharded-slab",
"smallvec",
"thread_local",
"tracing-core",
"tracing-log",
]
[[package]]
@ -4017,12 +3687,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "vcpkg"
version = "0.2.15"
@ -4345,15 +4009,6 @@ version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
[[package]]
name = "winnow"
version = "0.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
dependencies = [
"memchr",
]
[[package]]
name = "winnow"
version = "0.6.8"
@ -4363,15 +4018,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "wyz"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
dependencies = [
"tap",
]
[[package]]
name = "yansi"
version = "0.5.1"
@ -4410,15 +4056,6 @@ version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
[[package]]
name = "zune-inflate"
version = "0.2.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
dependencies = [
"simd-adler32",
]
[[package]]
name = "zune-jpeg"
version = "0.4.11"

View file

@ -5,47 +5,47 @@ resolver = "2"
[workspace.dependencies]
macro-rs = { path = "packages/macro-rs" }
napi = { git = "https://github.com/napi-rs/napi-rs.git", rev = "ca2cd5c35a0c39ec4a94e93c6c5695b681046df2", default-features = false }
napi = { git = "https://github.com/napi-rs/napi-rs.git", rev = "ca2cd5c35a0c39ec4a94e93c6c5695b681046df2" }
napi-derive = "2.16.5"
napi-build = "2.1.3"
argon2 = "0.5.3"
async-trait = "0.1.80"
basen = "0.1.0"
bb8 = "0.8.3"
bcrypt = "0.15.1"
chrono = "0.4.38"
convert_case = "0.6.0"
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"
argon2 = { version = "0.5.3", default-features = false }
async-trait = { version = "0.1.80", default-features = false }
basen = { version = "0.1.0", default-features = false }
bb8 = { version = "0.8.3", default-features = false }
bcrypt = { version = "0.15.1", default-features = false }
chrono = { version = "0.4.38", default-features = false }
convert_case = { version = "0.6.0", default-features = false }
cuid2 = { version = "0.1.2", default-features = false }
emojis = { version = "0.6.2", default-features = false }
idna = { version = "0.5.0", default-features = false }
image = { version = "0.25.1", default-features = false }
isahc = { version = "1.7.2", default-features = false }
nom-exif = { version = "1.2.0", default-features = false }
once_cell = { version = "1.19.0", default-features = false }
openssl = "0.10.64"
pretty_assertions = "1.4.0"
proc-macro2 = "1.0.84"
quote = "1.0.36"
rand = "0.8.5"
redis = { version = "0.25.3", default-features = false }
regex = "1.10.4"
rmp-serde = "1.3.0"
sea-orm = "0.12.15"
serde = "1.0.203"
serde_json = "1.0.117"
serde_yaml = "0.9.34"
strum = "0.26.2"
syn = "2.0.66"
sysinfo = "0.30.12"
thiserror = "1.0.61"
tokio = "1.37.0"
tokio-test = "0.4.4"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
url = "2.5.0"
urlencoding = "2.1.3"
web-push = { git = "https://github.com/pimeys/rust-web-push.git", rev = "40febe4085e3cef9cdfd539c315e3e945aba0656" }
pretty_assertions = { version = "1.4.0", default-features = false }
proc-macro2 = { version = "1.0.85", default-features = false }
quote = { version = "1.0.36", default-features = false }
rand = { version = "0.8.5", default-features = false }
redis = { version = "0.25.4", default-features = false }
regex = { version = "1.10.4", default-features = false }
rmp-serde = { version = "1.3.0", default-features = false }
sea-orm = { version = "0.12.15", default-features = false }
serde = { version = "1.0.203", default-features = false }
serde_json = { version = "1.0.117", default-features = false }
serde_yaml = { version = "0.9.34", default-features = false }
strum = { version = "0.26.2", default-features = false }
syn = { version = "2.0.66", default-features = false }
sysinfo = { version = "0.30.12", default-features = false }
thiserror = { version = "1.0.61", default-features = false }
tokio = { version = "1.38.0", default-features = false }
tokio-test = { version = "0.4.4", default-features = false }
tracing = { version = "0.1.40", default-features = false }
tracing-subscriber = { version = "0.3.18", default-features = false }
url = { version = "2.5.0", default-features = false }
urlencoding = { version = "2.1.3", default-features = false }
web-push = { git = "https://github.com/pimeys/rust-web-push.git", rev = "40febe4085e3cef9cdfd539c315e3e945aba0656", default-features = false }
[profile.release]
lto = true

View file

@ -11,6 +11,7 @@ format:
.PHONY: entities
entities:
rm --recursive --force ./packages/backend/built
pnpm --filter=backend run build:debug
pnpm run migrate
$(MAKE) -C ./packages/backend-rs regenerate-entities

View file

@ -5,6 +5,6 @@ These are the extensions to ActivityPub that Firefish implements. This page uses
## speakAsCat
- Compact IRI: `firefish:speakAsCat`
- Canonical IRI: `https://firefish.dev/ns#speakascat`
- Canonical IRI: `https://firefish.dev/ns#speakAsCat`
Used on actors to indicate that they not only identify as a cat, but also want to have their text be transformed to speak like one, expressed as a boolean value. If this property is set to true, displaying the actors posts will make them speak with “nya” instead of “na” and other cat-related text mannerisms. Used in combination with [misskey:isCat](https://misskey-hub.net/ns/#iscat).

View file

@ -2,6 +2,10 @@
Breaking changes are indicated by the :warning: icon.
## Unreleased
- `GET` request is now allowed for the `latest-version` endpoint.
## v20240523
- Added `scheduledAt` optional parameter to `notes/create` (!10789)

View file

@ -5,6 +5,16 @@ 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.
## Unreleased
- Add the ability to share posts via QR code
- Update the API document page (`/api-doc`)
- Fix bugs
## [v20240601](https://firefish.dev/firefish/firefish/-/merge_requests/10943/commits)
- Fix bugs
## [v20240523](https://firefish.dev/firefish/firefish/-/merge_requests/10898/commits)
- Add scheduled posts

View file

@ -4,9 +4,9 @@ Firefish depends on the following software.
## Runtime dependencies
- At least [NodeJS](https://nodejs.org/en/) v18.19.0 (v20/v21 recommended)
- At least [NodeJS](https://nodejs.org/en/) v18.19.0 (v20/v22 recommended)
- At least [PostgreSQL](https://www.postgresql.org/) v12 (v16 recommended) with [PGroonga](https://pgroonga.github.io/) extension
- At least [Redis](https://redis.io/) v7
- At least [Redis](https://redis.io/) v7 or [Valkey](https://valkey.io/) v7
- Web Proxy (one of the following)
- Caddy (recommended)
- Nginx (recommended)
@ -15,7 +15,7 @@ Firefish depends on the following software.
- Caching server (**optional**, one of the following)
- [DragonflyDB](https://www.dragonflydb.io/)
- [KeyDB](https://keydb.dev/)
- Another [Redis](https://redis.io/) server
- Another [Redis](https://redis.io/) / [Valkey](https://valkey.io/) server
## Build dependencies
@ -30,8 +30,6 @@ This document shows an example procedure for installing these dependencies and F
If you want to use the pre-built container image, please refer to [`install-container.md`](./install-container.md).
If you do not prepare your environment as document, be sure to meet the minimum dependencies given at the bottom of the page.
Make sure that you can use the `sudo` command before proceeding.
## 1. Install dependencies

View file

@ -2,7 +2,7 @@
You can skip intermediate versions when upgrading from an old version, but please read the notices and follow the instructions for each intermediate version before [upgrading](./upgrade.md).
## Unreleased
## v20240601
### For systemd/pm2 users
@ -32,7 +32,7 @@ Therefore, we have contributed to napi-rs to add support for `DateTime<FixedOffs
### For systemd/pm2 users
There is a bug where `pnpm install --frozen-lockfile` may fail on Linux 6.9.x ([GitHub issue](<https://github.com/nodejs/node/issues/53051>)).
There is a bug where `pnpm install --frozen-lockfile` may fail on Linux 6.9.0, 6.9.1, and 6.9.2 ([GitHub issue](<https://github.com/nodejs/node/issues/53051>)).
To check your Linux kernel version, run:

View file

@ -77,7 +77,7 @@ lists: "Listen"
noLists: "Du hast keine Listen angelegt"
note: "Beitrag"
notes: "Beiträge"
following: "Folgen"
following: "Folgend"
followers: "Folgen mir"
followsYou: "Folgt dir"
createList: "Liste erstellen"
@ -95,7 +95,7 @@ youShouldUpgradeClient: "Bitte aktualisiere diese Seite, um eine neuere Version
Clients zu verwenden."
enterListName: "Gib einen Namen für die Liste ein"
privacy: "Privatsphäre"
makeFollowManuallyApprove: "Folgeanfragen bedürfen der Genehmigung"
makeFollowManuallyApprove: "Folgeanfragen müssen akzeptiert werden"
defaultNoteVisibility: "Standard-Sichtbarkeit"
follow: "Folgen"
followRequest: "Follow anfragen"
@ -430,7 +430,7 @@ securityKeyName: "Schlüsselname"
registerSecurityKey: "Sicherheitsschlüssel registrieren"
lastUsed: "Zuletzt benutzt"
unregister: "Deaktivieren"
passwordLessLogin: "Passwortloses Anmelden einrichten"
passwordLessLogin: "Passwortloses Anmelden"
resetPassword: "Passwort zurücksetzen"
newPasswordIs: "Das neue Passwort ist „{password}“"
reduceUiAnimation: "Animationen der Benutzeroberfläche reduzieren"
@ -865,7 +865,7 @@ customCss: "Benutzerdefiniertes CSS"
customCssWarn: "Verwende diese Einstellung nur, wenn du weißt, was sie tut. Ungültige
Eingaben können dazu führen, dass der Client nicht mehr normal funktioniert."
global: "Global"
squareAvatars: "Profilbilder quadratisch anzeigen"
squareAvatars: "Profilbilder für Accounts ohne Katzenohren quadratisch anzeigen"
sent: "Gesendet"
received: "Erhalten"
searchResult: "Suchergebnisse"
@ -948,7 +948,7 @@ check: "Überprüfe"
driveCapOverrideLabel: "Die Cloud-Drive-Kapazität dieses Nutzers verändern"
driveCapOverrideCaption: "Gib einen Wert von 0 oder weniger ein, um die Kapazität
auf den Standard zurückzusetzen."
requireAdminForView: "Du musst dich mit einem Administratorkonto anmelden um dies
requireAdminForView: "Du musst dich mit einem Administratorkonto anmelden, um dies
zu sehen."
isSystemAccount: "Ein Nutzerkonto, dass durch das System erstellt und automatisch
kontrolliert wird. Jede Anpassung, Veränderung oder Löschung dieses Nutzerkontos,
@ -1241,6 +1241,7 @@ _wordMute:
muteLangsDescription2: Sprachcode verwenden, z.B. en, fr, ja, zh..
lang: Sprache
langDescription: Beiträge in der angegebenen Sprache in der Timeline ausblenden.
mutePatterns: Gedämpfte Muster
_instanceMute:
instanceMuteDescription: "Schaltet alle Beiträge/Boosts stumm, die von den gelisteten
Servern stammen, inklusive Antworten von Nutzern an einen Nutzer eines stummgeschalteten
@ -1332,7 +1333,7 @@ _sfx:
channel: "Kanalbenachrichtigung"
_ago:
future: "Zukunft"
justNow: "Gerade eben"
justNow: "gerade eben"
secondsAgo: "vor {n} s"
minutesAgo: "vor {n} min"
hoursAgo: "vor {n} h"
@ -1931,6 +1932,7 @@ _notification:
voted: haben bei deiner Umfrage abgestimmt
reacted: hat auf deinen Beitrag reagiert
renoted: hat deinen Beitrag geteilt
andCountUsers: und {count} mehr Nutzer {acted}
_deck:
alwaysShowMainColumn: "Hauptspalte immer zeigen"
columnAlign: "Spaltenausrichtung"
@ -1979,8 +1981,8 @@ flagSpeakAsCatDescription: Deine Beiträge werden im Katzenmodus nyanisiert
hiddenTags: Versteckte Hashtags
antennaInstancesDescription: Geben sie einen Server-Namen pro Zeile ein
secureModeInfo: Bei Anfragen an andere Server nicht ohne Nachweis zurücksenden.
renoteMute: Boosts stummschalten
renoteUnmute: Stummschaltung von Boosts aufheben
renoteMute: Boosts in Timelines stummschalten
renoteUnmute: Stummschaltung von Boosts in der Timeline aufheben
noInstances: Keine Server gefunden
privateModeInfo: Wenn diese Option aktiviert ist, können nur als vertrauenswürdig
eingestufte Server mit diesem Server föderieren. Alle Beiträge werden für die Öffentlichkeit
@ -2019,9 +2021,8 @@ moveAccountDescription: 'Dieser Vorgang kann nicht rückgängig gemacht werden!
wie folgt ein: @name@server.xyz'
sendPushNotificationReadMessage: Löschung der Push-Benachrichtigungen sobald die entsprechenden
Benachrichtigungen oder Nachrichten gelesen wurden
signupsDisabled: Derzeit sind keine Anmeldungen auf diesem Server möglich! Anmeldungen
auf anderen Servern sind jedoch möglich! Wenn Sie einen Einladungscode für diesen
Server haben, geben Sie ihn bitte unten ein.
signupsDisabled: Derzeit sind keine Anmeldungen auf diesem Server möglich. Wenn Sie
einen Einladungscode für diesen Server haben, geben Sie ihn bitte unten ein.
swipeOnDesktop: Am Desktop PC das Wischen wie bei mobilen Geräten zulassen
enterSendsMessage: Drücken sie zum Senden des Beitrages die Eingabetaste (Strg-Taste
ausgeschaltet)
@ -2212,3 +2213,129 @@ quotes: Zitate
moreUrlsDescription: "Die Seiten, welche angepinnt werde sollen, im Hilfe-Menü in
der unteren linken Ecke in folgender Notation angeben:\n\"Anzeigename\": https://example.com/"
toQuote: Zitat
releaseToReload: Loslassen, um neu zu laden
pullDownToReload: Herunterziehen zum Aktualisieren
antennaLimit: Die maximale Anzahl von Antennen, die jeder Nutzer erstellen kann
toEdit: Bearbeiten
squareCatAvatars: Profilbilder für Accounts mit Katzenohren quadratisch anzeigen
moderationNote: Moderationsnotiz
ipFirstAcknowledged: Das Datum des ersten Erwerbs der IP Adresse
driveCapacityOverride: Benutzerdefinierte Speicherkapazität
searchWordsDescription: "Hier den Suchbegriff für Beiträge eingeben. Mit einem Leerzeichen
getrennte Begriffe werden in einer UND Suche gesucht, um eine ODER Suche auszuführen
'OR' (ohne Anführungszeichen) zwischen die Begriffe schreiben.\nZum Beispiel findet
die Suche nach \"Morgen Nacht\" Beiträge, die sowohl \"Morgen, als auch \"Nacht\"\
\ enthalten. Die Suchanfrage \"Morgen OR Nacht\" findet Beiträge, die entweder \"\
Morgen\" oder \"Nacht\" (oder beides) enthalten.\nDie AND und OR Suche ist zudem
kombinierbar, z.B. so: \"(Morgen OR Nacht) Eule)\".\nUm nach einer Sequenz von Wörtern
(z.B. einem Satz) zu suchen, muss die gesamte Wortsequenz in Anführungszeichen stehen.
Beispiel: \"Nachrichten von heute\"\n\nUm zu einem bestimmten Profil oder Beitrag
zu gelangen, muss die ID oder URL (Webadresse) eingegeben und der Suchknopf gedrückt
werden. Ansonsten wird nach Beiträgen gesucht, die die ID oder URL wörtlich enthalten."
useCdnDescription: Einige statische Ressourcen, wie einen Twemoji, vom JSDelivr CDN
anstatt von diesem Firefish server laden.
suggested: Vorgeschlagen
preventMisclick: Schutz vor versehentlichen Clicks
replaceWidgetsButtonWithReloadButton: Widget-Knopf durch Aktualisierungs-Knopf ersetzen
hideFollowButtons: Folgen-Knopf in einer versehentlich clickbaren Position verstecken
forMobile: Mobil
privateDescription: Nur für Sie sichtbar machen
makePrivate: Als privat markieren
searchUsers: Erstellt von (optional)
searchWords: Suchbegriffe / ID oder URL als Suchanfrage
searchCwAndAlt: Inhaltswarnungen und Beschreibungen von Dateien einbeziehen
searchUsersDescription: "Um nach Beiträgen eines bestimmten Nutzers/ Servers zu suchen,
einfach die ID (@Benutzer@beispiel.de, or @Benutzer für einen lokalen Benutzer)
oder Webadresse (beispiel.de) eingeben.\n\nDie Suche \"me\" (ohne Anführungszeichen)
findet alle Ihre Beiträge (auch nicht-gelistete, direkte, geheime Beiträge und Beiträge,
die nur für Follower sichtbar sind).\n\nDie Suche \"local\" (ohne Anführungszeichen)
sorgt dafür, dass nur Beiträge von diesem Server angezeigt werden."
publishTimelines: Timelines für Besucher veröffentlichen
publishTimelinesDescription: Falls konfiguriert, werden die lokale und globale Timeline
auf {url} auch ohne Anmeldung angezeigt.
showNoAltTextWarning: Eine Warnung beim Hochladen von Dateien ohne Beschreibung anzeigen
_emojiModPerm:
add: Hinzufügen
full: Alles erlauben
unauthorized: Kein(e)
mod: Hinzufügen und bearbeiten
messagingUnencryptedInfo: Unterhaltungen auf Firefish sind nicht Ende-zu-Ende verschlüsselt.
Teilen Sie keine sensiblen Informationen über Firefish.
autocorrectNoteLanguage: Eine Warnung anzeigen, wenn die Beitragssprache nicht mit
der automatisch ermittelten Sprache übereinstimmt
emojiModPerm: Berechtigung, personalisierte Emojis zu verwalten
emojiModPermDescription: "Hinzufügen: Erlauben Sie diesem Benutzer, neue benutzerdefinierte
Emojis hinzuzufügen und Tag/Kategorie/Lizenz für neu hinzugefügte benutzerdefinierte
Emojis einzustellen.\nHinzufügen und Bearbeiten: \"Hinzufügen\" Berechtigung + Erlauben
Sie diesem Benutzer, den Namen/die Kategorie/Tag/die Lizenz der vorhandenen benutzerdefinierten
Emojis zu bearbeiten.\nAlles erlauben: \"Hinzufügen und Bearbeiten\" Berechtigung
+ Erlauben Sie diesem Benutzer, bestehende benutzerdefinierte Emojis zu löschen."
reloading: Aktualisiert
markLocalFilesNsfwByDefault: Standardmäßig alle neuen lokalen Dateien als sensibel
markieren
markLocalFilesNsfwByDefaultDescription: Unabhäning von dieser Einstellung lässt sich
eine NSFW-Markierung entfernen. Bereits existierende Dateien sind nicht betroffen.
noLanguage: Keine Sprache
showBigPostButton: Anzeigen eines großen Knopfes zum Teilen des Beitrags im Beitragsformular
private: Privat
searchRange: Veröffentlicht zwischen (optional)
searchPostsWithFiles: Nur Beiträge mit Dateien
noAltTextWarning: Einige der angehängten Dateien haben keine Beschreibung. Haben Sie
vergessen, diese zu schreiben?
toReply: Antworten
toPost: Beitrag teilen
sentFollowRequests: Gesendete Follow-Anfragen
replyMute: Antworten in Timelines stummschalten
replyUnmute: Stummschaltung von Antworten in Timelines aufheben
noSentFollowRequests: Keine gesendeten Follow-Anfragen
postSearch: Beitragssuche auf diesem Server
enablePullToRefresh: '"Herunterziehen um zu aktualisieren" aktivieren'
pullToRefreshThreshold: Benötigte heruntergezogene Distanz, um zu Aktualisieren
showAddFileDescriptionAtFirstPost: Öffne automatisch ein Eingabefeld, um fehlende
Dateibeschreibungen beim Hochladen zu ergänßen
searchRangeDescription: "Um eine Zeitspanne zu filtern, geben Sie diese in diesem
Format an: 20220615-20231031 (YYYYMMTT)\n\nDas Auslassen der Jahreszahl (z.B.: 0615-1031
oder 20220615-1031) wird automatisch wie die aktuelle Jahreszahl interpretiert.\n
\nZudem können das Anfangs- oder Enddatum ausgelassen werden. Zum Beispiel gibt
-1031 an, nach Beiträgen vor dem 31.10 dieses Jahres zu suchen. Umgekehrt führt
20220615- zu einer Suche nach allen Beiträgen nach dem 15.6.2022."
incorrectLanguageWarning: "Es sieht so aus, als wäre ihr Beitrag auf {detected}, aber
Sie haben {current} ausgewählt.\nMöchten Sie stattdessen die Sprache zu {detected}
ändern?"
noteEditHistory: Bearbeitungsgeschichte des Beitrags
_later:
justNow: gerade eben
secondsAgo: in {n}s
minutesAgo: in {n}min
hoursAgo: in {n}h
daysAgo: in {n}d
weeksAgo: in {n} Woche(n)
monthsAgo: in {n} Monat(en)
yearsAgo: in {n} Jahr(en)
future: zukünftig
scheduledPost: Veröffentlichungszeit manuell festlegen
scheduledDate: Geplantes Datum
mergeRenotesInTimeline: Mehrere Boosts eines Beitrags gruppieren
mergeThreadInTimeline: In der Timeline mehrere Beiträge im gleichen Thread zusammenlegen
cannotEditVisibility: Die Sichtbarkeit lässt sich nicht einstellen
useThisAccountConfirm: Mit diesem Benutzerkonto fortfahren?
inputAccountId: Bitte gib dein Benutzerkonto an (z.B. @firefish@info.firefish.dev)
remoteFollow: Folgen (fremde Instanz)
foldNotification: Ähnliche Benachrichtigungen gruppieren
i18nServerInfo: Neue Clients nutzen standardmäßig {language}.
i18nServerChange: Stattdessen {language} benutzen.
i18nServerSet: Für neue Clients {language} benutzen.
getQrCode: QR Code anzeigen
useCdn: Ressourcen von einem CDN laden
copyRemoteFollowUrl: URL zum Folgen auf einer fremden Instanz kopieren
showPreviewByDefault: Standardmäßig Vorschau in Beitragsform anzeigen
replaceChatButtonWithAccountButton: Unterhaltungen-Knopf durch Knopf zum Wechseln
des Benutzerkontos ersetzen
searchEngine: Verwendete Suchmaschine in der Suchleiste MFM
makePrivateConfirm: Diese Operation sendet eine Löschungsanfrage an fremde Server
und ändert die Sichtbarkeit zu 'privat'. Fortfahren?
enableTimelineStreaming: Timelines automatisch aktualisieren
scheduledPostAt: Der Beitrag wird {time} gesendet
cancelScheduledPost: Zeitplan entfernen
media: Medien
slashQuote: Kettenzitat

View file

@ -1236,8 +1236,8 @@ publishTimelinesDescription: "If enabled, the Local and Global timelines will be
on {url} even when signed out."
noAltTextWarning: "Some attached file(s) have no description. Did you forget to write?"
showNoAltTextWarning: "Show a warning if you attempt to post files without a description"
showAddFileDescriptionAtFirstPost: "Automatically open a form to write a description when you
attempt to post files without a description"
showAddFileDescriptionAtFirstPost: "Automatically open a form to write a description
when you attempt to post files without a description"
_emojiModPerm:
unauthorized: "None"

View file

@ -1073,7 +1073,7 @@ _aboutFirefish:
source: "Código fuente"
translation: "Traducir Firefish"
donate: "Donar a Firefish"
pleaseDonateToFirefish: Por favor considera donar a Firefish para apollar su desarrollo.
pleaseDonateToFirefish: Por favor considera donar a Firefish para apoyar su desarrollo.
donateHost: Dona a {host}
donateTitle: ¿Te gusta Firefish?
pleaseDonateToHost: También considera donar a tu propio servidor , {host}, para

View file

@ -244,3 +244,11 @@ enableEmojiReactions: Ativar reações com emoji
attachCancel: Remover anexo
flagShowTimelineReplies: Mostrar respostas na linha do tempo
addAccount: Adicionar conta
toReply: Responder
sentFollowRequests: Enviou solicitações para seguir
toPost: Postar
renotes: Impulsionamentos
renote: Impulsionar
unrenote: Retirar o impulsionamento
renoted: Impulsionado.
replies: Respostas

View file

@ -1950,7 +1950,7 @@ noteId: 帖子 ID
moveFrom: 从旧账号迁移至此账号
defaultReaction: 发出和收到帖子的默认表情符号反应
sendModMail: 发送管理通知
moderationNote: "管理笔记"
moderationNote: "管理员备注"
ipFirstAcknowledged: "首次获取此 IP 地址的日期"
driveCapacityOverride: "网盘容量变更"
isLocked: 该账号设置了关注请求

View file

@ -1,11 +1,11 @@
{
"name": "firefish",
"version": "20240523",
"version": "20240601",
"repository": {
"type": "git",
"url": "https://firefish.dev/firefish/firefish.git"
},
"packageManager": "pnpm@9.1.2",
"packageManager": "pnpm@9.2.0",
"private": true,
"scripts": {
"rebuild": "pnpm run clean && pnpm run build",
@ -41,14 +41,14 @@
"js-yaml": "4.1.0"
},
"devDependencies": {
"@biomejs/biome": "1.7.3",
"@biomejs/cli-darwin-arm64": "1.7.3",
"@biomejs/cli-darwin-x64": "1.7.3",
"@biomejs/cli-linux-arm64": "1.7.3",
"@biomejs/cli-linux-x64": "1.7.3",
"@types/node": "20.12.12",
"@biomejs/biome": "1.8.0",
"@biomejs/cli-darwin-arm64": "1.8.0",
"@biomejs/cli-darwin-x64": "1.8.0",
"@biomejs/cli-linux-arm64": "1.8.0",
"@biomejs/cli-linux-x64": "1.8.0",
"@types/node": "20.14.2",
"execa": "9.1.0",
"pnpm": "9.1.2",
"pnpm": "9.2.0",
"typescript": "5.4.5"
}
}

View file

@ -1,3 +0,0 @@
[*.rs]
indent_style = space
indent_size = 4

View file

@ -14,43 +14,43 @@ crate-type = ["cdylib", "lib"]
[dependencies]
macro-rs = { workspace = true }
napi = { workspace = true, optional = true, default-features = false, features = ["napi9", "tokio_rt", "chrono_date", "serde-json"] }
napi = { workspace = true, optional = true, features = ["chrono_date", "napi4", "serde-json", "tokio_rt"] }
napi-derive = { workspace = true, optional = true }
argon2 = { workspace = true, features = ["std"] }
async-trait = { workspace = true }
basen = { workspace = true }
bb8 = { workspace = true }
bcrypt = { workspace = true }
bcrypt = { workspace = true, features = ["std"] }
chrono = { workspace = true }
cuid2 = { workspace = true }
emojis = { workspace = true }
idna = { workspace = true }
image = { workspace = true }
isahc = { workspace = true }
image = { workspace = true, features = ["avif", "bmp", "gif", "ico", "jpeg", "png", "tiff", "webp"] }
isahc = { workspace = true, features = ["http2", "text-decoding"] }
nom-exif = { workspace = true }
once_cell = { workspace = true }
openssl = { workspace = true, features = ["vendored"] }
rand = { workspace = true }
redis = { workspace = true, default-features = false, features = ["streams", "tokio-comp"] }
redis = { workspace = true, features = ["streams", "tokio-comp"] }
regex = { workspace = true }
rmp-serde = { workspace = true }
sea-orm = { workspace = true, features = ["sqlx-postgres", "runtime-tokio-rustls"] }
sea-orm = { workspace = true, features = ["macros", "runtime-tokio-rustls", "sqlx-postgres", "with-chrono", "with-json"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
serde_yaml = { workspace = true }
strum = { workspace = true, features = ["derive"] }
sysinfo = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["full"] }
tokio = { workspace = true, features = ["fs", "io-std", "io-util", "macros", "process", "rt-multi-thread", "signal", "sync", "time"] }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
tracing-subscriber = { workspace = true, features = ["ansi"] }
url = { workspace = true }
urlencoding = { workspace = true }
web-push = { workspace = true }
web-push = { workspace = true, features = ["isahc-client"] }
[dev-dependencies]
pretty_assertions = { workspace = true }
pretty_assertions = { workspace = true, features = ["std"] }
tokio-test = { workspace = true }
[build-dependencies]

View file

@ -11,8 +11,9 @@ export const USER_ONLINE_THRESHOLD: number
export const USER_ACTIVE_THRESHOLD: number
/**
* List of file types allowed to be viewed directly in the browser
*
* Anything not included here will be responded as application/octet-stream
* SVG is not allowed because it generates XSS <- we need to fix this and later allow it to be viewed directly
* SVG is not allowed because it generates XSS (TODO: fix this and later allow it to be viewed directly)
* * <https://github.com/sindresorhus/file-type/blob/main/supported.js>
* * <https://github.com/sindresorhus/file-type/blob/main/core.js>
* * <https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers>
@ -28,6 +29,20 @@ export interface EnvConfig {
slow: boolean
}
export function loadEnv(): EnvConfig
export function fetchMeta(): Promise<Meta>
export function updateMetaCache(): Promise<void>
export interface PugArgs {
img: string | null
title: string
instanceName: string
desc: string | null
icon: string | null
splashIcon: string | null
themeColor: string | null
randomMotd: string
privateMode: boolean | null
}
export function metaToPugArgs(meta: Meta): PugArgs
export interface ServerConfig {
url: string
port: number
@ -207,48 +222,206 @@ export interface Acct {
}
export function stringToAcct(acct: string): Acct
export function acctToString(acct: Acct): string
/** Fetches and returns the NodeInfo (version 2.0) of a remote server. */
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
}
/** Prints the greeting message and the Firefish version to stdout. */
export function greet(): void
/** Initializes the [tracing] logger. */
export function initializeRustLogger(): void
/** Prints the server hardware information as the server info log. */
export function showServerInfo(): void
/**
* Checks if a server is blocked.
*
* ## Argument
* # Argument
* `host` - punycoded instance host
*
* # Example
* ```no_run
* # use backend_rs::misc::check_server_block::is_blocked_server;
* # async fn f() -> Result<(), Box<dyn std::error::Error>> {
* assert_eq!(true, is_blocked_server("blocked.com").await?);
* assert_eq!(false, is_blocked_server("not-blocked.com").await?);
* assert_eq!(true, is_blocked_server("subdomain.of.blocked.com").await?);
* assert_eq!(true, is_blocked_server("xn--l8jegik.blocked.com").await?);
* # Ok(())
* # }
* ```
*/
export function isBlockedServer(host: string): Promise<boolean>
/**
* Checks if a server is silenced.
*
* ## Argument
* # Argument
* `host` - punycoded instance host
*
* # Example
* ```no_run
* # use backend_rs::misc::check_server_block::is_silenced_server;
* # async fn f() -> Result<(), Box<dyn std::error::Error>> {
* assert_eq!(true, is_silenced_server("silenced.com").await?);
* assert_eq!(false, is_silenced_server("not-silenced.com").await?);
* assert_eq!(true, is_silenced_server("subdomain.of.silenced.com").await?);
* assert_eq!(true, is_silenced_server("xn--l8jegik.silenced.com").await?);
* # Ok(())
* # }
* ```
*/
export function isSilencedServer(host: string): Promise<boolean>
/**
* Checks if a server is allowlisted.
* Returns `Ok(true)` if private mode is disabled.
*
* ## Argument
* # Argument
* `host` - punycoded instance host
*
* # Example
* ```no_run
* # use backend_rs::misc::check_server_block::is_allowed_server;
* # async fn f() -> Result<(), Box<dyn std::error::Error>> {
* assert_eq!(true, is_allowed_server("allowed.com").await?);
* assert_eq!(false, is_allowed_server("not-allowed.com").await?);
* assert_eq!(false, is_allowed_server("subdomain.of.allowed.com").await?);
* assert_eq!(false, is_allowed_server("xn--l8jegik.allowed.com").await?);
* # Ok(())
* # }
* ```
*/
export function isAllowedServer(host: string): Promise<boolean>
export function checkWordMute(note: NoteLike, mutedWords: Array<string>, mutedPatterns: Array<string>): Promise<boolean>
/**
* Returns whether `note` should be hard-muted.
*
* More specifically, this function returns `Ok(true)`
* if and only if one or more of these conditions are met:
*
* * the note (text or CW) contains any of the words/patterns
* * the "parent" note(s) (reply, quote) contain any of the words/patterns
* * the alt text of the attached files contains any of the words/patterns
*
* # Arguments
*
* * `note` : [PartialNoteToElaborate] object
* * `muted_words` : list of muted keyword lists (each array item is a space-separated keyword list that represents an AND condition)
* * `muted_patterns` : list of JavaScript-style (e.g., `/foo/i`) regular expressions
*/
export function checkWordMute(note: PartialNoteToElaborate, mutedWords: Array<string>, mutedPatterns: Array<string>): Promise<boolean>
export function getFullApAccount(username: string, host?: string | undefined | null): string
export function isSelfHost(host?: string | undefined | null): boolean
export function isSameOrigin(uri: string): boolean
export function extractHost(uri: string): string
export function toPuny(host: string): string
export function isUnicodeEmoji(s: string): boolean
/** Escapes `%` and `\` in the given string. */
export function sqlLikeEscape(src: string): string
/** Returns `true` if `src` does not contain suspicious characters like `%`. */
export function safeForSql(src: string): boolean
/** Convert milliseconds to a human readable string */
/** Converts milliseconds to a human readable string. */
export function formatMilliseconds(milliseconds: number): string
export interface ImageSize {
width: number
height: number
}
export function getImageSizeFromUrl(url: string): Promise<ImageSize>
export interface NoteLikeForAllTexts {
export interface PartialNoteToElaborate {
fileIds: Array<string>
userId: string
text: string | null
@ -256,34 +429,44 @@ export interface NoteLikeForAllTexts {
renoteId: string | null
replyId: string | null
}
export interface NoteLikeForGetNoteSummary {
export interface PartialNoteToSummarize {
fileIds: Array<string>
text: string | null
cw: string | null
hasPoll: boolean
}
export function getNoteSummary(note: NoteLikeForGetNoteSummary): string
export function getNoteSummary(note: PartialNoteToSummarize): string
export function isQuote(note: Note): boolean
export function isSafeUrl(url: string): boolean
/** Returns the latest Firefish version. */
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>
export interface PugArgs {
img: string | null
title: string
instanceName: string
desc: string | null
icon: string | null
splashIcon: string | null
themeColor: string | null
randomMotd: string
privateMode: boolean | null
}
export function metaToPugArgs(meta: Meta): PugArgs
/**
* Converts the given text into the cat language.
*
* refs:
* * <https://misskey-hub.net/ns/#isCat>
* * <https://firefish.dev/ns#speakAsCat>
*
* # Arguments
*
* * `text` : original text
* * `lang` : language code (e.g., `Some("en")`, `Some("en-US")`, `Some("uk-UA")`, `None`)
*
* # Example
*
* ```
* # use backend_rs::misc::nyaify::nyaify;
* assert_eq!(nyaify("I'll take a nap.", Some("en")), "I'll take a nyap.");
* ```
*/
export function nyaify(text: string, lang?: string | undefined | null): string
/** Hashes the given password using [Argon2] algorithm. */
export function hashPassword(password: string): string
/** Checks whether the given password and hash match. */
export function verifyPassword(password: string, hash: string): boolean
/** Returns whether the [bcrypt] algorithm is used for the password hash. */
export function isOldPasswordAlgorithm(hash: string): boolean
export interface DecodedReaction {
reaction: string
@ -293,7 +476,7 @@ export interface DecodedReaction {
export function decodeReaction(reaction: string): DecodedReaction
export function countReactions(reactions: Record<string, number>): Record<string, number>
export function toDbReaction(reaction?: string | undefined | null, host?: string | undefined | null): Promise<string>
/** Delete all entries in the "attestation_challenge" table created at more than 5 minutes ago */
/** Delete all entries in the [attestation_challenge] table created at more than 5 minutes ago */
export function removeOldAttestationChallenges(): Promise<void>
export interface Cpu {
model: string
@ -1174,106 +1357,6 @@ export interface Webhook {
latestStatus: number | null
}
export function updateAntennasOnNewNote(note: Note, noteAuthor: Acct, noteMutedUsers: Array<string>): Promise<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 enum PushNotificationKind {
@ -1331,6 +1414,6 @@ export function getTimestamp(id: string): number
export function genId(): string
/** Generate an ID using a specific datetime */
export function genIdAt(date: Date): string
/** Generate random string based on [thread_rng] and [Alphanumeric]. */
/** Generates a random string based on [thread_rng] and [Alphanumeric]. */
export function generateSecureRandomString(length: number): string
export function generateUserToken(): string

View file

@ -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, greet, initializeRustLogger, showServerInfo, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, isQuote, isSafeUrl, latestVersion, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, cpuInfo, cpuUsage, memoryUsage, storageUsage, AntennaSrc, DriveFileUsageHint, MutedNoteReason, NoteVisibility, NotificationType, PageVisibility, PollNoteVisibility, RelayStatus, UserEmojiModPerm, UserProfileFfvisibility, UserProfileMutingNotificationTypes, updateAntennasOnNewNote, fetchNodeinfo, nodeinfo_2_1, nodeinfo_2_0, Protocol, Inbound, Outbound, watchNote, unwatchNote, PushNotificationKind, sendPushNotification, publishToChannelStream, ChatEvent, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, getTimestamp, genId, genIdAt, generateSecureRandomString, generateUserToken } = nativeBinding
const { SECOND, MINUTE, HOUR, DAY, USER_ONLINE_THRESHOLD, USER_ACTIVE_THRESHOLD, FILE_TYPE_BROWSERSAFE, loadEnv, fetchMeta, updateMetaCache, metaToPugArgs, loadConfig, stringToAcct, acctToString, fetchNodeinfo, nodeinfo_2_1, nodeinfo_2_0, Protocol, Inbound, Outbound, greet, initializeRustLogger, showServerInfo, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, isQuote, isSafeUrl, latestVersion, toMastodonId, fromMastodonId, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, cpuInfo, cpuUsage, memoryUsage, storageUsage, AntennaSrc, DriveFileUsageHint, MutedNoteReason, NoteVisibility, NotificationType, PageVisibility, PollNoteVisibility, RelayStatus, UserEmojiModPerm, UserProfileFfvisibility, UserProfileMutingNotificationTypes, updateAntennasOnNewNote, watchNote, unwatchNote, PushNotificationKind, sendPushNotification, publishToChannelStream, ChatEvent, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, getTimestamp, genId, genIdAt, generateSecureRandomString, generateUserToken } = nativeBinding
module.exports.SECOND = SECOND
module.exports.MINUTE = MINUTE
@ -320,9 +320,18 @@ module.exports.USER_ONLINE_THRESHOLD = USER_ONLINE_THRESHOLD
module.exports.USER_ACTIVE_THRESHOLD = USER_ACTIVE_THRESHOLD
module.exports.FILE_TYPE_BROWSERSAFE = FILE_TYPE_BROWSERSAFE
module.exports.loadEnv = loadEnv
module.exports.fetchMeta = fetchMeta
module.exports.updateMetaCache = updateMetaCache
module.exports.metaToPugArgs = metaToPugArgs
module.exports.loadConfig = loadConfig
module.exports.stringToAcct = stringToAcct
module.exports.acctToString = acctToString
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.greet = greet
module.exports.initializeRustLogger = initializeRustLogger
module.exports.showServerInfo = showServerInfo
@ -346,8 +355,6 @@ module.exports.isSafeUrl = isSafeUrl
module.exports.latestVersion = latestVersion
module.exports.toMastodonId = toMastodonId
module.exports.fromMastodonId = fromMastodonId
module.exports.fetchMeta = fetchMeta
module.exports.metaToPugArgs = metaToPugArgs
module.exports.nyaify = nyaify
module.exports.hashPassword = hashPassword
module.exports.verifyPassword = verifyPassword
@ -372,12 +379,6 @@ module.exports.UserEmojiModPerm = UserEmojiModPerm
module.exports.UserProfileFfvisibility = UserProfileFfvisibility
module.exports.UserProfileMutingNotificationTypes = UserProfileMutingNotificationTypes
module.exports.updateAntennasOnNewNote = updateAntennasOnNewNote
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.PushNotificationKind = PushNotificationKind

View file

@ -1,24 +1,27 @@
#[crate::export]
//! This module is used in the TypeScript backend only.
#[crate::ts_export]
pub const SECOND: i32 = 1000;
#[crate::export]
#[crate::ts_export]
pub const MINUTE: i32 = 60 * SECOND;
#[crate::export]
#[crate::ts_export]
pub const HOUR: i32 = 60 * MINUTE;
#[crate::export]
#[crate::ts_export]
pub const DAY: i32 = 24 * HOUR;
#[crate::export]
#[crate::ts_export]
pub const USER_ONLINE_THRESHOLD: i32 = 10 * MINUTE;
#[crate::export]
#[crate::ts_export]
pub const USER_ACTIVE_THRESHOLD: i32 = 3 * DAY;
/// List of file types allowed to be viewed directly in the browser
///
/// Anything not included here will be responded as application/octet-stream
/// SVG is not allowed because it generates XSS <- we need to fix this and later allow it to be viewed directly
/// SVG is not allowed because it generates XSS (TODO: fix this and later allow it to be viewed directly)
/// * <https://github.com/sindresorhus/file-type/blob/main/supported.js>
/// * <https://github.com/sindresorhus/file-type/blob/main/core.js>
/// * <https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers>
#[crate::export]
#[crate::ts_export]
pub const FILE_TYPE_BROWSERSAFE: [&str; 41] = [
// Images
"image/png",

View file

@ -1,3 +1,5 @@
//! Environment options
// FIXME: Are these options used?
#[crate::export(object)]
pub struct EnvConfig {

View file

@ -1,18 +1,29 @@
//! Server information
use crate::database::db_conn;
use crate::model::entity::meta;
use rand::prelude::*;
use sea_orm::{prelude::*, ActiveValue};
use std::sync::Mutex;
type Meta = meta::Model;
static CACHE: Mutex<Option<Meta>> = Mutex::new(None);
fn update_cache(meta: &Meta) {
fn set_cache(meta: &Meta) {
let _ = CACHE.lock().map(|mut cache| *cache = Some(meta.clone()));
}
#[crate::export]
pub async fn fetch_meta(use_cache: bool) -> Result<Meta, DbErr> {
#[crate::export(js_name = "fetchMeta")]
pub async fn local_server_info() -> Result<Meta, DbErr> {
local_server_info_impl(true).await
}
#[crate::export(js_name = "updateMetaCache")]
pub async fn update() -> Result<(), DbErr> {
local_server_info_impl(false).await?;
Ok(())
}
async fn local_server_info_impl(use_cache: bool) -> Result<Meta, DbErr> {
// try using cache
if use_cache {
if let Some(cache) = CACHE.lock().ok().and_then(|cache| cache.clone()) {
@ -24,7 +35,7 @@ pub async fn fetch_meta(use_cache: bool) -> Result<Meta, DbErr> {
let db = db_conn().await?;
let meta = meta::Entity::find().one(db).await?;
if let Some(meta) = meta {
update_cache(&meta);
set_cache(&meta);
return Ok(meta);
}
@ -35,7 +46,7 @@ pub async fn fetch_meta(use_cache: bool) -> Result<Meta, DbErr> {
})
.exec_with_returning(db)
.await?;
update_cache(&meta);
set_cache(&meta);
Ok(meta)
}
@ -52,8 +63,9 @@ pub struct PugArgs {
pub private_mode: Option<bool>,
}
#[crate::export]
#[crate::ts_export]
pub fn meta_to_pug_args(meta: Meta) -> PugArgs {
use rand::prelude::*;
let mut rng = rand::thread_rng();
let splash_icon = meta

View file

@ -1,5 +1,9 @@
//! Server configurations and environment variables
pub use meta::local_server_info;
pub use server::CONFIG;
pub mod constant;
pub mod environment;
pub mod meta;
pub mod server;

View file

@ -1,3 +1,5 @@
//! Server configuration
use once_cell::sync::Lazy;
use serde::Deserialize;
use std::env;

View file

@ -1,3 +1,5 @@
//! Utilities for using Redis cache
use crate::database::{redis_conn, redis_key, RedisConnError};
use redis::{AsyncCommands, RedisError};
use serde::{Deserialize, Serialize};
@ -21,10 +23,8 @@ pub enum Error {
Redis(#[from] RedisError),
#[error("Redis connection error: {0}")]
RedisConn(#[from] RedisConnError),
#[error("Data serialization error: {0}")]
Serialize(#[from] rmp_serde::encode::Error),
#[error("Data deserialization error: {0}")]
Deserialize(#[from] rmp_serde::decode::Error),
#[error("Failed to encode the data: {0}")]
Encode(#[from] rmp_serde::encode::Error),
}
#[inline]
@ -46,27 +46,30 @@ fn wildcard(category: Category) -> String {
///
/// This overwrites the exsisting cache with the same key.
///
/// ## Arguments
/// # Arguments
///
/// * `key` - key (will be prefixed automatically)
/// * `value` - (de)serializable value
/// * `expire_seconds` - TTL
/// * `key` : key (prefixed automatically)
/// * `value` : (de)serializable value
/// * `expire_seconds` : TTL
///
/// ## Example
/// # Example
///
/// ```
/// # use backend_rs::database::cache;
/// # tokio_test::block_on(async {
/// # async fn f() -> Result<(), Box<dyn std::error::Error>> {
/// let key = "apple";
/// let data = "I want to cache this string".to_string();
///
/// // caches the data for 10 seconds
/// cache::set(key, &data, 10).await;
/// cache::set(key, &data, 10).await?;
///
/// // get the cache
/// let cached_data = cache::get::<String>(key).await.unwrap();
/// let cached_data = cache::get::<String>(key).await?;
///
/// assert!(cached_data.is_some());
/// assert_eq!(data, cached_data.unwrap());
/// # })
/// # Ok(())
/// # }
/// ```
pub async fn set<V: for<'a> Deserialize<'a> + Serialize>(
key: &str,
@ -89,34 +92,36 @@ pub async fn set<V: for<'a> Deserialize<'a> + Serialize>(
/// 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
/// # Argument
///
/// * `key` - key (will be prefixed automatically)
/// * `key` : key (will be prefixed automatically)
///
/// ## Example
/// # Example
///
/// ```
/// # use backend_rs::database::cache;
/// # tokio_test::block_on(async {
/// # async fn f() -> Result<(), Box<dyn std::error::Error>> {
/// let key = "banana";
/// let data = "I want to cache this string".to_string();
///
/// // set cache
/// cache::set(key, &data, 10).await.unwrap();
/// cache::set(key, &data, 10).await?;
///
/// // get cache
/// let cached_data = cache::get::<String>(key).await.unwrap();
/// let cached_data = cache::get::<String>(key).await?;
/// assert!(cached_data.is_some());
/// assert_eq!(data, cached_data.unwrap());
///
/// // get nonexistent (or expired) cache
/// let no_cache = cache::get::<String>("nonexistent").await.unwrap();
/// let no_cache = cache::get::<String>("nonexistent").await?;
/// assert!(no_cache.is_none());
/// # })
/// # Ok(())
/// # }
/// ```
pub async fn get<V: for<'a> Deserialize<'a> + Serialize>(key: &str) -> Result<Option<V>, Error> {
let serialized_value: Option<Vec<u8>> = redis_conn().await?.get(prefix_key(key)).await?;
Ok(match serialized_value {
Some(v) => Some(rmp_serde::from_slice::<V>(v.as_ref())?),
Some(v) => rmp_serde::from_slice::<V>(v.as_ref()).ok(),
None => None,
})
}
@ -126,29 +131,30 @@ pub async fn get<V: for<'a> Deserialize<'a> + Serialize>(key: &str) -> Result<Op
/// If the Redis connection is fine, this returns `Ok(())`
/// regardless of whether the cache exists.
///
/// ## Arguments
/// # Argument
///
/// * `key` - key (will be prefixed automatically)
/// * `key` : key (prefixed automatically)
///
/// ## Example
/// # Example
///
/// ```
/// # use backend_rs::database::cache;
/// # tokio_test::block_on(async {
/// # async fn f() -> Result<(), Box<dyn std::error::Error>> {
/// let key = "chocolate";
/// let value = "I want to cache this string".to_string();
///
/// // set cache
/// cache::set(key, &value, 10).await.unwrap();
/// cache::set(key, &value, 10).await?;
///
/// // delete the cache
/// cache::delete("foo").await.unwrap();
/// cache::delete("nonexistent").await.unwrap(); // this is okay
/// cache::delete("foo").await?;
/// cache::delete("nonexistent").await?; // this is okay
///
/// // the cache is gone
/// let cached_value = cache::get::<String>("foo").await.unwrap();
/// let cached_value = cache::get::<String>("foo").await?;
/// assert!(cached_value.is_none());
/// # })
/// # Ok(())
/// # }
/// ```
pub async fn delete(key: &str) -> Result<(), Error> {
Ok(redis_conn().await?.del(prefix_key(key)).await?)
@ -159,12 +165,12 @@ pub async fn delete(key: &str) -> Result<(), Error> {
/// The usage is the same as [set], except that you need to
/// use [get_one] and [delete_one] to get/delete the cache.
///
/// ## Arguments
/// # Arguments
///
/// * `category` - one of [Category]
/// * `key` - key (will be prefixed automatically)
/// * `value` - (de)serializable value
/// * `expire_seconds` - TTL
/// * `category` : one of [Category]
/// * `key` : key (prefixed automatically)
/// * `value` : (de)serializable value
/// * `expire_seconds` : TTL
pub async fn set_one<V: for<'a> Deserialize<'a> + Serialize>(
category: Category,
key: &str,
@ -178,10 +184,10 @@ pub async fn set_one<V: for<'a> Deserialize<'a> + Serialize>(
///
/// The usage is basically the same as [get].
///
/// ## Arguments
/// # Arguments
///
/// * `category` - one of [Category]
/// * `key` - key (will be prefixed automatically)
/// * `category` : one of [Category]
/// * `key` : key (prefixed automatically)
pub async fn get_one<V: for<'a> Deserialize<'a> + Serialize>(
category: Category,
key: &str,
@ -193,19 +199,19 @@ pub async fn get_one<V: for<'a> Deserialize<'a> + Serialize>(
///
/// The usage is basically the same as [delete].
///
/// ## Arguments
/// # Arguments
///
/// * `category` - one of [Category]
/// * `key` - key (will be prefixed automatically)
/// - `category` : one of [Category]
/// - `key` : key (prefixed automatically)
pub async fn delete_one(category: Category, key: &str) -> Result<(), Error> {
delete(&categorize(category, key)).await
}
/// Deletes all Redis caches under a `category`.
///
/// ## Arguments
/// # Argument
///
/// * `category` - one of [Category]
/// * `category` : one of [Category]
pub async fn delete_all(category: Category) -> Result<(), Error> {
let mut redis = redis_conn().await?;
let keys: Vec<Vec<u8>> = redis.keys(wildcard(category)).await?;

View file

@ -1,3 +1,5 @@
//! Interfaces for accessing PostgreSQL and Redis
pub use postgresql::db_conn;
pub use redis::key as redis_key;
pub use redis::redis_conn;

View file

@ -1,11 +1,14 @@
//! PostgreSQL interface
use crate::config::CONFIG;
use once_cell::sync::OnceCell;
use sea_orm::{ConnectOptions, Database, DbConn, DbErr};
use std::time::Duration;
use tracing::log::LevelFilter;
static DB_CONN: OnceCell<DbConn> = OnceCell::new();
async fn init_database() -> Result<&'static DbConn, DbErr> {
async fn init_conn() -> Result<&'static DbConn, DbErr> {
let database_uri = format!(
"postgres://{}:{}@{}:{}/{}",
CONFIG.db.user,
@ -16,6 +19,7 @@ async fn init_database() -> Result<&'static DbConn, DbErr> {
);
let option: ConnectOptions = ConnectOptions::new(database_uri)
.sqlx_logging_level(LevelFilter::Trace)
.sqlx_slow_statements_logging_settings(LevelFilter::Warn, Duration::from_secs(3))
.to_owned();
tracing::info!("initializing connection");
@ -24,10 +28,11 @@ async fn init_database() -> Result<&'static DbConn, DbErr> {
Ok(DB_CONN.get_or_init(move || conn))
}
/// Returns an async PostgreSQL connection that can be used with [sea_orm] utilities.
pub async fn db_conn() -> Result<&'static DbConn, DbErr> {
match DB_CONN.get() {
Some(conn) => Ok(conn),
None => init_database().await,
None => init_conn().await,
}
}

View file

@ -1,18 +1,20 @@
//! Redis interface
use crate::config::CONFIG;
use async_trait::async_trait;
use bb8::{ManageConnection, Pool, PooledConnection, RunError};
use redis::{aio::MultiplexedConnection, Client, ErrorKind, IntoConnectionInfo, RedisError};
use tokio::sync::OnceCell;
/// A `bb8::ManageConnection` for `redis::Client::get_multiplexed_async_connection`.
/// A [bb8::ManageConnection] for [redis::Client::get_multiplexed_async_connection].
#[derive(Clone, Debug)]
pub struct RedisConnectionManager {
client: Client,
}
impl RedisConnectionManager {
/// Create a new `RedisConnectionManager`.
/// See `redis::Client::open` for a description of the parameter types.
/// Creates a new [RedisConnectionManager].
/// See [redis::Client::open] for a description of the parameter types.
pub fn new<T: IntoConnectionInfo>(info: T) -> Result<Self, RedisError> {
Ok(Self {
client: Client::open(info.into_connection_info()?)?,
@ -85,6 +87,7 @@ pub enum RedisConnError {
Bb8Pool(RunError<RedisError>),
}
/// Returns an async [redis] connection managed by a [bb8] connection pool.
pub async fn redis_conn(
) -> Result<PooledConnection<'static, RedisConnectionManager>, RedisConnError> {
if !CONN_POOL.initialized() {
@ -103,7 +106,7 @@ pub async fn redis_conn(
.map_err(RedisConnError::Bb8Pool)
}
/// prefix redis key
/// prefix Redis key
#[inline]
pub fn key(key: impl ToString) -> String {
format!("{}:{}", CONFIG.redis_key_prefix, key.to_string())

View file

@ -1 +1,4 @@
//! Services used to federate with other servers
pub mod acct;
pub mod nodeinfo;

View file

@ -1,8 +1,13 @@
use crate::service::nodeinfo::schema::*;
//! NodeInfo fetcher
//!
//! ref: <https://nodeinfo.diaspora.software/protocol.html>
use crate::federation::nodeinfo::schema::*;
use crate::util::http_client;
use isahc::AsyncReadResponseExt;
use serde::{Deserialize, Serialize};
use serde::Deserialize;
/// Errors that can occur while fetching NodeInfo from a remote server
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("HTTP client aquisition error: {0}")]
@ -19,25 +24,23 @@ pub enum Error {
MissingNodeinfo,
}
#[derive(Deserialize, Serialize, Debug)]
/// Represents the schema of `/.well-known/nodeinfo`.
#[derive(Deserialize, Debug)]
pub struct NodeinfoLinks {
links: Vec<NodeinfoLink>,
}
#[derive(Deserialize, Serialize, Debug)]
/// Represents one entry of `/.well-known/nodeinfo`.
#[derive(Deserialize, Debug)]
pub struct NodeinfoLink {
rel: String,
href: String,
}
#[inline]
fn wellknown_nodeinfo_url(host: &str) -> String {
format!("https://{}/.well-known/nodeinfo", host)
}
/// Fetches `/.well-known/nodeinfo` and parses the result.
async fn fetch_nodeinfo_links(host: &str) -> Result<NodeinfoLinks, Error> {
let client = http_client::client()?;
let wellknown_url = wellknown_nodeinfo_url(host);
let wellknown_url = format!("https://{}/.well-known/nodeinfo", host);
let mut wellknown_response = client.get_async(&wellknown_url).await?;
if !wellknown_response.status().is_success() {
@ -52,6 +55,9 @@ async fn fetch_nodeinfo_links(host: &str) -> Result<NodeinfoLinks, Error> {
Ok(serde_json::from_str(&wellknown_response.text().await?)?)
}
/// Check if any of the following relations is present in the given [NodeinfoLinks].
/// * <http://nodeinfo.diaspora.software/ns/schema/2.0>
/// * <http://nodeinfo.diaspora.software/ns/schema/2.1>
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"
@ -64,6 +70,7 @@ fn check_nodeinfo_link(links: NodeinfoLinks) -> Result<String, Error> {
Err(Error::MissingNodeinfo)
}
/// Fetches the nodeinfo from the given URL and parses the result.
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?;
@ -83,6 +90,7 @@ async fn fetch_nodeinfo_impl(nodeinfo_link: &str) -> Result<Nodeinfo20, Error> {
// for napi export
type Nodeinfo = Nodeinfo20;
/// Fetches and returns the NodeInfo (version 2.0) of a remote server.
#[crate::export]
pub async fn fetch_nodeinfo(host: &str) -> Result<Nodeinfo, Error> {
tracing::info!("fetching from {}", host);

View file

@ -1,12 +1,14 @@
use crate::config::CONFIG;
//! NodeInfo generator
use crate::config::{local_server_info, CONFIG};
use crate::database::{cache, db_conn};
use crate::misc::meta::fetch_meta;
use crate::federation::nodeinfo::schema::*;
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;
/// Errors that can occur while generating NodeInfo of the local server
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Database error: {0}")]
@ -17,6 +19,14 @@ pub enum Error {
Json(#[from] serde_json::Error),
}
/// Fetches the number of total/active local users and local posts.
///
/// # Return value
/// A tuple containing the following information in this order:
/// * the total number of local users
/// * the total number of local users active in the last 6 months
/// * the total number of local users active in the last month (MAU)
/// * the total number of posts from local users
async fn statistics() -> Result<(u64, u64, u64, u64), DbErr> {
let db = db_conn().await?;
@ -47,10 +57,12 @@ async fn statistics() -> Result<(u64, u64, u64, u64), DbErr> {
)
}
/// Generates NodeInfo (version 2.1) of the local server.
/// This function doesn't use caches and returns the latest information.
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 meta = local_server_info().await?;
let metadata = HashMap::from([
(
"nodeName".to_string(),
@ -112,6 +124,7 @@ async fn generate_nodeinfo_2_1() -> Result<Nodeinfo21, Error> {
})
}
/// Returns NodeInfo (version 2.1) of the local server.
pub async fn nodeinfo_2_1() -> Result<Nodeinfo21, Error> {
const NODEINFO_2_1_CACHE_KEY: &str = "nodeinfo_2_1";
@ -126,6 +139,7 @@ pub async fn nodeinfo_2_1() -> Result<Nodeinfo21, Error> {
}
}
/// Returns NodeInfo (version 2.0) of the local server.
pub async fn nodeinfo_2_0() -> Result<Nodeinfo20, Error> {
Ok(nodeinfo_2_1().await?.into())
}

View file

@ -0,0 +1,7 @@
//! NodeInfo handler
//!
//! ref: <https://nodeinfo.diaspora.software/>
pub mod fetch;
pub mod generate;
mod schema;

View file

@ -1,9 +1,13 @@
//! Schema definitions of NodeInfo version 2.0 and 2.1
//!
//! ref: <https://nodeinfo.diaspora.software/schema.html>
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)
// * #[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)]

View file

@ -11,6 +11,7 @@ const GREETING_MESSAGE: &str = "\
If you like Firefish, please consider contributing to the repo. https://firefish.dev/firefish/firefish
";
/// Prints the greeting message and the Firefish version to stdout.
#[crate::export]
pub fn greet() {
println!("{}", GREETING_MESSAGE);

View file

@ -2,6 +2,7 @@ use crate::config::CONFIG;
use tracing::Level;
use tracing_subscriber::FmtSubscriber;
/// Initializes the [tracing] logger.
#[crate::export(js_name = "initializeRustLogger")]
pub fn initialize_logger() {
let mut builder = FmtSubscriber::builder();

View file

@ -1,3 +1,5 @@
//! Initializers
pub mod greet;
pub mod log;
pub mod system_info;

View file

@ -5,10 +5,21 @@ pub type SysinfoPoisonError = PoisonError<MutexGuard<'static, System>>;
static SYSTEM_INFO: OnceLock<Mutex<System>> = OnceLock::new();
pub fn system_info() -> &'static std::sync::Mutex<sysinfo::System> {
/// Gives an access to the shared static [System] object.
///
/// # Example
///
/// ```
/// # use backend_rs::init::system_info::{system_info, SysinfoPoisonError};
/// let system_info = system_info().lock()?;
/// println!("The number of CPU threads is {}.", system_info.cpus().len());
/// # Ok::<(), SysinfoPoisonError>(())
/// ```
pub fn system_info() -> &'static std::sync::Mutex<System> {
SYSTEM_INFO.get_or_init(|| Mutex::new(System::new_all()))
}
/// Prints the server hardware information as the server info log.
#[crate::export]
pub fn show_server_info() -> Result<(), SysinfoPoisonError> {
let system_info = system_info().lock()?;

View file

@ -1,4 +1,4 @@
pub use macro_rs::{export, ts_export};
use macro_rs::{export, ts_export};
pub mod config;
pub mod database;

View file

@ -1,13 +1,26 @@
use crate::misc::meta::fetch_meta;
use sea_orm::DbErr;
//! This module is used in the TypeScript backend only.
// We may want to (re)implement these functions in the `federation` module
// in a Rusty way (e.g., traits of server type) if needed.
/// Checks if a server is blocked.
///
/// ## Argument
/// # Argument
/// `host` - punycoded instance host
#[crate::export]
pub async fn is_blocked_server(host: &str) -> Result<bool, DbErr> {
Ok(fetch_meta(true)
///
/// # Example
/// ```no_run
/// # use backend_rs::misc::check_server_block::is_blocked_server;
/// # async fn f() -> Result<(), Box<dyn std::error::Error>> {
/// assert_eq!(true, is_blocked_server("blocked.com").await?);
/// assert_eq!(false, is_blocked_server("not-blocked.com").await?);
/// assert_eq!(true, is_blocked_server("subdomain.of.blocked.com").await?);
/// assert_eq!(true, is_blocked_server("xn--l8jegik.blocked.com").await?);
/// # Ok(())
/// # }
/// ```
#[crate::ts_export]
pub async fn is_blocked_server(host: &str) -> Result<bool, sea_orm::DbErr> {
Ok(crate::config::local_server_info()
.await?
.blocked_hosts
.iter()
@ -18,11 +31,23 @@ pub async fn is_blocked_server(host: &str) -> Result<bool, DbErr> {
/// Checks if a server is silenced.
///
/// ## Argument
/// # Argument
/// `host` - punycoded instance host
#[crate::export]
pub async fn is_silenced_server(host: &str) -> Result<bool, DbErr> {
Ok(fetch_meta(true)
///
/// # Example
/// ```no_run
/// # use backend_rs::misc::check_server_block::is_silenced_server;
/// # async fn f() -> Result<(), Box<dyn std::error::Error>> {
/// assert_eq!(true, is_silenced_server("silenced.com").await?);
/// assert_eq!(false, is_silenced_server("not-silenced.com").await?);
/// assert_eq!(true, is_silenced_server("subdomain.of.silenced.com").await?);
/// assert_eq!(true, is_silenced_server("xn--l8jegik.silenced.com").await?);
/// # Ok(())
/// # }
/// ```
#[crate::ts_export]
pub async fn is_silenced_server(host: &str) -> Result<bool, sea_orm::DbErr> {
Ok(crate::config::local_server_info()
.await?
.silenced_hosts
.iter()
@ -34,11 +59,23 @@ pub async fn is_silenced_server(host: &str) -> Result<bool, DbErr> {
/// Checks if a server is allowlisted.
/// Returns `Ok(true)` if private mode is disabled.
///
/// ## Argument
/// # Argument
/// `host` - punycoded instance host
#[crate::export]
pub async fn is_allowed_server(host: &str) -> Result<bool, DbErr> {
let meta = fetch_meta(true).await?;
///
/// # Example
/// ```no_run
/// # use backend_rs::misc::check_server_block::is_allowed_server;
/// # async fn f() -> Result<(), Box<dyn std::error::Error>> {
/// assert_eq!(true, is_allowed_server("allowed.com").await?);
/// assert_eq!(false, is_allowed_server("not-allowed.com").await?);
/// assert_eq!(false, is_allowed_server("subdomain.of.allowed.com").await?);
/// assert_eq!(false, is_allowed_server("xn--l8jegik.allowed.com").await?);
/// # Ok(())
/// # }
/// ```
#[crate::ts_export]
pub async fn is_allowed_server(host: &str) -> Result<bool, sea_orm::DbErr> {
let meta = crate::config::local_server_info().await?;
if !meta.private_mode.unwrap_or(false) {
return Ok(true);

View file

@ -1,4 +1,4 @@
use crate::misc::get_note_all_texts::{all_texts, NoteLike};
use crate::misc::get_note_all_texts::{all_texts, PartialNoteToElaborate};
use once_cell::sync::Lazy;
use regex::Regex;
use sea_orm::DbErr;
@ -26,9 +26,23 @@ fn check_word_mute_impl(
})
}
/// Returns whether `note` should be hard-muted.
///
/// More specifically, this function returns `Ok(true)`
/// if and only if one or more of these conditions are met:
///
/// * the note (text or CW) contains any of the words/patterns
/// * the "parent" note(s) (reply, quote) contain any of the words/patterns
/// * the alt text of the attached files contains any of the words/patterns
///
/// # Arguments
///
/// * `note` : [PartialNoteToElaborate] object
/// * `muted_words` : list of muted keyword lists (each array item is a space-separated keyword list that represents an AND condition)
/// * `muted_patterns` : list of JavaScript-style (e.g., `/foo/i`) regular expressions
#[crate::export]
pub async fn check_word_mute(
note: NoteLike,
note: PartialNoteToElaborate,
muted_words: &[String],
muted_patterns: &[String],
) -> Result<bool, DbErr> {

View file

@ -1,4 +1,6 @@
use crate::config::CONFIG;
//! This module is used in the TypeScript backend only.
// We may want to (re)implement these functions in the `federation` module
// in a Rusty way (e.g., traits of actor type) if needed.
#[derive(thiserror::Error, Debug)]
pub enum Error {
@ -10,28 +12,28 @@ pub enum Error {
NoHostname,
}
#[crate::export]
#[crate::ts_export]
pub fn get_full_ap_account(username: &str, host: Option<&str>) -> Result<String, Error> {
Ok(match host {
Some(host) => format!("{}@{}", username, to_puny(host)?),
None => format!("{}@{}", username, extract_host(&CONFIG.url)?),
None => format!("{}@{}", username, extract_host(&crate::config::CONFIG.url)?),
})
}
#[crate::export]
#[crate::ts_export]
pub fn is_self_host(host: Option<&str>) -> Result<bool, Error> {
Ok(match host {
Some(host) => extract_host(&CONFIG.url)? == to_puny(host)?,
Some(host) => extract_host(&crate::config::CONFIG.url)? == to_puny(host)?,
None => true,
})
}
#[crate::export]
#[crate::ts_export]
pub fn is_same_origin(uri: &str) -> Result<bool, Error> {
Ok(url::Url::parse(uri)?.origin().ascii_serialization() == CONFIG.url)
Ok(url::Url::parse(uri)?.origin().ascii_serialization() == crate::config::CONFIG.url)
}
#[crate::export]
#[crate::ts_export]
pub fn extract_host(uri: &str) -> Result<String, Error> {
url::Url::parse(uri)?
.host_str()
@ -39,29 +41,7 @@ pub fn extract_host(uri: &str) -> Result<String, Error> {
.and_then(|v| Ok(to_puny(v)?))
}
#[crate::export]
#[crate::ts_export]
pub fn to_puny(host: &str) -> Result<String, idna::Errors> {
idna::domain_to_ascii(host)
}
#[cfg(test)]
mod unit_test {
use super::{extract_host, to_puny};
use pretty_assertions::assert_eq;
#[test]
fn extract_host_test() {
assert_eq!(
extract_host("https://firefish.dev/firefish/firefish.git").unwrap(),
"firefish.dev"
);
}
#[test]
fn to_puny_test() {
assert_eq!(
to_puny("何もかも.owari.shop").unwrap(),
"xn--u8jyfb5762a.owari.shop"
);
}
}

View file

@ -1,3 +1,5 @@
//! This module is used in the TypeScript backend only.
#[crate::ts_export]
pub fn is_unicode_emoji(s: &str) -> bool {
emojis::get(s).is_some()

View file

@ -1,8 +1,10 @@
/// Escapes `%` and `\` in the given string.
#[crate::export]
pub fn sql_like_escape(src: &str) -> String {
src.replace('%', r"\%").replace('_', r"\_")
}
/// Returns `true` if `src` does not contain suspicious characters like `%`.
#[crate::export]
pub fn safe_for_sql(src: &str) -> bool {
!src.contains([

View file

@ -1,4 +1,4 @@
/// Convert milliseconds to a human readable string
/// Converts milliseconds to a human readable string.
#[crate::export]
pub fn format_milliseconds(milliseconds: u32) -> String {
let mut seconds = milliseconds / 1000;

View file

@ -2,9 +2,8 @@ use crate::database::db_conn;
use crate::model::entity::{drive_file, note};
use sea_orm::{prelude::*, QuerySelect};
// TODO?: handle name collisions
#[crate::export(object, js_name = "NoteLikeForAllTexts")]
pub struct NoteLike {
#[crate::export(object)]
pub struct PartialNoteToElaborate {
pub file_ids: Vec<String>,
pub user_id: String,
pub text: Option<String>,
@ -16,11 +15,14 @@ pub struct NoteLike {
/// Returns [`Vec<String>`] containing the post text, content warning,
/// those of the "parent" (replied/quoted) posts, and alt texts of attached files.
///
/// ## Arguments
/// # Arguments
///
/// * `note` - [NoteLike] object
/// * `include_parent` - whether to take the reply-to post and quoted post into account
pub async fn all_texts(note: NoteLike, include_parent: bool) -> Result<Vec<String>, DbErr> {
/// * `note` : [PartialNoteToElaborate] object
/// * `include_parent` : whether to take the reply-to post and quoted post into account
pub async fn all_texts(
note: PartialNoteToElaborate,
include_parent: bool,
) -> Result<Vec<String>, DbErr> {
let db = db_conn().await?;
let mut texts: Vec<String> = vec![];

View file

@ -1,10 +1,9 @@
use serde::{Deserialize, Serialize};
use serde::Deserialize;
// TODO?: handle name collisions
#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[crate::export(object, js_name = "NoteLikeForGetNoteSummary")]
pub struct NoteLike {
#[crate::export(object)]
pub struct PartialNoteToSummarize {
pub file_ids: Vec<String>,
pub text: Option<String>,
pub cw: Option<String>,
@ -12,7 +11,7 @@ pub struct NoteLike {
}
#[crate::export]
pub fn get_note_summary(note: NoteLike) -> String {
pub fn get_note_summary(note: PartialNoteToSummarize) -> String {
let mut buf: Vec<String> = vec![];
if let Some(cw) = note.cw {
@ -36,12 +35,12 @@ pub fn get_note_summary(note: NoteLike) -> String {
#[cfg(test)]
mod unit_test {
use super::{get_note_summary, NoteLike};
use super::{get_note_summary, PartialNoteToSummarize};
use pretty_assertions::assert_eq;
#[test]
fn test_note_summary() {
let note = NoteLike {
let note = PartialNoteToSummarize {
file_ids: vec![],
text: Some("Hello world!".to_string()),
cw: None,
@ -49,7 +48,7 @@ mod unit_test {
};
assert_eq!(get_note_summary(note), "Hello world!");
let note_with_cw = NoteLike {
let note_with_cw = PartialNoteToSummarize {
file_ids: vec![],
text: Some("Hello world!".to_string()),
cw: Some("Content warning".to_string()),
@ -57,7 +56,7 @@ mod unit_test {
};
assert_eq!(get_note_summary(note_with_cw), "Content warning");
let note_with_file_and_cw = NoteLike {
let note_with_file_and_cw = PartialNoteToSummarize {
file_ids: vec!["9s7fmcqogiq4igin".to_string()],
text: None,
cw: Some("Selfie, no ec".to_string()),
@ -65,7 +64,7 @@ mod unit_test {
};
assert_eq!(get_note_summary(note_with_file_and_cw), "Selfie, no ec 📎");
let note_with_files_only = NoteLike {
let note_with_files_only = PartialNoteToSummarize {
file_ids: vec![
"9s7fmcqogiq4igin".to_string(),
"9s7qrld5u14cey98".to_string(),
@ -78,7 +77,7 @@ mod unit_test {
};
assert_eq!(get_note_summary(note_with_files_only), "📎 (4)");
let note_all = NoteLike {
let note_all = PartialNoteToSummarize {
file_ids: vec![
"9s7fmcqogiq4igin".to_string(),
"9s7qrld5u14cey98".to_string(),

View file

@ -1,7 +1,9 @@
//! Fetch latest Firefish version from the Firefish repository
use crate::database::cache;
use crate::util::http_client;
use isahc::ReadResponseExt;
use serde::{Deserialize, Serialize};
use serde::Deserialize;
#[derive(thiserror::Error, Debug)]
pub enum Error {
@ -23,7 +25,7 @@ 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)]
#[derive(Debug, Deserialize)]
struct Response {
version: String,
}
@ -43,6 +45,7 @@ async fn get_latest_version() -> Result<String, Error> {
Ok(res_parsed.version)
}
/// Returns the latest Firefish version.
#[crate::export]
pub async fn latest_version() -> Result<String, Error> {
let version: Option<String> =

View file

@ -1,3 +1,5 @@
//! Miscellaneous utilities
pub mod check_server_block;
pub mod check_word_mute;
pub mod convert_host;
@ -11,7 +13,6 @@ pub mod is_quote;
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;

View file

@ -1,6 +1,25 @@
//! Cat language converter
use once_cell::sync::Lazy;
use regex::{Captures, Regex};
/// Converts the given text into the cat language.
///
/// refs:
/// * <https://misskey-hub.net/ns/#isCat>
/// * <https://firefish.dev/ns#speakAsCat>
///
/// # Arguments
///
/// * `text` : original text
/// * `lang` : language code (e.g., `Some("en")`, `Some("en-US")`, `Some("uk-UA")`, `None`)
///
/// # Example
///
/// ```
/// # use backend_rs::misc::nyaify::nyaify;
/// assert_eq!(nyaify("I'll take a nap.", Some("en")), "I'll take a nyap.");
/// ```
#[crate::export]
pub fn nyaify(text: &str, lang: Option<&str>) -> String {
let mut to_return = text.to_owned();

View file

@ -1,9 +1,12 @@
//! Utilities for password hash generation and verification
use argon2::{
password_hash,
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2,
};
/// Hashes the given password using [Argon2] algorithm.
#[crate::export]
pub fn hash_password(password: &str) -> Result<String, password_hash::errors::Error> {
let salt = SaltString::generate(&mut OsRng);
@ -13,7 +16,7 @@ pub fn hash_password(password: &str) -> Result<String, password_hash::errors::Er
}
#[derive(thiserror::Error, Debug)]
pub enum VerifyError {
pub enum Error {
#[error("An error occured while bcrypt verification: {0}")]
Bcrypt(#[from] bcrypt::BcryptError),
#[error("Invalid argon2 password hash: {0}")]
@ -22,8 +25,9 @@ pub enum VerifyError {
Argon2(#[from] argon2::Error),
}
/// Checks whether the given password and hash match.
#[crate::export]
pub fn verify_password(password: &str, hash: &str) -> Result<bool, VerifyError> {
pub fn verify_password(password: &str, hash: &str) -> Result<bool, Error> {
if is_old_password_algorithm(hash) {
Ok(bcrypt::verify(password, hash)?)
} else {
@ -34,6 +38,7 @@ pub fn verify_password(password: &str, hash: &str) -> Result<bool, VerifyError>
}
}
/// Returns whether the [bcrypt] algorithm is used for the password hash.
#[inline]
#[crate::export]
pub fn is_old_password_algorithm(hash: &str) -> bool {

View file

@ -1,6 +1,5 @@
use crate::config::local_server_info;
use crate::database::db_conn;
use crate::misc::convert_host::to_puny;
use crate::misc::meta::fetch_meta;
use crate::model::entity::emoji;
use once_cell::sync::Lazy;
use regex::Regex;
@ -87,7 +86,7 @@ pub async fn to_db_reaction(reaction: Option<&str>, host: Option<&str>) -> Resul
if let Some(host) = host {
// remote emoji
let ascii_host = to_puny(host)?;
let ascii_host = idna::domain_to_ascii(host)?;
// TODO: Does SeaORM have the `exists` method?
if emoji::Entity::find()
@ -119,7 +118,7 @@ pub async fn to_db_reaction(reaction: Option<&str>, host: Option<&str>) -> Resul
};
};
Ok(fetch_meta(true).await?.default_reaction)
Ok(local_server_info().await?.default_reaction)
}
#[cfg(test)]

View file

@ -3,9 +3,9 @@
use crate::database::db_conn;
use crate::model::entity::attestation_challenge;
use chrono::{Duration, Utc};
use sea_orm::{ColumnTrait, DbErr, EntityTrait, QueryFilter};
use sea_orm::prelude::*;
/// Delete all entries in the "attestation_challenge" table created at more than 5 minutes ago
/// Delete all entries in the [attestation_challenge] table created at more than 5 minutes ago
#[crate::export]
pub async fn remove_old_attestation_challenges() -> Result<(), DbErr> {
let res = attestation_challenge::Entity::delete_many()

View file

@ -1,3 +1,5 @@
//! Utilities to check hardware information such as cpu, memory, storage usage
use crate::init::system_info::{system_info, SysinfoPoisonError};
use sysinfo::{Disks, MemoryRefreshKind};

View file

@ -1 +1,3 @@
//! Database structure, auto-generated by [sea_orm]
pub mod entity;

View file

@ -2,7 +2,7 @@ use crate::config::CONFIG;
use crate::database::{cache, db_conn};
use crate::federation::acct::Acct;
use crate::model::entity::{antenna, blocking, following, note, sea_orm_active_enums::*};
use sea_orm::{ColumnTrait, DbErr, EntityTrait, QueryFilter, QuerySelect};
use sea_orm::{prelude::*, QuerySelect};
#[derive(thiserror::Error, Debug)]
pub enum AntennaCheckError {

View file

@ -1,12 +1,12 @@
use crate::database::{cache, db_conn, redis_conn, redis_key, RedisConnError};
use crate::federation::acct::Acct;
use crate::misc::get_note_all_texts::{all_texts, NoteLike};
use crate::misc::get_note_all_texts::{all_texts, PartialNoteToElaborate};
use crate::model::entity::{antenna, note};
use crate::service::antenna::check_hit::{check_hit_antenna, AntennaCheckError};
use crate::service::stream;
use crate::util::id::{get_timestamp, InvalidIdError};
use redis::{streams::StreamMaxlen, AsyncCommands, RedisError};
use sea_orm::{DbErr, EntityTrait};
use sea_orm::prelude::*;
#[derive(thiserror::Error, Debug)]
pub enum Error {
@ -53,7 +53,7 @@ pub async fn update_antennas_on_new_note(
) -> Result<(), Error> {
let note_cloned = note.clone();
let note_all_texts = all_texts(
NoteLike {
PartialNoteToElaborate {
file_ids: note.file_ids,
user_id: note.user_id,
text: note.text,

View file

@ -1,5 +1,6 @@
//! Services provided for local users
pub mod antenna;
pub mod nodeinfo;
pub mod note;
pub mod push_notification;
pub mod stream;

View file

@ -1,3 +0,0 @@
pub mod fetch;
pub mod generate;
mod schema;

View file

@ -1,7 +1,7 @@
use crate::database::db_conn;
use crate::model::entity::note_watching;
use crate::util::id::gen_id;
use sea_orm::{ActiveValue, ColumnTrait, DbErr, EntityTrait, ModelTrait, QueryFilter};
use crate::util::id::gen_id_at;
use sea_orm::{prelude::*, ActiveValue};
#[crate::export]
pub async fn watch_note(
@ -10,9 +10,11 @@ pub async fn watch_note(
note_id: &str,
) -> Result<(), DbErr> {
if watcher_id != note_author_id {
let now = chrono::Utc::now();
note_watching::Entity::insert(note_watching::ActiveModel {
id: ActiveValue::set(gen_id()),
created_at: ActiveValue::set(chrono::Utc::now().into()),
id: ActiveValue::set(gen_id_at(now)),
created_at: ActiveValue::set(now.into()),
user_id: ActiveValue::Set(watcher_id.to_string()),
note_user_id: ActiveValue::Set(note_author_id.to_string()),
note_id: ActiveValue::Set(note_id.to_string()),

View file

@ -1,10 +1,10 @@
use crate::config::local_server_info;
use crate::database::db_conn;
use crate::misc::get_note_summary::{get_note_summary, NoteLike};
use crate::misc::meta::fetch_meta;
use crate::misc::get_note_summary::{get_note_summary, PartialNoteToSummarize};
use crate::model::entity::sw_subscription;
use crate::util::http_client;
use once_cell::sync::OnceCell;
use sea_orm::{prelude::*, DbErr};
use sea_orm::prelude::*;
use web_push::{
ContentEncoding, IsahcWebPushClient, SubscriptionInfo, SubscriptionKeys, VapidSignatureBuilder,
WebPushClient, WebPushError, WebPushMessageBuilder,
@ -87,7 +87,7 @@ fn compact_content(
));
}
let note_like: NoteLike = serde_json::from_value(note.clone())?;
let note_like: PartialNoteToSummarize = serde_json::from_value(note.clone())?;
let text = get_note_summary(note_like);
let note_object = note.as_object_mut().unwrap();
@ -102,7 +102,7 @@ fn compact_content(
}
async fn handle_web_push_failure(
db: &DatabaseConnection,
db: &DbConn,
err: WebPushError,
subscription_id: &str,
error_message: &str,
@ -140,7 +140,7 @@ pub async fn send_push_notification(
kind: PushNotificationKind,
content: &serde_json::Value,
) -> Result<(), Error> {
let meta = fetch_meta(true).await?;
let meta = local_server_info().await?;
if !meta.enable_service_worker || meta.sw_public_key.is_none() || meta.sw_private_key.is_none()
{

View file

@ -1,8 +1,8 @@
use crate::service::stream::{publish_to_stream, Error, Stream};
use serde::{Deserialize, Serialize};
use serde::Serialize;
// TODO: define schema type in other place
#[derive(Deserialize, Serialize)]
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
#[crate::export(object)]
pub struct PackedEmoji {

View file

@ -1,7 +1,7 @@
use crate::service::stream::{publish_to_stream, Error, Stream};
use serde::{Deserialize, Serialize};
use serde::Serialize;
#[derive(Deserialize, Serialize)]
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
#[crate::export(object)]
pub struct AbuseUserReportLike {

View file

@ -1,3 +1,5 @@
//! Shared [isahc] HTTP client
use crate::config::CONFIG;
use isahc::{config::*, HttpClient};
use once_cell::sync::OnceCell;
@ -13,6 +15,22 @@ pub enum Error {
static CLIENT: OnceCell<HttpClient> = OnceCell::new();
/// Returns an [HttpClient] that takes the proxy configuration into account.
///
/// # Example
/// ```no_run
/// # use backend_rs::util::http_client::client;
/// use isahc::ReadResponseExt;
///
/// # fn f() -> Result<(), Box<dyn std::error::Error>> {
/// let mut response = client()?.get("https://example.com/")?;
///
/// if response.status().is_success() {
/// println!("{}", response.text()?);
/// }
/// # Ok(())
/// # }
/// ```
pub fn client() -> Result<HttpClient, Error> {
CLIENT
.get_or_try_init(|| {

View file

@ -1,3 +1,5 @@
//! Basic utilities such as ID generator and HTTP client
pub mod http_client;
pub mod id;
pub mod random;

View file

@ -1,6 +1,8 @@
//! Secure random string generator
use rand::{distributions::Alphanumeric, thread_rng, Rng};
/// Generate random string based on [thread_rng] and [Alphanumeric].
/// Generates a random string based on [thread_rng] and [Alphanumeric].
#[crate::export]
pub fn generate_secure_random_string(length: u16) -> String {
thread_rng()

View file

@ -1,23 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Firefish API</title>
<!-- needed for adaptive design -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--
ReDoc doesn't change outer page styles
-->
<style>
body {
margin: 0;
padding: 0;
}
</style>
<!-- Embed elements Elements via Web Component -->
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css">
</head>
<body>
<redoc spec-url="/api.json" expand-responses="200" expand-single-schema-field="true"></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@2.0.0-rc.50/bundles/redoc.standalone.js" integrity="sha256-WJbngBWN9vp6vkEuzeoSj5tE5saW9Hfj6/SinkzhL2s=" crossorigin="anonymous"></script>
<elements-api
id="element-main"
apiDescriptionUrl='/api.json'
router="hash"
layout="responsive"
/>
</body>
</html>

View file

@ -22,26 +22,26 @@
"@swc/core-android-arm64": "1.3.11"
},
"dependencies": {
"@bull-board/api": "5.18.3",
"@bull-board/koa": "5.18.3",
"@bull-board/ui": "5.18.3",
"@bull-board/api": "5.20.0",
"@bull-board/koa": "5.20.0",
"@bull-board/ui": "5.20.0",
"@discordapp/twemoji": "15.0.3",
"@koa/cors": "5.0.0",
"@koa/multer": "3.0.2",
"@koa/router": "12.0.1",
"@ladjs/koa-views": "9.0.0",
"@peertube/http-signature": "1.7.0",
"@redocly/openapi-core": "1.13.0",
"@redocly/openapi-core": "1.14.0",
"@sinonjs/fake-timers": "11.2.2",
"adm-zip": "0.5.10",
"ajv": "8.14.0",
"adm-zip": "0.5.14",
"ajv": "8.16.0",
"archiver": "7.0.1",
"aws-sdk": "2.1628.0",
"aws-sdk": "2.1635.0",
"axios": "1.7.2",
"backend-rs": "workspace:*",
"blurhash": "2.0.5",
"bull": "4.12.9",
"cacheable-lookup": "TheEssem/cacheable-lookup",
"cacheable-lookup": "git+https://github.com/TheEssem/cacheable-lookup.git#dd2fb616366a3c68dcf321a57a67295967b204bf",
"cbor-x": "1.5.9",
"chalk": "5.3.0",
"cli-highlight": "2.1.11",
@ -57,7 +57,7 @@
"firefish-js": "workspace:*",
"fluent-ffmpeg": "2.1.3",
"form-data": "4.0.0",
"got": "14.3.0",
"got": "14.4.0",
"gunzip-maybe": "1.4.2",
"hpagent": "1.2.0",
"ioredis": "5.4.1",
@ -87,9 +87,9 @@
"node-fetch": "3.3.2",
"nodemailer": "6.9.13",
"opencc-js": "1.0.5",
"otpauth": "9.2.4",
"otpauth": "9.3.1",
"parse5": "7.1.2",
"pg": "8.11.5",
"pg": "8.12.0",
"private-ip": "3.0.2",
"probe-image-size": "7.2.3",
"promise-limit": "2.7.0",
@ -122,14 +122,14 @@
},
"devDependencies": {
"@swc/cli": "0.3.12",
"@swc/core": "1.5.7",
"@swc/core": "1.5.25",
"@types/adm-zip": "0.5.5",
"@types/color-convert": "2.0.3",
"@types/content-disposition": "0.5.8",
"@types/escape-regexp": "0.0.3",
"@types/fluent-ffmpeg": "2.1.24",
"@types/jsdom": "21.1.6",
"@types/jsonld": "1.5.13",
"@types/jsdom": "21.1.7",
"@types/jsonld": "1.5.14",
"@types/jsrsasign": "10.5.14",
"@types/katex": "0.16.7",
"@types/koa": "2.15.0",
@ -143,10 +143,10 @@
"@types/koa__multer": "2.0.7",
"@types/koa__router": "12.0.4",
"@types/mocha": "10.0.6",
"@types/node": "20.12.12",
"@types/node": "20.14.2",
"@types/node-fetch": "2.6.11",
"@types/nodemailer": "6.4.15",
"@types/oauth": "0.9.4",
"@types/oauth": "0.9.5",
"@types/opencc-js": "1.0.3",
"@types/pg": "8.11.6",
"@types/probe-image-size": "7.2.4",
@ -168,7 +168,7 @@
"@types/websocket": "1.0.10",
"@types/ws": "8.5.10",
"cross-env": "7.0.3",
"eslint": "9.3.0",
"eslint": "9.4.0",
"mocha": "10.4.0",
"pug": "3.0.3",
"strict-event-emitter-types": "2.0.0",
@ -176,7 +176,7 @@
"ts-loader": "9.5.1",
"ts-node": "10.9.2",
"tsconfig-paths": "4.2.0",
"type-fest": "4.18.3",
"type-fest": "4.19.0",
"typescript": "5.4.5",
"webpack": "5.91.0",
"ws": "8.17.0"

View file

@ -12,6 +12,8 @@ import { masterMain } from "./master.js";
import { workerMain } from "./worker.js";
import os from "node:os";
import { initializeRustLogger } from "backend-rs";
const logger = new Logger("core", "cyan");
const clusterLogger = logger.createSubLogger("cluster", "orange", false);
const ev = new Xev();
@ -20,6 +22,8 @@ const ev = new Xev();
* Init process
*/
export default async function () {
initializeRustLogger();
const mode =
process.env.mode && ["web", "queue"].includes(process.env.mode)
? `(${process.env.mode})`

View file

@ -4,11 +4,10 @@ import semver from "semver";
import Logger from "@/services/logger.js";
import {
fetchMeta,
greet,
initializeRustLogger,
removeOldAttestationChallenges,
showServerInfo,
updateMetaCache,
type Config,
} from "backend-rs";
import { config, envOption } from "@/config.js";
@ -24,7 +23,6 @@ const bootLogger = logger.createSubLogger("boot", "magenta", false);
export async function masterMain() {
// initialize app
try {
initializeRustLogger();
greet();
showEnvironment();
showServerInfo();
@ -55,7 +53,7 @@ export async function masterMain() {
import("../daemons/server-stats.js").then((x) => x.default());
import("../daemons/queue-stats.js").then((x) => x.default());
// Update meta cache every 5 minitues
setInterval(() => fetchMeta(false), 1000 * 60 * 5);
setInterval(() => updateMetaCache(), 1000 * 60 * 5);
// Remove old attestation challenges
setInterval(() => removeOldAttestationChallenges(), 1000 * 60 * 30);
}

View file

@ -13,7 +13,7 @@ export default async function () {
ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length || 50));
});
const meta = await fetchMeta(true);
const meta = await fetchMeta();
if (!meta.enableServerMachineStats) return;
async function tick() {

View file

@ -217,7 +217,7 @@ export const db = new DataSource({
: false,
logging: log,
logger: log ? new DbLogger() : undefined,
maxQueryExecutionTime: 300,
maxQueryExecutionTime: 3000,
entities: entities,
migrations: ["../../migration/*.js"],
});

View file

@ -3,7 +3,7 @@ import type { ILocalUser } from "@/models/entities/user.js";
import { Users } from "@/models/index.js";
export async function fetchProxyAccount(): Promise<ILocalUser | null> {
const meta = await fetchMeta(true);
const meta = await fetchMeta();
if (meta.proxyAccountId == null) return null;
return (await Users.findOneByOrFail({
id: meta.proxyAccountId,

View file

@ -26,7 +26,7 @@ export async function translate(
from: PostLanguage | null,
to: PostLanguage,
) {
const instance = await fetchMeta(true);
const instance = await fetchMeta();
if (instance.deeplAuthKey == null && instance.libreTranslateApiUrl == null) {
throw Error("No translator is set up on this server.");

View file

@ -6,7 +6,7 @@ import { downloadTextFile } from "@/misc/download-text-file.js";
import { Users, DriveFiles, Mutings } from "@/models/index.js";
import type { DbUserImportJobData } from "@/queue/types.js";
import type { User } from "@/models/entities/user.js";
import { genId, isSelfHost, stringToAcct, toPuny } from "backend-rs";
import { genIdAt, isSelfHost, stringToAcct, toPuny } from "backend-rs";
import { IsNull } from "typeorm";
import { inspect } from "node:util";
@ -80,9 +80,11 @@ export async function importMuting(
}
async function mute(user: User, target: User) {
const now = new Date();
await Mutings.insert({
id: genId(),
createdAt: new Date(),
id: genIdAt(now),
createdAt: now,
muterId: user.id,
muteeId: target.id,
});

View file

@ -10,7 +10,7 @@ import {
UserLists,
UserListJoinings,
} from "@/models/index.js";
import { genId, isSelfHost, stringToAcct, toPuny } from "backend-rs";
import { genIdAt, isSelfHost, stringToAcct, toPuny } from "backend-rs";
import type { DbUserImportJobData } from "@/queue/types.js";
import { IsNull } from "typeorm";
import { inspect } from "node:util";
@ -54,9 +54,10 @@ export async function importUserLists(
});
if (list == null) {
const now = new Date();
list = await UserLists.insert({
id: genId(),
createdAt: new Date(),
id: genIdAt(now),
createdAt: now,
userId: user.id,
name: listName,
}).then((x) => UserLists.findOneByOrFail(x.identifiers[0]));

View file

@ -15,7 +15,7 @@ import type { UserPublickey } from "@/models/entities/user-publickey.js";
import { verify } from "node:crypto";
export async function hasSignature(req: IncomingMessage): Promise<string> {
const meta = await fetchMeta(true);
const meta = await fetchMeta();
const required = meta.secureMode || meta.privateMode;
try {
@ -30,7 +30,7 @@ export async function hasSignature(req: IncomingMessage): Promise<string> {
}
export async function checkFetch(req: IncomingMessage): Promise<number> {
const meta = await fetchMeta(true);
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
if (req.headers.host !== config.host) return 400;

View file

@ -4,7 +4,7 @@ import type { IFlag } from "../../type.js";
import { getApIds } from "../../type.js";
import { AbuseUserReports, Users } from "@/models/index.js";
import { In } from "typeorm";
import { genId } from "backend-rs";
import { genIdAt } from "backend-rs";
export default async (
actor: CacheableRemoteUser,
@ -23,9 +23,11 @@ export default async (
});
if (users.length < 1) return "skip";
const now = new Date();
await AbuseUserReports.insert({
id: genId(),
createdAt: new Date(),
id: genIdAt(now),
createdAt: now,
targetUserId: users[0].id,
targetUserHost: users[0].host,
reporterId: actor.id,

View file

@ -36,7 +36,7 @@ export async function createImage(
apLogger.info(`Creating an image: ${image.url}`);
const instance = await fetchMeta(true);
const instance = await fetchMeta();
let file = await uploadFromUrl({
url: image.url,

View file

@ -16,7 +16,7 @@ import type { IRemoteUser, CacheableUser } from "@/models/entities/user.js";
import { User } from "@/models/entities/user.js";
import type { Emoji } from "@/models/entities/emoji.js";
import { UserNotePining } from "@/models/entities/user-note-pining.js";
import { genId, isSameOrigin, toPuny } from "backend-rs";
import { genId, genIdAt, isSameOrigin, toPuny } from "backend-rs";
import { UserPublickey } from "@/models/entities/user-publickey.js";
import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js";
import { UserProfile } from "@/models/entities/user-profile.js";
@ -242,16 +242,17 @@ export async function createPerson(
// Create user
let user: IRemoteUser;
const now = new Date();
try {
// Start transaction
await db.transaction(async (transactionalEntityManager) => {
user = (await transactionalEntityManager.save(
new User({
id: genId(),
id: genIdAt(now),
avatarId: null,
bannerId: null,
createdAt: new Date(),
lastFetchedAt: new Date(),
createdAt: now,
lastFetchedAt: now,
name: truncate(person.name, nameLength),
isLocked: !!person.manuallyApprovesFollowers,
movedToUri: person.movedTo,

View file

@ -243,7 +243,7 @@ router.get("/notes/:note", async (ctx, next) => {
ctx.body = renderActivity(await renderNote(note, false));
const meta = await fetchMeta(true);
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
} else {
@ -273,7 +273,7 @@ router.get("/notes/:note/activity", async (ctx) => {
}
ctx.body = renderActivity(await packActivity(note));
const meta = await fetchMeta(true);
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
} else {
@ -328,7 +328,7 @@ router.get("/users/:user/publickey", async (ctx) => {
if (Users.isLocalUser(user)) {
ctx.body = renderActivity(renderKey(user, keypair));
const meta = await fetchMeta(true);
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
} else {
@ -348,7 +348,7 @@ async function userInfo(ctx: Router.RouterContext, user: User | null) {
}
ctx.body = renderActivity(await renderPerson(user as ILocalUser));
const meta = await fetchMeta(true);
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
} else {
@ -432,7 +432,7 @@ router.get("/emojis/:emoji", async (ctx) => {
}
ctx.body = renderActivity(renderEmoji(emoji));
const meta = await fetchMeta(true);
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
} else {
@ -464,7 +464,7 @@ router.get("/likes/:like", async (ctx) => {
}
ctx.body = renderActivity(await renderLike(reaction, note));
const meta = await fetchMeta(true);
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
} else {
@ -502,7 +502,7 @@ router.get(
}
ctx.body = renderActivity(renderFollow(follower, followee));
const meta = await fetchMeta(true);
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
} else {
@ -545,7 +545,7 @@ router.get("/follows/:followRequestId", async (ctx: Router.RouterContext) => {
return;
}
const meta = await fetchMeta(true);
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
} else {

View file

@ -57,7 +57,7 @@ export default async (ctx: Router.RouterContext) => {
ctx.body = renderActivity(rendered);
const meta = await fetchMeta(true);
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
} else {

View file

@ -110,7 +110,7 @@ export default async (ctx: Router.RouterContext) => {
ctx.body = renderActivity(rendered);
setResponseType(ctx);
}
const meta = await fetchMeta(true);
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
} else {

View file

@ -110,7 +110,7 @@ export default async (ctx: Router.RouterContext) => {
ctx.body = renderActivity(rendered);
setResponseType(ctx);
}
const meta = await fetchMeta(true);
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
} else {

View file

@ -117,7 +117,7 @@ export default async (ctx: Router.RouterContext) => {
setResponseType(ctx);
}
const meta = await fetchMeta(true);
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
} else {

View file

@ -84,7 +84,7 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) =>
// Log IP
if (user) {
fetchMeta(true).then((meta) => {
fetchMeta().then((meta) => {
if (!meta.enableIpLogging) return;
const ip = ctx.ip;
const ips = userIpHistories.get(user.id);

View file

@ -117,7 +117,7 @@ export default async (
}
// private mode
const meta = await fetchMeta(true);
const meta = await fetchMeta();
if (
meta.privateMode &&
ep.meta.requireCredentialPrivateMode &&

View file

@ -68,12 +68,10 @@ export async function readUserMessagingMessage(
await sendPushNotification(userId, PushNotificationKind.ReadAllChats, {});
} else {
// そのユーザーとのメッセージで未読がなければイベント発行
const hasUnread = await MessagingMessages.exists({
where: {
userId: otherpartyId,
recipientId: userId,
isRead: false,
},
const hasUnread = await MessagingMessages.existsBy({
userId: otherpartyId,
recipientId: userId,
isRead: false,
});
if (!hasUnread) {

View file

@ -3,7 +3,7 @@ import type Koa from "koa";
import { config } from "@/config.js";
import type { ILocalUser } from "@/models/entities/user.js";
import { Signins } from "@/models/index.js";
import { genId } from "backend-rs";
import { genIdAt } from "backend-rs";
import { publishMainStream } from "@/services/stream.js";
export default function (ctx: Koa.Context, user: ILocalUser, redirect = false) {
@ -29,9 +29,10 @@ export default function (ctx: Koa.Context, user: ILocalUser, redirect = false) {
(async () => {
// Append signin history
const now = new Date();
const record = await Signins.insert({
id: genId(),
createdAt: new Date(),
id: genIdAt(now),
createdAt: now,
userId: user.id,
ip: ctx.ip,
headers: ctx.headers,

View file

@ -3,7 +3,7 @@ import { User } from "@/models/entities/user.js";
import { Users, UsedUsernames } from "@/models/index.js";
import { UserProfile } from "@/models/entities/user-profile.js";
import { IsNull } from "typeorm";
import { genId, generateUserToken, hashPassword, toPuny } from "backend-rs";
import { genIdAt, generateUserToken, hashPassword, toPuny } from "backend-rs";
import { UserKeypair } from "@/models/entities/user-keypair.js";
import { UsedUsername } from "@/models/entities/used-username.js";
import { db } from "@/db/postgre.js";
@ -82,19 +82,23 @@ export async function signup(opts: {
let account!: User;
const exists = await Users.existsBy({
usernameLower: username.toLowerCase(),
host: IsNull(),
});
if (exists) {
throw new Error("the username is already used");
}
// Start transaction
await db.transaction(async (transactionalEntityManager) => {
const exist = await transactionalEntityManager.findOneBy(User, {
usernameLower: username.toLowerCase(),
host: IsNull(),
});
if (exist) throw new Error(" the username is already used");
const now = new Date();
account = await transactionalEntityManager.save(
new User({
id: genId(),
createdAt: new Date(),
id: genIdAt(now),
createdAt: now,
username: username,
usernameLower: username.toLowerCase(),
host: host == null ? null : toPuny(host),
@ -125,7 +129,7 @@ export async function signup(opts: {
await transactionalEntityManager.save(
new UsedUsername({
createdAt: new Date(),
createdAt: now,
username: username.toLowerCase(),
}),
);

View file

@ -1,6 +1,6 @@
import define from "@/server/api/define.js";
import { Ads } from "@/models/index.js";
import { genId } from "backend-rs";
import { genIdAt } from "backend-rs";
export const meta = {
tags: ["admin"],
@ -32,9 +32,11 @@ export const paramDef = {
} as const;
export default define(meta, paramDef, async (ps) => {
const now = new Date();
await Ads.insert({
id: genId(),
createdAt: new Date(),
id: genIdAt(now),
createdAt: now,
expiresAt: new Date(ps.expiresAt),
url: ps.url,
imageUrl: ps.imageUrl,

View file

@ -1,6 +1,6 @@
import define from "@/server/api/define.js";
import { Announcements } from "@/models/index.js";
import { genId } from "backend-rs";
import { genIdAt } from "backend-rs";
export const meta = {
tags: ["admin"],
@ -74,9 +74,10 @@ export const paramDef = {
} as const;
export default define(meta, paramDef, async (ps) => {
const now = new Date();
const announcement = await Announcements.insert({
id: genId(),
createdAt: new Date(),
id: genIdAt(now),
createdAt: now,
updatedAt: null,
title: ps.title,
text: ps.text,

View file

@ -1,7 +1,7 @@
import rndstr from "rndstr";
import define from "@/server/api/define.js";
import { RegistrationTickets } from "@/models/index.js";
import { genId } from "backend-rs";
import { genIdAt } from "backend-rs";
export const meta = {
tags: ["admin"],
@ -38,9 +38,11 @@ export default define(meta, paramDef, async () => {
chars: "2-9A-HJ-NP-Z", // [0-9A-Z] w/o [01IO] (32 patterns)
});
const now = new Date();
await RegistrationTickets.insert({
id: genId(),
createdAt: new Date(),
id: genIdAt(now),
createdAt: now,
code,
});

View file

@ -470,7 +470,7 @@ export const paramDef = {
} as const;
export default define(meta, paramDef, async () => {
const instance = await fetchMeta(false);
const instance = await fetchMeta();
return {
maintainerName: instance.maintainerName,

View file

@ -40,9 +40,9 @@ export default define(meta, paramDef, async (ps, user) => {
throw err;
});
const exist = await PromoNotes.exist({ where: { noteId: note.id } });
const exists = await PromoNotes.existsBy({ noteId: note.id });
if (exist) {
if (exists) {
throw new ApiError(meta.errors.alreadyPromoted);
}

View file

@ -1,5 +1,5 @@
import define from "@/server/api/define.js";
import { fetchMeta, genId } from "backend-rs";
import { fetchMeta, genIdAt } from "backend-rs";
import { Antennas, UserLists, UserGroupJoinings } from "@/models/index.js";
import { ApiError } from "@/server/api/error.js";
import { publishInternalEvent } from "@/services/stream.js";
@ -123,7 +123,7 @@ export default define(meta, paramDef, async (ps, user) => {
let userList;
let userGroupJoining;
const instance = await fetchMeta(true);
const instance = await fetchMeta();
const antennas = await Antennas.findBy({
userId: user.id,
@ -152,9 +152,11 @@ export default define(meta, paramDef, async (ps, user) => {
}
}
const now = new Date();
const antenna = await Antennas.insert({
id: genId(),
createdAt: new Date(),
id: genIdAt(now),
createdAt: now,
userId: user.id,
name: ps.name,
src: ps.src,

View file

@ -1,6 +1,6 @@
import define from "@/server/api/define.js";
import { Apps } from "@/models/index.js";
import { genId, generateSecureRandomString } from "backend-rs";
import { genIdAt, generateSecureRandomString } from "backend-rs";
import { unique } from "@/prelude/array.js";
export const meta = {
@ -48,9 +48,10 @@ export default define(meta, paramDef, async (ps, user) => {
);
// Create account
const now = new Date();
const app = await Apps.insert({
id: genId(),
createdAt: new Date(),
id: genIdAt(now),
createdAt: now,
userId: user ? user.id : null,
name: ps.name,
description: ps.description,

View file

@ -2,7 +2,7 @@ import * as crypto from "node:crypto";
import define from "@/server/api/define.js";
import { ApiError } from "@/server/api/error.js";
import { AuthSessions, AccessTokens, Apps } from "@/models/index.js";
import { genId, generateSecureRandomString } from "backend-rs";
import { genIdAt, generateSecureRandomString } from "backend-rs";
export const meta = {
tags: ["auth"],
@ -40,14 +40,12 @@ export default define(meta, paramDef, async (ps, user) => {
const accessToken = generateSecureRandomString(32);
// Fetch exist access token
const exist = await AccessTokens.exists({
where: {
appId: session.appId,
userId: user.id,
},
const exists = await AccessTokens.existsBy({
appId: session.appId,
userId: user.id,
});
if (!exist) {
if (!exists) {
// Lookup app
const app = await Apps.findOneByOrFail({ id: session.appId });
@ -60,7 +58,7 @@ export default define(meta, paramDef, async (ps, user) => {
// Insert access token doc
await AccessTokens.insert({
id: genId(),
id: genIdAt(now),
createdAt: now,
lastUsedAt: now,
appId: session.appId,

View file

@ -3,7 +3,7 @@ import { config } from "@/config.js";
import define from "@/server/api/define.js";
import { ApiError } from "@/server/api/error.js";
import { Apps, AuthSessions } from "@/models/index.js";
import { genId } from "backend-rs";
import { genIdAt } from "backend-rs";
export const meta = {
tags: ["auth"],
@ -60,9 +60,10 @@ export default define(meta, paramDef, async (ps) => {
const token = uuid();
// Create session token document
const now = new Date();
const doc = await AuthSessions.insert({
id: genId(),
createdAt: new Date(),
id: genIdAt(now),
createdAt: now,
appId: app.id,
token: token,
}).then((x) => AuthSessions.findOneByOrFail(x.identifiers[0]));

Some files were not shown because too many files have changed in this diff Show more