diff --git a/.dockerignore b/.dockerignore index 30e75f6237..d4c11934d1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -40,14 +40,11 @@ packages/backend/assets/instance.css .gitattributes .weblate animated.svg -cliff.toml docker-compose.yml docker-compose.example.yml -firefish.apache.conf -firefish.nginx.conf title.svg /.gitlab -/chart +/ci /dev /docs /scripts @@ -56,6 +53,8 @@ biome.json CODE_OF_CONDUCT.md CONTRIBUTING.md Dockerfile -Procfile +Makefile README.md SECURITY.md +patrons.json +renovate.json diff --git a/.gitignore b/.gitignore index 1c46fa0a0a..1cf6420856 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ node_modules report.*.json # Cargo +/.cargo /target # Cypress diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ac3db424b5..063082d28c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,10 +25,11 @@ workflow: - when: never stages: - - dependency - test - doc - build + - dependency + - clean variables: POSTGRES_DB: 'firefish_db' @@ -36,6 +37,7 @@ variables: POSTGRES_PASSWORD: 'password' POSTGRES_HOST_AUTH_METHOD: 'trust' DEBIAN_FRONTEND: 'noninteractive' + NODE_OPTIONS: '--max_old_space_size=3072' CARGO_PROFILE_DEV_OPT_LEVEL: '0' CARGO_PROFILE_DEV_LTO: 'off' CARGO_PROFILE_DEV_DEBUG: 'none' @@ -119,6 +121,7 @@ test:build:backend_ts: - cp packages/backend-rs/index.js packages/backend-rs/built/index.js - cp packages/backend-rs/index.d.ts packages/backend-rs/built/index.d.ts - cp ci/cargo/config.toml /usr/local/cargo/config.toml + - test -f packages/backend-rs/built/backend-rs.linux-x64-gnu.node || pnpm install --frozen-lockfile - test -f packages/backend-rs/built/backend-rs.linux-x64-gnu.node || pnpm --filter 'backend-rs' run build:debug - cp .config/ci.yml .config/default.yml - export PGPASSWORD="${POSTGRES_PASSWORD}" @@ -199,6 +202,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" @@ -221,6 +226,31 @@ build:container: - buildah inspect "${IMAGE_TAG}" - buildah push "${IMAGE_TAG}" +cargo:check:msrv: + stage: test + image: docker.io/rust:1.74-slim-bookworm + rules: + - if: $TEST == 'true' + when: always + - if: $TEST == 'false' + when: never + - if: $CI_COMMIT_BRANCH == 'develop' || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'develop' + changes: + paths: + - packages/backend-rs/**/* + - packages/macro-rs/**/* + - Cargo.toml + - Cargo.lock + when: always + services: [] + before_script: + - apt-get update && apt-get -y upgrade + - apt-get install -y --no-install-recommends build-essential clang mold python3 perl nodejs postgresql-client + - cp ci/cargo/config.toml /usr/local/cargo/config.toml + script: + - cargo fetch --locked --manifest-path Cargo.toml + - cargo check --locked --frozen --all-features + cargo:test: stage: test rules: @@ -235,7 +265,6 @@ cargo:test: - packages/macro-rs/**/* - Cargo.toml - Cargo.lock - - package.json when: always script: - curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C /usr/local/cargo/bin @@ -287,7 +316,7 @@ cargo:doc: - cp ci/cargo/config.toml /usr/local/cargo/config.toml script: - cargo doc --document-private-items - - printf "window.ALL_CRATES = ['backend_rs', 'macro_rs'];" > target/doc/crates.js + - printf 'window.ALL_CRATES = ["backend_rs", "macros", "macros_impl"];' > target/doc/crates.js - printf '' 'backend_rs' > target/doc/index.html - cd target/doc - npx --yes netlify-cli deploy --prod --site="${CARGO_DOC_SITE_ID}" --dir=. @@ -303,3 +332,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 diff --git a/.gitlab/issue_templates/bug.md b/.gitlab/issue_templates/bug.md deleted file mode 100644 index f96bdec01e..0000000000 --- a/.gitlab/issue_templates/bug.md +++ /dev/null @@ -1,49 +0,0 @@ - - -## What happened? - - -## What did you expect to happen? - - -## Version - - -## What type of issue is this? - -- [ ] server-side -- [ ] client-side -- [ ] not sure - -
- -### Instance - - -### What browser are you using? (client-side issues only) - - -### What operating system are you using? (client-side issues only) - - -### How do you deploy Firefish on your server? (server-side issues only) - - -### What operating system are you using? (Server-side issues only) - - -### Relevant log output - - -
- -## Contribution Guidelines -By submitting this issue, you agree to follow our [Contribution Guidelines](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md) -- [ ] I agree to follow this project's Contribution Guidelines -- [ ] I have searched the issue tracker for similar issues, and this is not a duplicate. - -## Are you willing to fix this bug? (optional) -- [ ] Yes. I will fix this bug and open a merge request if the change is agreed upon. diff --git a/.gitlab/issue_templates/default.md b/.gitlab/issue_templates/default.md new file mode 100644 index 0000000000..f5823ea666 --- /dev/null +++ b/.gitlab/issue_templates/default.md @@ -0,0 +1,99 @@ + + + + +## What type of issue is this? + + + + + + + + + + + + + +## What happened? + + + +## What did you expect to happen? + + + +## Steps to reproduce the issue + + + +## Reproduces how often + + + +## What did you try to solve the issue + + + +## Version + + + +
+ +### Instance + + + +### What browser are you using? (client-side issues only) + + +### What operating system are you using? (client-side issues only) + + +### How do you deploy Firefish on your server? (server-side issues only) + + +### What operating system are you using? (Server-side issues only) + + +### Relevant log output + + + +
+ +## Contribution Guidelines +By submitting this issue, you agree to follow our [Contribution Guidelines](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md) +- [ ] I agree to follow this project's Contribution Guidelines +- [ ] I have searched the issue tracker for similar issues, and this is not a duplicate. + +## Are you willing to fix this bug? (optional) + + + + + + + + + +/label Bug? diff --git a/.gitlab/issue_templates/feature.md b/.gitlab/issue_templates/feature.md index 4c9ee56226..a99265161b 100644 --- a/.gitlab/issue_templates/feature.md +++ b/.gitlab/issue_templates/feature.md @@ -1,18 +1,45 @@ - + + +🤝 By submitting this refactor proposal, you agree to follow our [Contribution Guidelines.](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md) --> -## What feature would you like implemented? +## What type of refactoring is this? + + + + + + + + + + + + + +## What feature would you like implemented? + -## Why should we add this feature? +## Why should we add this feature? + -## Version +## Version + -## Instance +## Instance + ## Contribution Guidelines @@ -21,4 +48,20 @@ By submitting this issue, you agree to follow our [Contribution Guidelines](http - [ ] I have searched the issue tracker for similar requests, and this is not a duplicate. ## Are you willing to implement this feature? (optional) -- [ ] Yes. I will implement this feature and open a merge request if the change is agreed upon. + + + + + + + + + +/label Feature diff --git a/.gitlab/issue_templates/refactor.md b/.gitlab/issue_templates/refactor.md new file mode 100644 index 0000000000..e9f9ffe05f --- /dev/null +++ b/.gitlab/issue_templates/refactor.md @@ -0,0 +1,67 @@ + + + + +## What type of feature is this? + + + + + + + + + + + + + +## What parts of the code do you think should be refactored? + + + +## Why should the code be refactored that way? + + + +## Version + + + +## Instance + + + +## Contribution Guidelines +By submitting this issue, you agree to follow our [Contribution Guidelines](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md) +- [ ] I agree to follow this project's Contribution Guidelines +- [ ] I have searched the issue tracker for similar requests, and this is not a duplicate. + +## Are you willing to refactor the code? (optional) + + + + + + + + + +/label Refactor diff --git a/.gitlab/merge_request_templates/default.md b/.gitlab/merge_request_templates/default.md index d13a146da0..6d09072a87 100644 --- a/.gitlab/merge_request_templates/default.md +++ b/.gitlab/merge_request_templates/default.md @@ -1,16 +1,19 @@ - + -## What does this PR do? +## What does this merge request do? + ## Contribution Guidelines By submitting this merge request, you agree to follow our [Contribution Guidelines](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md) -- [ ] This change is reviewed in an issue / This is a minor bug fix -- [ ] I agree to follow this project's Contribution Guidelines -- [ ] I have made sure to test this pull request -- [ ] I have made sure to run `pnpm run format` before submitting this pull request +- [ ] This closes issue #0000 (please substitute the number) +- [ ] This is a minor bug fix or refactoring -If this merge request makes changes to the Firefish API, please update `docs/api-change.md` +- [ ] I agree to follow this project's Contribution Guidelines +- [ ] I have made sure to test this merge request +- [ ] I have made sure to run `pnpm run format` before submitting this merge request + +If this merge request makes changes to API, please update `docs/api-change.md` - [ ] I updated the document / This merge request doesn't include API changes diff --git a/.gitlab/merge_request_templates/release.md b/.gitlab/merge_request_templates/release.md index ea40f38813..6b83d5fdc4 100644 --- a/.gitlab/merge_request_templates/release.md +++ b/.gitlab/merge_request_templates/release.md @@ -1,4 +1,5 @@ +/label Release ## Checklist @@ -13,7 +14,5 @@ I have updated... - [ ] `packages/backend-rs/index.js` - [ ] OCI container image - - ## Remarks diff --git a/Cargo.lock b/Cargo.lock index f907f47515..d91ec63a4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,7 +92,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -143,7 +143,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -154,7 +154,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -207,10 +207,10 @@ dependencies = [ "chrono", "cuid2", "emojis", - "idna", + "idna 1.0.2", "image", "isahc", - "macro-rs", + "macros", "napi", "napi-build", "napi-derive", @@ -226,7 +226,6 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "strum 0.26.2", "sysinfo", "thiserror", "tokio", @@ -240,9 +239,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.72" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", @@ -291,12 +290,11 @@ checksum = "1dbe4bb73fd931c4d1aaf53b35d1286c8a948ad00ec92c8e3c856f15fd027f43" [[package]] name = "bb8" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7c2093d15d6a1d33b1f972e1c5ea3177748742b97a5f392aa83a65262c6780" +checksum = "b10cf871f3ff2ce56432fddc2615ac7acc3aa22ca321f8fea800846fbb32f188" dependencies = [ "async-trait", - "futures-channel", "futures-util", "tokio", ] @@ -327,9 +325,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" dependencies = [ "serde", ] @@ -382,9 +380,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.16.0" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" +checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" [[package]] name = "byteorder" @@ -412,9 +410,9 @@ checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" [[package]] name = "cc" -version = "1.0.98" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" +checksum = "74b6a57f98764a267ff415d50a25e6e166f3831a5071af4995296ea97d210490" dependencies = [ "jobserver", "libc", @@ -631,7 +629,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" dependencies = [ "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -669,9 +667,9 @@ dependencies = [ [[package]] name = "curl-sys" -version = "0.4.72+curl-8.6.0" +version = "0.4.73+curl-8.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29cbdc8314c447d11e8fd156dcdd031d9e02a7a976163e396b548c03153bc9ea" +checksum = "450ab250ecf17227c39afb9a2dd9261dc0035cb80f2612472fc0c4aac2dcb84d" dependencies = [ "cc", "libc", @@ -724,7 +722,7 @@ dependencies = [ "proc-macro2", "quote", "syn 1.0.109", - "synstructure", + "synstructure 0.12.6", ] [[package]] @@ -766,6 +764,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -816,9 +825,9 @@ dependencies = [ [[package]] name = "either" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" dependencies = [ "serde", ] @@ -947,7 +956,7 @@ checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" dependencies = [ "futures-core", "futures-sink", - "spin 0.9.8", + "spin", ] [[package]] @@ -1264,6 +1273,124 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", +] + [[package]] name = "idna" version = "0.5.0" @@ -1274,6 +1401,18 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "idna" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd69211b9b519e98303c015e21a007e293db403b6c85b9b124e133d25e242cdd" +dependencies = [ + "icu_normalizer", + "icu_properties", + "smallvec", + "utf8_iter", +] + [[package]] name = "image" version = "0.25.1" @@ -1328,7 +1467,7 @@ checksum = "0122b7114117e64a63ac49f752a5ca4624d534c7b1c7de796ac196381cd2d947" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -1357,7 +1496,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -1477,11 +1616,11 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin 0.5.2", + "spin", ] [[package]] @@ -1503,9 +1642,9 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" dependencies = [ "cfg-if", "windows-targets 0.52.5", @@ -1556,6 +1695,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + [[package]] name = "lock_api" version = "0.4.12" @@ -1568,9 +1713,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "loop9" @@ -1582,18 +1727,24 @@ dependencies = [ ] [[package]] -name = "macro-rs" +name = "macros" version = "0.0.0" dependencies = [ - "convert_case", - "napi", - "napi-derive", + "macros-impl", "proc-macro2", "quote", "serde", "serde_json", - "syn 2.0.66", - "thiserror", +] + +[[package]] +name = "macros-impl" +version = "0.0.0" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn 2.0.68", ] [[package]] @@ -1618,9 +1769,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mime" @@ -1636,9 +1787,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", "simd-adler32", @@ -1657,12 +1808,14 @@ dependencies = [ [[package]] name = "napi" -version = "3.0.0-alpha.2" -source = "git+https://github.com/napi-rs/napi-rs.git?rev=ca2cd5c35a0c39ec4a94e93c6c5695b681046df2#ca2cd5c35a0c39ec4a94e93c6c5695b681046df2" +version = "2.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1bd081bbaef43600fd2c5dd4c525b8ecea7dfdacf40ebc674e87851dce6559e" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "chrono", "ctor", + "napi-derive", "napi-sys", "once_cell", "serde", @@ -1678,35 +1831,38 @@ checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a" [[package]] name = "napi-derive" -version = "2.16.5" +version = "2.16.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0e034ddf6155192cf83f267ede763fe6c164dfa9971585436b16173718d94c4" +checksum = "eafd2b920906ea5b1f5f1f9d1eff9cc74e4ff8124dca41b501c1413079589187" dependencies = [ "cfg-if", "convert_case", "napi-derive-backend", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] name = "napi-derive-backend" -version = "1.0.67" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff2c00437f3b3266391eb5e6aa25d0029187daf5caf05b8e3271468fb5ae73e" +checksum = "b370b784440c65eb9001d839012eb912ee43e3a2d0361e2c30c13052372c39fe" dependencies = [ "convert_case", "once_cell", "proc-macro2", "quote", - "syn 2.0.66", + "regex", + "semver", + "syn 2.0.68", ] [[package]] name = "napi-sys" version = "2.4.0" -source = "git+https://github.com/napi-rs/napi-rs.git?rev=ca2cd5c35a0c39ec4a94e93c6c5695b681046df2#ca2cd5c35a0c39ec4a94e93c6c5695b681046df2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3" dependencies = [ "libloading", ] @@ -1780,9 +1936,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", @@ -1828,7 +1984,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -1884,9 +2040,9 @@ dependencies = [ [[package]] name = "object" -version = "0.35.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e" +checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" dependencies = [ "memchr", ] @@ -1903,7 +2059,7 @@ version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cfg-if", "foreign-types", "libc", @@ -1920,7 +2076,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -1931,9 +2087,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.3.0+3.3.0" +version = "300.3.1+3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eba8804a1c5765b18c4b3f907e6897ebabeedebc9830e1a0046c4a4cf44663e1" +checksum = "7259953d42a81bf137fbbd73bd30a8e1914d6dce43c2b90ed575783a22608b91" dependencies = [ "cc", ] @@ -1981,7 +2137,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -2038,7 +2194,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.1", + "redox_syscall 0.5.2", "smallvec", "windows-targets 0.52.5", ] @@ -2139,7 +2295,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -2289,9 +2445,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.84" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -2312,7 +2468,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" dependencies = [ "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -2460,18 +2616,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] name = "regex" -version = "1.10.4" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", @@ -2481,9 +2637,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", @@ -2492,9 +2648,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "rfc6979" @@ -2508,9 +2664,9 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.37" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" +checksum = "a7439be6844e40133eda024efd85bf07f59d0dd2f59b10c00dd6cfb92cc5c741" dependencies = [ "bytemuck", ] @@ -2525,7 +2681,7 @@ dependencies = [ "cfg-if", "getrandom", "libc", - "spin 0.9.8", + "spin", "untrusted", "windows-sys 0.52.0", ] @@ -2605,7 +2761,7 @@ version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -2642,12 +2798,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "rustversion" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" - [[package]] name = "ryu" version = "1.0.18" @@ -2689,7 +2839,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -2710,7 +2860,7 @@ dependencies = [ "serde", "serde_json", "sqlx", - "strum 0.25.0", + "strum", "thiserror", "time", "tracing", @@ -2728,7 +2878,7 @@ dependencies = [ "proc-macro2", "quote", "sea-bae", - "syn 2.0.66", + "syn 2.0.68", "unicode-ident", ] @@ -2782,6 +2932,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + [[package]] name = "serde" version = "1.0.203" @@ -2799,14 +2955,14 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] name = "serde_json" -version = "1.0.117" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" dependencies = [ "itoa", "ryu", @@ -2962,12 +3118,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "spin" version = "0.9.8" @@ -2999,11 +3149,10 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" +checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" dependencies = [ - "itertools", "nom", "unicode_categories", ] @@ -3111,7 +3260,7 @@ checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" dependencies = [ "atoi", "base64 0.21.7", - "bitflags 2.5.0", + "bitflags 2.6.0", "byteorder", "bytes", "chrono", @@ -3154,7 +3303,7 @@ checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" dependencies = [ "atoi", "base64 0.21.7", - "bitflags 2.5.0", + "bitflags 2.6.0", "byteorder", "chrono", "crc", @@ -3209,6 +3358,12 @@ dependencies = [ "urlencoding", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -3232,33 +3387,11 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" -[[package]] -name = "strum" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.66", -] - [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" @@ -3273,9 +3406,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.66" +version = "2.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" dependencies = [ "proc-macro2", "quote", @@ -3294,6 +3427,17 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", +] + [[package]] name = "sysinfo" version = "0.30.12" @@ -3356,7 +3500,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -3411,10 +3555,20 @@ dependencies = [ ] [[package]] -name = "tinyvec" -version = "1.6.0" +name = "tinystr" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c55115c6fbe2d2bef26eb09ad74bde02d8255476fc0c7b515ef09fbb35742d82" dependencies = [ "tinyvec_macros", ] @@ -3427,9 +3581,9 @@ 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", @@ -3445,13 +3599,13 @@ 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", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -3493,9 +3647,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.13" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" dependencies = [ "serde", "serde_spanned", @@ -3514,9 +3668,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.13" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" +checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" dependencies = [ "indexmap", "serde", @@ -3545,7 +3699,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -3644,12 +3798,12 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", - "idna", + "idna 0.5.0", "percent-encoding", ] @@ -3660,10 +3814,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] -name = "uuid" -version = "1.8.0" +name = "utf16_iter" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439" dependencies = [ "serde", ] @@ -3745,7 +3911,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", "wasm-bindgen-shared", ] @@ -3767,7 +3933,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4003,13 +4169,25 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.6.9" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86c949fede1d13936a99f14fafd3e76fd642b556dd2ce96287fbe2e0151bfac6" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" dependencies = [ "memchr", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "yansi" version = "0.5.1" @@ -4017,23 +4195,68 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] -name = "zerocopy" -version = "0.7.34" +name = "yoke" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", + "synstructure 0.13.1", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", +] + +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", + "synstructure 0.13.1", ] [[package]] @@ -4042,6 +4265,28 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", +] + [[package]] name = "zune-core" version = "0.4.12" diff --git a/Cargo.toml b/Cargo.toml index 8a8c3b7f54..12315f18eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,51 +1,58 @@ [workspace] -members = ["packages/backend-rs", "packages/macro-rs"] +members = ["packages/backend-rs", "packages/macro-rs/macros", "packages/macro-rs/macros-impl"] resolver = "2" [workspace.dependencies] -macro-rs = { path = "packages/macro-rs" } +macros = { path = "packages/macro-rs/macros" } +macros-impl = { path = "packages/macro-rs/macros-impl" } -napi = { git = "https://github.com/napi-rs/napi-rs.git", rev = "ca2cd5c35a0c39ec4a94e93c6c5695b681046df2", default-features = false } -napi-derive = { version = "2.16.5", default-features = false } -napi-build = { version = "2.1.3", default-features = false } +napi = "2.16.8" +napi-derive = "2.16.8" +napi-build = "2.1.3" 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 } +bb8 = { version = "0.8.5", 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 } +idna = { version = "1.0.2", 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 = { version = "0.10.64", default-features = false } pretty_assertions = { version = "1.4.0", default-features = false } -proc-macro2 = { version = "1.0.84", default-features = false } +proc-macro2 = { version = "1.0.86", 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 } +regex = { version = "1.10.5", 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_json = { version = "1.0.120", 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 } +syn = { version = "2.0.68", default-features = false } sysinfo = { version = "0.30.12", default-features = false } thiserror = { version = "1.0.61", default-features = false } -tokio = { version = "1.37.0", 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 } +url = { version = "2.5.2", 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 } +# subdependencies +## explicitly list OpenSSL to use the vendored version +openssl = "0.10.64" + +## some subdependencies require higher Rust version than 1.74 (our MSRV) +## cargo update && cargo update ravif --precise 0.11.5 && cargo update bitstream-io --precise 2.3.0 +## to pin their versions if needed + [profile.release] lto = true diff --git a/Dockerfile b/Dockerfile index 5bad58b949..eea100f4ce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,23 +2,20 @@ FROM docker.io/node:20-alpine as build WORKDIR /firefish +# Copy only backend-rs pnpm-related files first, to cache efficiently +COPY package.json pnpm-workspace.yaml ./ +COPY packages/backend-rs/package.json packages/backend-rs/package.json + # Install compilation dependencies RUN apk update && apk add --no-cache build-base linux-headers curl ca-certificates python3 perl RUN curl --proto '=https' --tlsv1.2 --silent --show-error --fail https://sh.rustup.rs | sh -s -- -y ENV PATH="/root/.cargo/bin:${PATH}" # Copy only backend-rs dependency-related files first, to cache efficiently -COPY package.json pnpm-workspace.yaml ./ -COPY packages/backend-rs/package.json packages/backend-rs/package.json -COPY packages/backend-rs/npm/linux-x64-musl/package.json packages/backend-rs/npm/linux-x64-musl/package.json -COPY packages/backend-rs/npm/linux-arm64-musl/package.json packages/backend-rs/npm/linux-arm64-musl/package.json - -COPY Cargo.toml Cargo.toml -COPY Cargo.lock Cargo.lock -COPY packages/backend-rs/Cargo.toml packages/backend-rs/Cargo.toml +COPY packages/macro-rs packages/macro-rs/ COPY packages/backend-rs/src/lib.rs packages/backend-rs/src/ -COPY packages/macro-rs/Cargo.toml packages/macro-rs/Cargo.toml -COPY packages/macro-rs/src/lib.rs packages/macro-rs/src/ +COPY packages/backend-rs/Cargo.toml packages/backend-rs/Cargo.toml +COPY Cargo.toml Cargo.lock ./ # Configure pnpm, and install backend-rs dependencies RUN corepack enable && corepack prepare pnpm@latest --activate && pnpm --filter backend-rs install @@ -26,10 +23,10 @@ RUN cargo fetch --locked --manifest-path Cargo.toml # Copy in the rest of the rust files COPY packages/backend-rs packages/backend-rs/ -# COPY packages/macro-rs packages/macro-rs/ # Compile backend-rs -RUN NODE_ENV='production' pnpm run --filter backend-rs build +RUN ln -s $(which gcc) /usr/bin/aarch64-linux-musl-gcc +RUN NODE_ENV='production' NODE_OPTIONS='--max_old_space_size=3072' pnpm run --filter backend-rs build # Copy/Overwrite index.js to mitigate the bug in napi-rs codegen COPY packages/backend-rs/index.js packages/backend-rs/built/index.js @@ -49,7 +46,7 @@ RUN pnpm install --frozen-lockfile COPY . ./ # Build other workspaces -RUN NODE_ENV='production' pnpm run --recursive --filter '!backend-rs' build && pnpm run build:assets +RUN NODE_ENV='production' NODE_OPTIONS='--max_old_space_size=3072' pnpm run --recursive --filter '!backend-rs' build && pnpm run build:assets # Trim down the dependencies to only those for production RUN find . -path '*/node_modules/*' -delete && pnpm install --prod --frozen-lockfile diff --git a/Procfile b/Procfile deleted file mode 100644 index fdab9a4c81..0000000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: NODE_ENV=production npm start diff --git a/biome.json b/biome.json index 487165266a..89897aadec 100644 --- a/biome.json +++ b/biome.json @@ -1,28 +1,400 @@ { - "$schema": "https://biomejs.dev/schemas/1.6.4/schema.json", - "organizeImports": { - "enabled": false - }, + "$schema": "https://biomejs.dev/schemas/1.8.2/schema.json", + "organizeImports": { "enabled": false }, "linter": { "enabled": true, "rules": { - "recommended": true, + "recommended": false, + "complexity": { + "noBannedTypes": "error", + "noExtraBooleanCast": "error", + "noMultipleSpacesInRegularExpressionLiterals": "error", + "noUselessCatch": "error", + "noUselessConstructor": "off", + "noUselessLoneBlockStatements": "error", + "noUselessRename": "error", + "noUselessTernary": "error", + "noUselessThisAlias": "error", + "noUselessTypeConstraint": "error", + "noVoid": "error", + "noWith": "error", + "useLiteralKeys": "error", + "useRegexLiterals": "error" + }, + "correctness": { + "noConstAssign": "error", + "noConstantCondition": "error", + "noEmptyCharacterClassInRegex": "error", + "noEmptyPattern": "error", + "noGlobalObjectCalls": "error", + "noInvalidConstructorSuper": "error", + "noInvalidUseBeforeDeclaration": "error", + "noNewSymbol": "error", + "noPrecisionLoss": "error", + "noSelfAssign": "error", + "noSwitchDeclarations": "error", + "noUndeclaredVariables": "error", + "noUnreachable": "error", + "noUnreachableSuper": "error", + "noUnsafeFinally": "error", + "noUnusedVariables": "off", + "useArrayLiterals": "off", + "useIsNan": "error" + }, + "security": { "noGlobalEval": "error" }, "style": { - "noUselessElse": "off" + "noCommaOperator": "error", + "noInferrableTypes": "error", + "noNamespace": "error", + "noNonNullAssertion": "warn", + "noUselessElse": "off", + "noVar": "error", + "useAsConstAssertion": "error", + "useBlockStatements": "off", + "useConst": "error", + "useImportType": "error", + "useSingleVarDeclarator": "warn" + }, + "suspicious": { + "noAssignInExpressions": "error", + "noAsyncPromiseExecutor": "error", + "noCatchAssign": "error", + "noClassAssign": "error", + "noCompareNegZero": "error", + "noConfusingLabels": "off", + "noConsoleLog": "warn", + "noControlCharactersInRegex": "error", + "noDebugger": "warn", + "noDoubleEquals": "error", + "noDuplicateCase": "error", + "noDuplicateClassMembers": "error", + "noDuplicateObjectKeys": "error", + "noDuplicateParameters": "error", + "noEmptyBlockStatements": "error", + "noEmptyInterface": "error", + "noExplicitAny": "warn", + "noExtraNonNullAssertion": "error", + "noFallthroughSwitchClause": "error", + "noFunctionAssign": "error", + "noGlobalAssign": "error", + "noImportAssign": "error", + "noMisleadingCharacterClass": "error", + "noMisleadingInstantiator": "error", + "noPrototypeBuiltins": "off", + "noRedeclare": "error", + "noSelfCompare": "error", + "noShadowRestrictedNames": "error", + "noUnsafeNegation": "error", + "useAwait": "off", + "useDefaultSwitchClauseLast": "error", + "useNamespaceKeyword": "error", + "useValidTypeof": "error" } - } + }, + "ignore": [ + "**/*.json5", + "**/*.min.*", + "**/dist", + "**/LICENSE*", + "**/output", + "**/coverage", + "**/public", + "**/temp", + "**/packages-lock.json", + "**/pnpm-lock.yaml", + "**/yarn.lock", + "**/__snapshots__" + ] + }, + "javascript": { + "globals": [ + "jest", + "withDefaults", + "$computed", + "$shallowRef", + "defineExpose", + "$toRef", + "h", + "$customRef", + "navigator", + "window", + "defineEmits", + "$ref", + "defineProps", + "document" + ] }, "overrides": [ { - "include": ["*.vue", "packages/client/*.ts"], + "include": ["**/__tests__/*.{j,t}s?(x)", "**/*.spec.{j,t}s?(x)"], + "linter": { "rules": { "suspicious": { "noConsoleLog": "off" } } } + }, + { + "include": ["*.vue"], + "linter": { "rules": { "correctness": { "noUnusedVariables": "off" } } } + }, + { + "include": ["**/__tests__/*.{j,t}s?(x)", "**/*.spec.{j,t}s?(x)"], + "linter": { "rules": { "suspicious": { "noConsoleLog": "off" } } } + }, + { "include": ["*.vue"], "linter": { "rules": {} } }, + { "include": ["*.json", "*.json5"], "linter": { "rules": {} } }, + { "include": ["*.yaml", "*.yml"], "linter": { "rules": {} } }, + { "include": ["package.json"], "linter": { "rules": {} } }, + { "include": ["*.d.ts"], "linter": { "rules": {} } }, + { "include": ["*.js"] }, + { + "include": ["scripts/**/*.*", "cli.*"], + "linter": { "rules": { "suspicious": { "noConsoleLog": "off" } } } + }, + { + "include": ["*.test.ts", "*.test.js", "*.spec.ts", "*.spec.js"], + "linter": { "rules": {} } + }, + { + "include": ["**/*.md/*.*"], "linter": { "rules": { + "correctness": { + "noUndeclaredVariables": "off", + "noUnusedVariables": "off" + }, + "suspicious": { "noConsoleLog": "off" } + } + } + }, + { "include": ["*.js"], "linter": { "rules": {} } }, + { + "include": ["**/*.md/*.*"], + "linter": { + "rules": { + "correctness": { + "noInvalidUseBeforeDeclaration": "off", + "noUnusedVariables": "off" + }, + "suspicious": { "noRedeclare": "off" } + } + } + }, + { "include": ["*.json", "*.json5"], "linter": { "rules": {} } }, + { "include": ["*.yaml", "*.yml"], "linter": { "rules": {} } }, + { "include": ["package.json"], "linter": { "rules": {} } }, + { "include": ["*.d.ts"], "linter": { "rules": {} } }, + { "include": ["*.js"] }, + { + "include": ["scripts/**/*.*", "cli.*"], + "linter": { "rules": { "suspicious": { "noConsoleLog": "off" } } } + }, + { + "include": ["*.test.ts", "*.test.js", "*.spec.ts", "*.spec.js"], + "linter": { "rules": {} } + }, + { + "include": ["**/*.md/*.*"], + "linter": { + "rules": { + "correctness": { + "noUndeclaredVariables": "off", + "noUnusedVariables": "off" + }, + "suspicious": { "noConsoleLog": "off" } + } + } + }, + { "include": ["*.js"], "linter": { "rules": {} } }, + { + "include": ["**/*.md/*.*"], + "linter": { + "rules": { + "correctness": { + "noInvalidUseBeforeDeclaration": "off", + "noUnusedVariables": "off" + }, + "suspicious": { "noRedeclare": "off" } + } + } + }, + { + "include": ["*.ts", "*.tsx", "*.mts", "*.cts"], + "linter": { + "rules": { + "correctness": { + "noConstAssign": "off", + "noGlobalObjectCalls": "off", + "noInvalidConstructorSuper": "off", + "noNewSymbol": "off", + "noSetterReturn": "off", + "noUndeclaredVariables": "off", + "noUnreachable": "off", + "noUnreachableSuper": "off" + }, "style": { - "useImportType": "warn", - "useShorthandFunctionType": "warn", - "useTemplate": "warn", - "noNonNullAssertion": "off", - "useNodejsImportProtocol": "off" + "noArguments": "error", + "noVar": "error", + "useConst": "error" + }, + "suspicious": { + "noDuplicateClassMembers": "off", + "noDuplicateObjectKeys": "off", + "noDuplicateParameters": "off", + "noFunctionAssign": "off", + "noImportAssign": "off", + "noRedeclare": "off", + "noUnsafeNegation": "off", + "useGetterReturn": "off", + "useValidTypeof": "off" + } + } + } + }, + { "include": ["*.json", "*.json5"], "linter": { "rules": {} } }, + { "include": ["*.yaml", "*.yml"], "linter": { "rules": {} } }, + { "include": ["package.json"], "linter": { "rules": {} } }, + { "include": ["*.d.ts"], "linter": { "rules": {} } }, + { "include": ["*.js"], "linter": { "rules": {} } }, + { + "include": ["*.ts", "*.tsx", "*.mts", "*.cts"], + "linter": { "rules": { "complexity": { "noVoid": "error" } } } + }, + { + "include": ["script/**/*.*", "scripts/**/*.*", "cli.*"], + "linter": { "rules": { "suspicious": { "noConsoleLog": "off" } } } + }, + { + "include": ["*.test.ts", "*.test.js", "*.spec.ts", "*.spec.js"], + "linter": { "rules": {} } + }, + { + "include": ["**/*.md/*.*"], + "linter": { + "rules": { + "correctness": { + "noInvalidUseBeforeDeclaration": "off", + "noUndeclaredVariables": "off", + "noUnusedVariables": "off" + }, + "style": { "useImportType": "off" }, + "suspicious": { "noConsoleLog": "off", "noRedeclare": "off" } + } + } + }, + { "include": ["*.json", "*.json5"], "linter": { "rules": {} } }, + { "include": ["*.yaml", "*.yml"], "linter": { "rules": {} } }, + { "include": ["package.json"], "linter": { "rules": {} } }, + { "include": ["*.d.ts"], "linter": { "rules": {} } }, + { "include": ["*.js"], "linter": { "rules": {} } }, + { + "include": ["*.ts", "*.tsx", "*.mts", "*.cts"], + "linter": { "rules": { "complexity": { "noVoid": "error" } } } + }, + { + "include": ["script/**/*.*", "scripts/**/*.*", "cli.*"], + "linter": { "rules": { "suspicious": { "noConsoleLog": "off" } } } + }, + { + "include": ["*.test.ts", "*.test.js", "*.spec.ts", "*.spec.js"], + "linter": { "rules": {} } + }, + { + "include": ["**/*.md/*.*"], + "linter": { + "rules": { + "correctness": { + "noInvalidUseBeforeDeclaration": "off", + "noUndeclaredVariables": "off", + "noUnusedVariables": "off" + }, + "style": { "useImportType": "off" }, + "suspicious": { "noConsoleLog": "off", "noRedeclare": "off" } + } + } + }, + { "include": ["*.md"] }, + { + "include": ["**/*.md/**"], + "linter": { + "rules": { + "correctness": { + "noUndeclaredVariables": "off", + "noUnusedVariables": "off" + } + } + } + }, + { + "include": ["*.ts", "*.tsx", "*.mts", "*.cts"], + "linter": { + "rules": { + "correctness": { + "noConstAssign": "off", + "noGlobalObjectCalls": "off", + "noInvalidConstructorSuper": "off", + "noNewSymbol": "off", + "noSetterReturn": "off", + "noUndeclaredVariables": "off", + "noUnreachable": "off", + "noUnreachableSuper": "off" + }, + "style": { + "noArguments": "error", + "noVar": "error", + "useConst": "error" + }, + "suspicious": { + "noDuplicateClassMembers": "off", + "noDuplicateObjectKeys": "off", + "noDuplicateParameters": "off", + "noFunctionAssign": "off", + "noImportAssign": "off", + "noRedeclare": "off", + "noUnsafeNegation": "off", + "useGetterReturn": "off", + "useValidTypeof": "off" + } + } + } + }, + { "include": ["*.md"] }, + { + "include": ["**/*.md/**"], + "linter": { + "rules": { + "correctness": { + "noUndeclaredVariables": "off", + "noUnusedVariables": "off" + } + } + } + }, + { + "include": ["*.ts", "*.tsx", "*.mts", "*.cts"], + "linter": { + "rules": { + "correctness": { + "noConstAssign": "off", + "noGlobalObjectCalls": "off", + "noInvalidConstructorSuper": "off", + "noNewSymbol": "off", + "noSetterReturn": "off", + "noUndeclaredVariables": "off", + "noUnreachable": "off", + "noUnreachableSuper": "off" + }, + "style": { + "noArguments": "error", + "noVar": "error", + "useConst": "error" + }, + "suspicious": { + "noDuplicateClassMembers": "off", + "noDuplicateObjectKeys": "off", + "noDuplicateParameters": "off", + "noFunctionAssign": "off", + "noImportAssign": "off", + "noRedeclare": "off", + "noUnsafeNegation": "off", + "useGetterReturn": "off", + "useValidTypeof": "off" } } } diff --git a/cliff.toml b/cliff.toml deleted file mode 100644 index 8a1e7b1002..0000000000 --- a/cliff.toml +++ /dev/null @@ -1,98 +0,0 @@ -# configuration file for git-cliff (0.1.0) - -[changelog] -# changelog header -header = """ -# Changelog\n -""" -# template for the changelog body -# https://tera.netlify.app/docs/#introduction -body = """ -{% if version %}\ - ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} -{% else %}\ - ## [unreleased] -{% endif %}\ -{% for group, commits in commits | group_by(attribute="group") %} - ### {{ group | upper_first }} - {% for commit in commits %} - - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\ - {% endfor %} -{% endfor %}\n -""" -# remove the leading and trailing whitespace from the template -trim = true -# changelog footer -footer = """ - -""" - -[git] -# parse the commits based on https://www.conventionalcommits.org -conventional_commits = false -# filter out the commits that are not conventional -filter_unconventional = true -# process each line of a commit as an individual commit -split_commits = false -# regex for parsing and grouping commits -commit_parsers = [ - { message = "^feat", group = "Features"}, - { message = "^add", group = "Features"}, - { message = "^fix", group = "Bug Fixes"}, - { message = "^prevent", group = "Bug Fixes"}, - { message = "^doc", group = "Documentation"}, - { message = "^perf", group = "Performance"}, - { message = "^🎨", group = "Refactor"}, - { message = "^enhance", group = "Refactor"}, - { message = "^⚡️", group = "Refactor"}, - { message = "^🔥", group = "Features"}, - { message = "^🐛", group = "Bug Fixes"}, - { message = "^🚑️", group = "Bug Fixes"}, - { message = "^block", group = "Bug Fixes"}, - { message = "^✨", group = "Features"}, - { message = "^📝", group = "Documentation"}, - { message = "^🚀", group = "Features"}, - { message = "^💄", group = "Styling"}, - { message = "^✅", group = "Testing"}, - { message = "^🔒️", group = "Security"}, - { message = "^🚨", group = "Testing"}, - { message = "^💚", group = "CI"}, - { message = "^👷", group = "CI"}, - { message = "^⬇️", group = "Miscellaneous Tasks"}, - { message = "^⬆️", group = "Miscellaneous Tasks"}, - { message = "^📌", group = "Miscellaneous Tasks"}, - { message = "^➕", group = "Miscellaneous Tasks"}, - { message = "^➖", group = "Miscellaneous Tasks"}, - { message = "^♻️", group = "Refactor"}, - { message = "^🔧", group = "CI"}, - { message = "^🔨", group = "CI"}, - { message = "^🌐", group = "Localization"}, - { message = "^✏️", group = "Localization"}, - { message = "^👽️", group = "Bug Fixes"}, - { message = "^🍱", group = "Styling"}, - { message = "^♿️", group = "Styling"}, - { message = "^🩹", group = "Bug Fixes"}, - { message = "^refactor", group = "Refactor"}, - { message = "^style", group = "Styling"}, - { message = "^test", group = "Testing"}, - { message = "^chore\\(release\\): prepare for", skip = true}, - { message = "^chore", group = "Miscellaneous Tasks"}, - { message = "^update", group = "Miscellaneous Tasks"}, - { body = ".*security", group = "Security"}, -] -# protect breaking changes from being skipped due to matching a skipping commit_parser -protect_breaking_commits = false -# filter out the commits that are not matched by commit parsers -filter_commits = false -# glob pattern for matching git tags -tag_pattern = "v[0-9]*" -# regex for skipping tags -skip_tags = "v0.1.0-beta.1" -# regex for ignoring tags -ignore_tags = "" -# sort the tags chronologically -date_order = false -# sort the commits inside sections by oldest/newest order -sort_commits = "oldest" -# limit the number of commits included in the changelog. -# limit_commits = 42 diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 9cd6d1cdce..4903205b93 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -27,10 +27,10 @@ services: interval: 5s timeout: 5s retries: 5 - deploy: - resources: - limits: - memory: 4096M + # deploy: + # resources: + # limits: + # memory: 2048M redis: restart: unless-stopped @@ -45,11 +45,10 @@ services: interval: 5s timeout: 5s retries: 5 - # deploy: - # resources: - # limits: - # memory: 200M - + # deploy: + # resources: + # limits: + # memory: 256M db: restart: unless-stopped @@ -66,10 +65,10 @@ services: interval: 5s timeout: 5s retries: 5 - # deploy: - # resources: - # limits: - # memory: 200M + # deploy: + # resources: + # limits: + # memory: 2048M networks: calcnet: diff --git a/docs/api-change.md b/docs/api-change.md index 91b20d76f2..2ae2275707 100644 --- a/docs/api-change.md +++ b/docs/api-change.md @@ -2,6 +2,10 @@ Breaking changes are indicated by the :warning: icon. +## v20240607 + +- `GET` request is now allowed for the `latest-version` endpoint. + ## v20240523 - Added `scheduledAt` optional parameter to `notes/create` (!10789) diff --git a/docs/changelog.md b/docs/changelog.md index 4b7d11bd05..513370dd63 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,9 +2,37 @@ Critical security updates are indicated by the :warning: icon. -- Server administrators should check [notice-for-admins.md](./notice-for-admins.md) as well. +- Server administrators must 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 + +- Fix bugs + +## [v20240630](https://firefish.dev/firefish/firefish/-/merge_requests/11072/commits) + +- Add ability to automatically append #Alt4Me hashtag when posting a file without an alt text ([What is #Alt4Me?](https://social.growyourown.services/@FediTips/112055775451305236)) +- Fix a build issue on some environments +- Fix bugs + +## [v20240623](https://firefish.dev/firefish/firefish/-/merge_requests/11049/commits) + +- Fix bugs + +## [v20240613](https://firefish.dev/firefish/firefish/-/merge_requests/11003/commits) + +This update contains code refactoring and dependency updates, with no major user-facing changes. + +## [v20240607](https://firefish.dev/firefish/firefish/-/merge_requests/10978/commits) + +- 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 diff --git a/docs/downgrade.sql b/docs/downgrade.sql index e7de398a22..895046087a 100644 --- a/docs/downgrade.sql +++ b/docs/downgrade.sql @@ -1,6 +1,7 @@ BEGIN; DELETE FROM "migrations" WHERE name IN ( + 'RefactorScheduledPosts1716804636187', 'RemoveEnumTypenameSuffix1716462794927', 'CreateScheduledNote1714728200194', 'AddBackTimezone1715351290096', @@ -33,6 +34,38 @@ DELETE FROM "migrations" WHERE name IN ( 'RemoveNativeUtilsMigration1705877093218' ); +-- refactor-scheduled-post +CREATE TABLE "scheduled_note" ( + "id" character varying(32) NOT NULL PRIMARY KEY, + "noteId" character varying(32) NOT NULL, + "userId" character varying(32) NOT NULL, + "scheduledAt" TIMESTAMP WITH TIME ZONE NOT NULL +); +COMMENT ON COLUMN "scheduled_note"."noteId" IS 'The ID of the temporarily created note that corresponds to the schedule.'; +CREATE EXTENSION pgcrypto; +CREATE FUNCTION generate_scheduled_note_id(size int) RETURNS text AS $$ DECLARE + characters text := 'abcdefghijklmnopqrstuvwxyz0123456789'; + bytes bytea := gen_random_bytes(size); + l int := length(characters); + i int := 0; + output text := ''; + BEGIN + WHILE i < size LOOP + output := output || substr(characters, get_byte(bytes, i) % l + 1, 1); + i := i + 1; + END LOOP; + RETURN output; + END; +$$ LANGUAGE plpgsql VOLATILE; +INSERT INTO "scheduled_note" ("id", "noteId", "userId", "scheduledAt") (SELECT generate_scheduled_note_id(16), "id", "userId", "scheduledAt" FROM "note" WHERE "note"."scheduledAt" IS NOT NULL); +DROP EXTENSION pgcrypto; +DROP FUNCTION "generate_scheduled_note_id"; +CREATE INDEX "IDX_noteId_ScheduledNote" ON "scheduled_note" ("noteId"); +CREATE INDEX "IDX_userId_ScheduledNote" ON "scheduled_note" ("userId"); +ALTER TABLE "scheduled_note" ADD FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION; +ALTER TABLE "scheduled_note" ADD FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION; +ALTER TABLE "note" DROP COLUMN "scheduledAt"; + -- remove-enum-typename-suffix ALTER TYPE "antenna_src" RENAME TO "antenna_src_enum"; ALTER TYPE "drive_file_usage_hint" RENAME TO "drive_file_usage_hint_enum"; diff --git a/firefish.nginx.conf b/docs/firefish.nginx.conf similarity index 100% rename from firefish.nginx.conf rename to docs/firefish.nginx.conf diff --git a/docs/install.md b/docs/install.md index 80dffdaeb9..6a2079fd73 100644 --- a/docs/install.md +++ b/docs/install.md @@ -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,27 +15,78 @@ 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 - At least [Rust](https://www.rust-lang.org/) v1.74 -- C/C++ compiler & build tools +- C/C++ compiler & build tools (like [GNU Make](https://www.gnu.org/software/make/)) - `build-essential` on Debian/Ubuntu Linux - `base-devel` on Arch Linux + - `"Development Tools"` on Fedora/Red Hat Linux - [Python 3](https://www.python.org/) - [Perl](https://www.perl.org/) This document shows an example procedure for installing these dependencies and Firefish on Debian 12. Note that there is much room for customizing the server setup; this document merely demonstrates a simple installation. +### Install on non-Linux systems + +We don't test Firefish on non-Linux systems, so please install Firefish on such an environment **only if you can address any problems yourself**. There is absolutely no support. That said, it is possible to install Firefish on some non-Linux systems. + +
+ +Possible setup on FreeBSD (as of version `20240630`) + +You can install Firefish on FreeBSD by adding these extra steps to the standard instructions: + +1. Install `vips` package +2. Add the following block to [`package.json`](../package.json) + ```json + "pnpm": { + "overrides": { + "rollup": "npm:@rollup/wasm-node@4.17.2" + } + } + ``` +3. Create an rc script for Firefish + ```sh + #!/bin/sh + + # PROVIDE: firefish + # REQUIRE: DAEMON redis caddy postgresql + # KEYWORD: shutdown + + . /etc/rc.subr + + name=firefish + rcvar=firefish_enable + + desc="Firefish daemon" + + load_rc_config ${name} + + : ${firefish_chdir:="/path/to/firefish/local/repository"} + : ${firefish_env:="npm_config_cache=/tmp NODE_ENV=production NODE_OPTIONS=--max-old-space-size=3072"} + + pidfile="/var/run/${name}.pid" + command=/usr/sbin/daemon + command_args="-f -S -u firefish -P ${pidfile} /usr/local/bin/pnpm run start" + + run_rc_command "$1" + ``` + +
+ +Please let us know if you deployed Firefish on a curious environment :smile: + +### Use Docker/Podman containers + 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. +## 1. Install dependencies Make sure that you can use the `sudo` command before proceeding. -## 1. Install dependencies - ### Utilities ```sh @@ -215,7 +266,7 @@ sudo ufw status ### 2. Set up a reverse proxy -In this instruction, we use [Caddy](https://caddyserver.com/) to make the Firefish server accesible from internet. However, you can also use [Nginx](https://nginx.org/en/) if you want ([example Nginx config file](../firefish.nginx.conf)). +In this instruction, we use [Caddy](https://caddyserver.com/) to make the Firefish server accesible from internet. However, you can also use [Nginx](https://nginx.org/en/) if you want ([example Nginx config file](./firefish.nginx.conf)). 1. Install Caddy ```sh @@ -310,7 +361,9 @@ In this instruction, we use [Caddy](https://caddyserver.com/) to make the Firefi sudo systemctl enable --now firefish ``` -## Upgrading +# Maintain the server + +## Upgrade Firefish version Please refer to the [upgrade instruction](./upgrade.md). Be sure to switch to `firefish` user and go to the Firefish directory before executing the `git` command: @@ -319,6 +372,85 @@ sudo su --login firefish cd ~/firefish ``` +## Rotate logs + +As the server runs longer and longer, the size of the log files increases, filling up the disk space. To prevent this, you should set up a log rotation (removing old logs automatically). + +You can edit the `SystemMaxUse` value in the `[journal]` section of `/etc/systemd/journald.conf` to do it: + +```conf +[journal] +... (omitted) +SystemMaxUse=500M +... +``` + +Make sure to remove the leading `#` to uncomment the line. After editing the config file, you need to restart `systemd-journald` service. + +```sh +sudo systemctl restart systemd-journald +``` + +It is also recommended that you change the [PGroonga log level](https://pgroonga.github.io/reference/parameters/log-level.html). The default level is `notice`, but this is too verbose for daily use. + +To control the log level, add this line to your `postgresql.conf`: + +```conf +pgroonga.log_level = error +``` + +You can check the `postgresql.conf` location by this command: + +```sh +sudo --user=postgres psql --command='SHOW config_file' +``` + +The PGroonga log file (`pgroonga.log`) is located under this directory: + +```sh +sudo --user=postgres psql --command='SHOW data_directory' +``` + +## Tune database configuration + +The default PostgreSQL configuration not suitable for running a Firefish server. Thus, it is highly recommended that you use [PGTune](https://pgtune.leopard.in.ua/) to tweak the configuration. + +Here is an example set of parameters you can provide to PGTune: + +| Parameter | Value | +|----------------------:|---------------------------------------------------------| +| DB version | 16 (your PostgreSQL major version) | +| OS Type | Linux | +| DB Type | Data warehouse | +| Total Memory | [total physical memory] minus 700 MB | +| Number of CPUs | number of CPU threads (or lower value if you have many) | +| Number of connections | 200 | +| Data storage | SSD storage | + +Since this is not a dedicated database server, be sure to leave some memory space for other software such as Firefish and Redis. + +Once you have entered the appropriate values for your environment, click the "Generate" button to generate a configuration and replace the values in `postgresql.conf` with the suggested values. + +After that, you need to restart the PostgreSQL service. + +```sh +sudo systemctl stop firefish +sudo systemctl restart postgresql +sudo systemctl start firefish +``` + +## VACUUM your database + +If the database runs long, accumulated "garbage" can degrade its performance or cause problems. To prevent this, you should `VACUUM` your database regularly. + +```sh +sudo systemctl stop firefish +sudo --user=postgres psql --dbname=firefish_db --command='VACUUM FULL VERBOSE ANALYZE' +sudo systemctl start firefish +``` + +Note that this operation takes some time. + ## Customize - To add custom CSS for all users, edit `./custom/assets/instance.css`. diff --git a/docs/notice-for-admins.md b/docs/notice-for-admins.md index 5471832c83..fbc58623fe 100644 --- a/docs/notice-for-admins.md +++ b/docs/notice-for-admins.md @@ -2,8 +2,29 @@ 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). +## Upcoming breaking change (unreleased) + +Please take a look at #10947. + ## Unreleased +### For all users + +This is not related to the recent changes, but we have added a new section called "[Maintain the server](https://firefish.dev/firefish/firefish/-/blob/develop/docs/install.md#maintain-the-server)" in the installation guide. We suggest that you take a look at it. (and we welcome your docs contributions!) + +## v20240607 + +The following environment variables are deprecated and no longer have any effect: +- `MK_ONLY_QUEUE` +- `MK_ONLY_SERVER` +- `MK_NO_DAEMONS` +- `MK_DISABLE_CLUSTERING` +- `MK_VERBOSE` +- `MK_WITH_LOG_TIME` +- `MK_SLOW` + +## v20240601 + ### For systemd/pm2 users Required Node.js version has been bumped from v18.17.0 to v18.19.0. Also, as written in the [v20240430 note](https://firefish.dev/firefish/firefish/-/blob/d3394b97f021dea323ec3ae36e39930680242482/docs/notice-for-admins.md#v20240430), it is highly recommended that you use an even newer version since v18.19.0 has known vulnerabilities. @@ -32,7 +53,7 @@ Therefore, we have contributed to napi-rs to add support for `DateTime)). +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]()). To check your Linux kernel version, run: diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index 622074056c..6329fbb8b6 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -446,7 +446,7 @@ tooShort: "قصير جدًا" tooLong: "طويل جدًا" weakPassword: "الكلمة السرية ضعيفة" normalPassword: "الكلمة السرية جيدة" -strongPassword: "الكلمة السرية قوية" +veryStrongPassword: "الكلمة السرية قوية" passwordMatched: "التطابق صحيح!" passwordNotMatched: "غير متطابقتان" signinWith: "الولوج عبر {x}" diff --git a/locales/bg-BG.yml b/locales/bg-BG.yml index 86eeb6691c..03009bf588 100644 --- a/locales/bg-BG.yml +++ b/locales/bg-BG.yml @@ -586,7 +586,7 @@ unavailable: Не е свободно tooShort: Твърде кратко tooLong: Твърде дълго weakPassword: Слаба парола -strongPassword: Силна парола +veryStrongPassword: Силна парола passwordMatched: Съвпада passwordNotMatched: Не съвпада signinWith: Вход с {x} diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index d17766b499..db3874ceb9 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -462,7 +462,7 @@ tooShort: "খুব ছোট" tooLong: "খুব বড়" weakPassword: "দুর্বল পাসওয়ার্ড" normalPassword: "সাধারণ পাসওয়ার্ড" -strongPassword: "শক্তিশালী পাসওয়ার্ড" +veryStrongPassword: "শক্তিশালী পাসওয়ার্ড" passwordMatched: "মিলেছে" passwordNotMatched: "মিলেনি" signinWith: "{x} এর সাহায্যে সাইন ইন করুন" diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 409b2e4de6..4e4f96cbb2 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -395,7 +395,7 @@ _profile: metadataLabel: Etiqueta metadataContent: Contingut changeAvatar: Canvia l'avatar - changeBanner: Canvia el banner + changeBanner: Canvia el bàner locationDescription: Si primer introduïu la vostra ciutat, es mostrarà l'hora local a altres usuaris. name: Nom @@ -1022,7 +1022,7 @@ yearsOld: '{age} anys' copyUrl: Copia l'adreça URL rename: Renombra unwatch: Deixa de veure -accept: Accepta +accept: Acceptar reject: Rebutja yearX: '{year}' pages: Pàgines @@ -1091,7 +1091,7 @@ usernameInvalidFormat: Pots fer servir lletres en majúscules o minúscules, nom tooShort: Massa curt tooLong: Massa llarg weakPassword: Contrasenya amb seguretat feble -strongPassword: Contrasenya amb seguretat forta +veryStrongPassword: Contrasenya amb seguretat forta passwordMatched: Coincidències signinWith: Inicia sessió com {x} signinFailed: No es pot iniciar sessió. El nom d'usuari o la contrasenya són incorrectes. @@ -1114,7 +1114,7 @@ createAccount: Crea un compte fontSize: Mida del text noFollowRequests: No tens cap sol·licitud de seguiment per aprovar openImageInNewTab: Obre les imatges a una pestanya nova -dashboard: Tauler +dashboard: Taulell local: Local remote: Remot total: Total @@ -1551,7 +1551,7 @@ itsOn: Activat itsOff: Desactivat emailRequiredForSignup: Requereix una adreça de correu electrònic per registrar-te unread: Sense llegir -controlPanel: Tauler de control +controlPanel: Taulell de control manageAccounts: Gestionar comptes makeReactionsPublic: Estableix l'historial de reaccions com a públic classic: Centrat @@ -2233,7 +2233,7 @@ enablePullToRefresh: Activa "Baixa per actualitzar" pullDownToReload: Baixa per actualitzar pullToRefreshThreshold: Distancia de baixada per actualitzar searchWords: Paraules / ID o adreça URL que vols cercar -noSentFollowRequests: No tens cap sol·licitud de seguiment enviada +noSentFollowRequests: No has enviat cap sol·licitud de seguiment sentFollowRequests: Enviar sol·licituds de seguiment replyMute: Silencia les respostes a les línies de temps replyUnmute: Treu el silencia de les respostes a les línies de temps @@ -2311,7 +2311,7 @@ _later: future: futur justNow: ara mateix secondsAgo: en {n}s - minutesAgo: en {n}m + minutesAgo: en {n}min daysAgo: en {n}d weeksAgo: en {n}s monthsAgo: en {n}me @@ -2321,3 +2321,6 @@ scheduledDate: Publica el scheduledPost: Programa aquesta publicació scheduledPostAt: Aquesta publicació s'enviarà {time} cancelScheduledPost: Elimina la planificació +addAlt4MeTag: "Afegeix automàticament l'etiqueta #Alt4Me a les teves publicacions + que tinguin un fitxer adjunt sense descripció" +strongPassword: Bona contrasenya diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index 96107996cf..8a08c784c8 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -401,7 +401,7 @@ tooShort: "Příliš krátké" tooLong: "Příliš dlouhé" weakPassword: "Slabé heslo" normalPassword: "Dobré heslo" -strongPassword: "Silné heslo" +veryStrongPassword: "Silné heslo" passwordMatched: "Hesla se schodují" passwordNotMatched: "Hesla se neschodují" signinWith: "Přihlásit se s {x}" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 012f1ef3ea..f6093b823d 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -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" @@ -480,7 +480,7 @@ tooShort: "Zu kurz" tooLong: "Zu lang" weakPassword: "Schwaches Passwort" normalPassword: "Durchschnittliches Passwort" -strongPassword: "Starkes Passwort" +veryStrongPassword: "Starkes Passwort" passwordMatched: "Stimmt überein" passwordNotMatched: "Stimmt nicht überein" signinWith: "Mit {x} anmelden" @@ -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,131 @@ 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: 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 +addAlt4MeTag: 'Automatisch den Hashtag #Alt4Me am Ende deines Beitrags einfügen, wenn + eine angehängte Datei keine Beschreibung hat' diff --git a/locales/el-GR.yml b/locales/el-GR.yml index 2d2b657e01..f5fcc86755 100644 --- a/locales/el-GR.yml +++ b/locales/el-GR.yml @@ -677,7 +677,7 @@ checking: Έλεγχος... invitationCode: Κωδικός πρόσκλησης normalPassword: Μέτριος κωδικός weakPassword: Αδύναμος κωδικός -strongPassword: Δυνατός κωδικός +veryStrongPassword: Δυνατός κωδικός signinWith: Συνδεθείτε με {x} tapSecurityKey: Βάλτε το κλειδί ασφάλειας signinFailed: Αδυναμία σύνδεσης. Το όνομα μέλους ή ο κωδικός είναι λάθος. diff --git a/locales/en-US.yml b/locales/en-US.yml index 12c0062b22..261b0d5673 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -514,8 +514,9 @@ usernameInvalidFormat: "You can use upper- and lowercase letters, numbers, and u tooShort: "Too short" tooLong: "Too long" weakPassword: "Weak password" -normalPassword: "Average password" -strongPassword: "Strong password" +normalPassword: "Medium password" +strongPassword: "Good password" +veryStrongPassword: "Great password" passwordMatched: "Matches" passwordNotMatched: "Does not match" signinWith: "Sign in with {x}" @@ -542,7 +543,7 @@ existingAccount: "Existing account" regenerate: "Regenerate" fontSize: "Font size" noFollowRequests: "You don't have any pending follow requests" -noSentFollowRequests: "You don't have any sent follow requests" +noSentFollowRequests: "You haven't sent any follow requests" openImageInNewTab: "Open images in new tab" dashboard: "Dashboard" local: "Local" @@ -1236,8 +1237,9 @@ 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" +addAlt4MeTag: "Automatically append #Alt4Me hashtag to your post if attached file has no description" _emojiModPerm: unauthorized: "None" @@ -1590,7 +1592,7 @@ _later: future: "future" justNow: "right now" secondsAgo: "in {n}s" - minutesAgo: "in {n}m" + minutesAgo: "in {n}min" hoursAgo: "in {n}h" daysAgo: "in {n}d" weeksAgo: "in {n}w" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 9980607521..2b45b41ff7 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -465,7 +465,7 @@ tooShort: "Demasiado corto" tooLong: "Demasiado largo" weakPassword: "Contraseña débil" normalPassword: "Buena contraseña" -strongPassword: "Muy buena contraseña" +veryStrongPassword: "Muy buena contraseña" passwordMatched: "Correcto" passwordNotMatched: "Las contraseñas no son las mismas" signinWith: "Inicie sesión con {x}" @@ -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 diff --git a/locales/fi.yml b/locales/fi.yml index 123498d6da..c333452f88 100644 --- a/locales/fi.yml +++ b/locales/fi.yml @@ -569,7 +569,7 @@ tooShort: Liian lyhyt tooLong: Liian pitkä weakPassword: Heikko salasana normalPassword: Kohtalainen salasana -strongPassword: Vahva salasana +veryStrongPassword: Vahva salasana passwordMatched: Vastaa signinWith: Kirjaudu sisään {x} signinFailed: Ei voitu kirjautua sisään. Annettu käyttäjänimi tai salasana virheellinen. diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index 3780db91fc..44cec380ca 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -468,7 +468,7 @@ tooShort: "Trop court" tooLong: "Trop long" weakPassword: "Mot de passe faible" normalPassword: "Mot de passe acceptable" -strongPassword: "Mot de passe fort" +veryStrongPassword: "Mot de passe fort" passwordMatched: "Les mots de passe correspondent" passwordNotMatched: "Les mots de passe ne correspondent pas" signinWith: "Se connecter avec {x}" diff --git a/locales/id-ID.yml b/locales/id-ID.yml index b06f906000..325941779d 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -464,7 +464,7 @@ tooShort: "Terlalu pendek" tooLong: "Terlalu panjang" weakPassword: "Kata sandi lemah" normalPassword: "Kata sandi baik" -strongPassword: "Kata sandi kuat" +veryStrongPassword: "Kata sandi kuat" passwordMatched: "Kata sandi sama" passwordNotMatched: "Kata sandi tidak sama" signinWith: "Masuk dengan {x}" diff --git a/locales/it-IT.yml b/locales/it-IT.yml index e686df5890..8ecfd09bdc 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -454,7 +454,7 @@ tooShort: "Troppo breve" tooLong: "Troppo lungo" weakPassword: "Password debole" normalPassword: "Password buona" -strongPassword: "Password forte" +veryStrongPassword: "Password forte" passwordMatched: "Corretta" passwordNotMatched: "Le password non corrispondono" signinWith: "Accedi con {x}" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 3881a1af89..b9b2d6745d 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -465,7 +465,7 @@ tooShort: "短すぎます" tooLong: "長すぎます" weakPassword: "弱いパスワード" normalPassword: "普通のパスワード" -strongPassword: "強いパスワード" +veryStrongPassword: "とても強いパスワード" passwordMatched: "一致しました" passwordNotMatched: "一致していません" signinWith: "{x}でログイン" @@ -2087,3 +2087,5 @@ scheduledPost: 予約投稿 scheduledDate: 予定日 cancelScheduledPost: 予約を解除する scheduledPostAt: '{time}に投稿されます' +strongPassword: 強いパスワード +addAlt4MeTag: '説明の無いファイルを投稿する際に自動で #Alt4Me のハッシュタグをつける' diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 970a27d0ed..941135e8cf 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -435,7 +435,7 @@ tooShort: "短すぎやろ!" tooLong: "長すぎやろ!" weakPassword: "へぼいパスワード" normalPassword: "普通のパスワード" -strongPassword: "ええ感じのパスワード" +veryStrongPassword: "ええ感じのパスワード" passwordMatched: "よし!一致や!" passwordNotMatched: "一致しとらんで?" signinWith: "{x}でログイン" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index d67e37d4cf..bad1f8ee80 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -442,7 +442,7 @@ tooShort: "너무 짧습니다" tooLong: "너무 깁니다" weakPassword: "약한 비밀번호" normalPassword: "좋은 비밀번호" -strongPassword: "강한 비밀번호" +veryStrongPassword: "강한 비밀번호" passwordMatched: "일치합니다" passwordNotMatched: "일치하지 않습니다" signinWith: "{x}로 로그인" diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index 9d120ccf79..6079d7624b 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -576,7 +576,7 @@ quoteAttached: Quote noMessagesYet: Nog geen berichten weakPassword: Zwak wachtwoord normalPassword: Middelmatig wachtwoord -strongPassword: Sterk wachtwoord +veryStrongPassword: Sterk wachtwoord onlyOneFileCanBeAttached: Je kan maar één bestand toevoegen aan je bericht invitationCode: Uitnodigingscode checking: Controleren... diff --git a/locales/no-NO.yml b/locales/no-NO.yml index b446ff4359..adafb8face 100644 --- a/locales/no-NO.yml +++ b/locales/no-NO.yml @@ -571,7 +571,7 @@ youHaveNoGroups: Du har ingen grupper noHistory: Ingen historikk er tilgjengelig aboutX: Om {x} signinHistory: Innloggings-historikk -strongPassword: Sterkt passord +veryStrongPassword: Sterkt passord noFollowRequests: Du har ingen utestående følgeforespørsler openImageInNewTab: Åpne bilder i ny fane dashboard: Dashbord diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 43eeb3c8f0..ec27ccbe24 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -454,7 +454,7 @@ tooShort: "Zbyt krótka" tooLong: "Zbyt długa" weakPassword: "Słabe hasło" normalPassword: "Dobre hasło" -strongPassword: "Silne hasło" +veryStrongPassword: "Silne hasło" passwordMatched: "Pasuje" passwordNotMatched: "Hasła nie pasują do siebie" signinWith: "Zaloguj się z {x}" diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml index a0392abc98..acf4084403 100644 --- a/locales/ro-RO.yml +++ b/locales/ro-RO.yml @@ -464,7 +464,7 @@ tooShort: "Prea scurt" tooLong: "Prea lung" weakPassword: "Parolă slabă" normalPassword: "Parolă medie" -strongPassword: "Parolă puternică" +veryStrongPassword: "Parolă puternică" passwordMatched: "Se potrivește!" passwordNotMatched: "Nu se potrivește" signinWith: "Autentifică-te cu {x}" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 5c81e19a04..e360b5ae27 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -460,7 +460,7 @@ tooShort: "Слишком короткий" tooLong: "Слишком длинный" weakPassword: "Слабый пароль" normalPassword: "Годный пароль" -strongPassword: "Надёжный пароль" +veryStrongPassword: "Надёжный пароль" passwordMatched: "Совпали" passwordNotMatched: "Не совпадают" signinWith: "Использовать {x} для входа" @@ -2137,3 +2137,15 @@ replies: Ответы quotes: Цитаты clickToShowPatterns: Нажмите, чтобы показать модуль шаблонов renotes: Репосты +markLocalFilesNsfwByDefaultDescription: Независимо от данной настройки, пользователи + могут самостоятельно удалять метку NSFW. Не применяется на существующие файлы. +toEdit: Редактировать +attachedToNotes: Посты с этим файлом +showAttachedNotes: Показывать посты с этим файлом +strongPassword: Хороший пароль +toReply: Ответить +toPost: Выложить +sentFollowRequests: Отправленные запросы на подписку +toQuote: Цитировать +cannotEditVisibility: Вы не можете изменить видимость +noSentFollowRequests: Вы не отправляли никаких запросов на подписку diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index e1ed198e4e..5d9e535491 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -461,7 +461,7 @@ tooShort: "Príliš krátke" tooLong: "Príliš dlhé" weakPassword: "Slabé heslo" normalPassword: "Dobré heslo" -strongPassword: "Silné heslo" +veryStrongPassword: "Silné heslo" passwordMatched: "Heslá sú rovnaké" passwordNotMatched: "Heslá nie sú rovnaké" signinWith: "Prihlásiť sa použitím {x}" diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml index 5e7ed1d768..13cb3c66ec 100644 --- a/locales/sv-SE.yml +++ b/locales/sv-SE.yml @@ -381,7 +381,7 @@ noMessagesYet: Inga meddelande ännu newMessageExists: Det finns inga nya meddelanden weakPassword: Svagt lösenord normalPassword: Dugligt lösenord -strongPassword: Starkt lösenord +veryStrongPassword: Starkt lösenord passwordMatched: Matchar passwordNotMatched: Matchar inte signinWith: Logga in med {x} diff --git a/locales/th-TH.yml b/locales/th-TH.yml index 4a668f910a..93904a1125 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -452,7 +452,7 @@ tooShort: "สั้นเกินไปนะ" tooLong: "ยาวเกินไปนะ" weakPassword: "รหัสผ่าน แย่มาก" normalPassword: "รหัสผ่านปกติ" -strongPassword: "รหัสผ่านรัดกุมมาก" +veryStrongPassword: "รหัสผ่านรัดกุมมาก" passwordMatched: "ถูกต้อง!" passwordNotMatched: "ไม่ถูกต้อง" signinWith: "ลงชื่อเข้าใช้ด้วย {x}" diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml index cb36a6a07c..66bfe28133 100644 --- a/locales/tr-TR.yml +++ b/locales/tr-TR.yml @@ -521,7 +521,7 @@ newMessageExists: Yeni mesaj yok invitations: Davetler invitationCode: Davet kodu signinWith: '{x} ile giriş yap' -strongPassword: Güçlü şifre +veryStrongPassword: Güçlü şifre passwordNotMatched: Uyuşmuyor signinFailed: Giriş yapılamadı. Şifre ve ya kullanıcı adı yanlış. tapSecurityKey: Güvenlik anahtarınıza dokunun diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index aa669f596f..206a52ce95 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -460,7 +460,7 @@ tooShort: "Занадто короткий" tooLong: "Занадто довгий" weakPassword: "Слабкий пароль" normalPassword: "Достатній пароль" -strongPassword: "Міцний пароль" +veryStrongPassword: "Міцний пароль" passwordMatched: "Все вірно" passwordNotMatched: "Паролі не співпадають" signinWith: "Увійти за допомогою {x}" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index 4c6a01f1ae..f24ef9a864 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -462,7 +462,7 @@ tooShort: "Quá ngắn" tooLong: "Quá dài" weakPassword: "Mật khẩu yếu" normalPassword: "Mật khẩu tạm được" -strongPassword: "Mật khẩu mạnh" +veryStrongPassword: "Mật khẩu mạnh" passwordMatched: "Trùng khớp" passwordNotMatched: "Không trùng khớp" signinWith: "Đăng nhập bằng {x}" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 5b888cad44..5cf4c8883f 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -450,7 +450,7 @@ tooShort: "太短" tooLong: "太长" weakPassword: "密码强度:弱" normalPassword: "密码强度:中等" -strongPassword: "密码强度:强" +veryStrongPassword: "密码强度:强" passwordMatched: "密码一致" passwordNotMatched: "密码不一致" signinWith: "以 {x} 登录" @@ -1950,7 +1950,7 @@ noteId: 帖子 ID moveFrom: 从旧账号迁移至此账号 defaultReaction: 发出和收到帖子的默认表情符号反应 sendModMail: 发送管理通知 -moderationNote: "管理笔记" +moderationNote: "管理员备注" ipFirstAcknowledged: "首次获取此 IP 地址的日期" driveCapacityOverride: "网盘容量变更" isLocked: 该账号设置了关注请求 diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 00a7827203..a153513f03 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -448,7 +448,7 @@ tooShort: "過短" tooLong: "過長" weakPassword: "密碼強度過弱" normalPassword: "密碼強度普通" -strongPassword: "密碼強度高" +veryStrongPassword: "密碼強度高" passwordMatched: "密碼一致" passwordNotMatched: "密碼不一致" signinWith: "以{x}登錄" diff --git a/package.json b/package.json index 4d0ab3fda8..1f6e3c65d3 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "firefish", - "version": "20240523", + "version": "20240630", "repository": { "type": "git", "url": "https://firefish.dev/firefish/firefish.git" }, - "packageManager": "pnpm@9.1.4", + "packageManager": "pnpm@9.4.0", "private": true, "scripts": { "rebuild": "pnpm run clean && pnpm run build", @@ -22,13 +22,14 @@ "dev": "pnpm node ./scripts/dev.mjs", "dev:staging": "NODE_OPTIONS=--max_old_space_size=3072 NODE_ENV=development pnpm run build && pnpm run start", "lint": "pnpm run lint:ts; pnpm run lint:rs", - "lint:ts": "pnpm --filter !firefish-js -r --parallel run lint", + "lint:ts": "pnpm --filter !firefish-js --recursive run lint ; pnpm run format:ts", "lint:rs": "cargo clippy --fix --allow-dirty --allow-staged && cargo fmt --all --", "debug": "pnpm run build:debug && pnpm run start", "mocha": "pnpm --filter backend run mocha", - "test": "pnpm run test:ts && pnpm run test:rs", + "test": "pnpm run test:rs && pnpm run test:rs:miri && pnpm run test:ts", "test:ts": "pnpm run mocha", "test:rs": "cargo test --doc && cargo nextest run", + "test:rs:miri": "MIRIFLAGS='-Zmiri-disable-isolation' cargo +nightly miri nextest run -j$(nproc --all)", "format": "pnpm run format:ts; pnpm run format:rs", "format:ts": "pnpm -r --parallel run format", "format:rs": "cargo fmt --all --", @@ -41,14 +42,13 @@ "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.13", - "execa": "9.1.0", - "pnpm": "9.1.4", - "typescript": "5.4.5" + "@biomejs/biome": "1.8.3", + "@biomejs/cli-darwin-arm64": "1.8.3", + "@biomejs/cli-darwin-x64": "1.8.3", + "@biomejs/cli-linux-arm64": "1.8.3", + "@biomejs/cli-linux-x64": "1.8.3", + "@types/node": "20.14.9", + "execa": "9.3.0", + "pnpm": "9.4.0" } } diff --git a/packages/backend-rs/Cargo.toml b/packages/backend-rs/Cargo.toml index d4942cd61c..790ad0f317 100644 --- a/packages/backend-rs/Cargo.toml +++ b/packages/backend-rs/Cargo.toml @@ -6,13 +6,13 @@ rust-version = "1.74" [features] default = [] -napi = ["dep:napi", "dep:napi-derive"] +napi = ["dep:napi", "dep:napi-derive", "dep:napi-build"] [lib] crate-type = ["cdylib", "lib"] [dependencies] -macro-rs = { workspace = true } +macros = { workspace = true } napi = { workspace = true, optional = true, features = ["chrono_date", "napi4", "serde-json", "tokio_rt"] } napi-derive = { workspace = true, optional = true } @@ -25,7 +25,7 @@ bcrypt = { workspace = true, features = ["std"] } chrono = { workspace = true } cuid2 = { workspace = true } emojis = { workspace = true } -idna = { workspace = true } +idna = { workspace = true, features = ["std", "compiled_data"] } image = { workspace = true, features = ["avif", "bmp", "gif", "ico", "jpeg", "png", "tiff", "webp"] } isahc = { workspace = true, features = ["http2", "text-decoding"] } nom-exif = { workspace = true } @@ -39,7 +39,6 @@ sea-orm = { workspace = true, features = ["macros", "runtime-tokio-rustls", "sql 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 = ["fs", "io-std", "io-util", "macros", "process", "rt-multi-thread", "signal", "sync", "time"] } @@ -54,4 +53,4 @@ pretty_assertions = { workspace = true, features = ["std"] } tokio-test = { workspace = true } [build-dependencies] -napi-build = { workspace = true } +napi-build = { workspace = true, optional = true } diff --git a/packages/backend-rs/Makefile b/packages/backend-rs/Makefile index abe6045801..9e53b37837 100644 --- a/packages/backend-rs/Makefile +++ b/packages/backend-rs/Makefile @@ -11,25 +11,25 @@ regenerate-entities: --output-dir='src/model/entity' \ --database-url='postgres://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@localhost:25432/$(POSTGRES_DB)' \ --date-time-crate='chrono' \ + --with-serde='both' \ --model-extra-attributes='NAPI_EXTRA_ATTR_PLACEHOLDER' && \ for file in src/model/entity/*; do \ base=$$(basename -- "$${file}"); \ jsname=$$(printf '%s\n' "$${base%.*}" | perl -pe 's/(^|_)./uc($$&)/ge;s/_//g'); \ - attribute=$$(printf 'cfg_attr(feature = "napi", napi_derive::napi(object, js_name = "%s", use_nullable = true))' "$${jsname}"); \ + attribute=$$(printf 'macros::export(object, js_name = "%s")' "$${jsname}"); \ sed -i "s/NAPI_EXTRA_ATTR_PLACEHOLDER/$${attribute}/" "$${file}"; \ - sed -i 's/#\[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)\]/#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)]\n#[serde(rename_all = "camelCase")]/' "$${file}"; \ + sed -i 's/#\[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)\]/#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]\n#[serde(rename_all = "camelCase")]/' "$${file}"; \ done - sed -i 's/#\[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)\]/#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, serde::Serialize, serde::Deserialize)]\n#[serde(rename_all = "camelCase")]\n#[cfg_attr(not(feature = "napi"), derive(Clone))]\n#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]/' \ + sed -i 's/#\[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)\]/#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)]\n#[serde(rename_all = "camelCase")]\n#[macros::derive_clone_and_export(string_enum = "camelCase")]/' \ src/model/entity/sea_orm_active_enums.rs cargo fmt --all -- .PHONY: update-index update-index: index.js index.d.ts -index.js index.d.ts: $(SRC) +index.js index.d.ts: $(SRC) package.json NODE_OPTIONS='--max_old_space_size=3072' pnpm run build:debug [ -f built/index.js ] && [ -f built/index.d.ts ] rm --force index.js index.d.ts cp built/index.js index.js cp built/index.d.ts index.d.ts - sed -i 's/^ \*r"/ */g' index.d.ts diff --git a/packages/backend-rs/build.rs b/packages/backend-rs/build.rs index 9e5e97713c..71dd87aa16 100644 --- a/packages/backend-rs/build.rs +++ b/packages/backend-rs/build.rs @@ -1,9 +1,8 @@ -extern crate napi_build; - fn main() { // watch the version in the project root package.json println!("cargo:rerun-if-changed=../../package.json"); // napi + #[cfg(feature = "napi")] napi_build::setup(); } diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts index 5060e38bdb..52e4e1af1a 100644 --- a/packages/backend-rs/index.d.ts +++ b/packages/backend-rs/index.d.ts @@ -1,155 +1,216 @@ -/* tslint:disable */ -/* eslint-disable */ - /* auto-generated by NAPI-RS */ +/* Do NOT edit this file manually */ -export const SECOND: number -export const MINUTE: number -export const HOUR: number -export const DAY: number -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 (TODO: fix this and later allow it to be viewed directly) - * * - * * - * * - */ -export const FILE_TYPE_BROWSERSAFE: string[] -export interface EnvConfig { - onlyQueue: boolean - onlyServer: boolean - noDaemons: boolean - disableClustering: boolean - verbose: boolean - withLogTime: boolean - slow: boolean +type DateTimeWithTimeZone = Date; + +type Json = any; + +export interface AbuseUserReport { + id: string + createdAt: DateTimeWithTimeZone + targetUserId: string + reporterId: string + assigneeId: string | null + resolved: boolean + comment: string + targetUserHost: string | null + reporterHost: string | null + forwarded: boolean } -export function loadEnv(): EnvConfig -export interface ServerConfig { + +export interface AbuseUserReportLike { + id: string + targetUserId: string + reporterId: string + comment: string +} + +export interface AccessToken { + id: string + createdAt: DateTimeWithTimeZone + token: string + hash: string + userId: string + appId: string | null + lastUsedAt: DateTimeWithTimeZone | null + session: string | null + name: string | null + description: string | null + iconUrl: string | null + permission: Array + fetched: boolean +} + +export interface Acct { + username: string + host: string | null +} + +export declare function acctToString(acct: Acct): string + +export interface Ad { + id: string + createdAt: DateTimeWithTimeZone + expiresAt: DateTimeWithTimeZone + place: string + priority: string url: string - port: number - /** the host address to bind to */ - bind?: string - disableHsts?: boolean - /** PostgreSQL configurations */ - db: DbConfig - /** Redis configurations */ - redis: RedisConfig - /** secondary Redis server configurations */ - cacheServer?: RedisConfig - /** proxy host used for HTTP requests */ - proxy?: string - /** proxy host used for SMTP requests */ - proxySmtp?: string - /** hosts to bypass the proxy */ - proxyBypassHosts?: Array - allowedPrivateNetworks?: Array - /** maximum file size that can be uploaded to the drive (in bytes) */ - maxFileSize?: number - accessLog?: string - clusterLimits?: WorkerConfigInternal - cuid?: IdConfig - outgoingAddress?: string - deliverJobConcurrency?: number - inboxJobConcurrency?: number - deliverJobPerSec?: number - inboxJobPerSec?: number - deliverJobMaxAttempts?: number - inboxJobMaxAttempts?: number - /** deprecated in favor of `max_log_level` */ - logLevel?: Array - /** verbosity of the server log. `error`, `warn`, `info`, `debug`, or `trace` */ - maxLogLevel?: string - syslog?: SysLogConfig - proxyRemoteFiles?: boolean - mediaProxy?: string - summalyProxyUrl?: string - reservedUsernames?: Array - maxUserSignups?: number - isManagedHosting?: boolean - maxNoteLength?: number - maxCaptionLength?: number - deepl?: DeepLConfig - libreTranslate?: LibreTranslateConfig - email?: EmailConfig - objectStorage?: ObjectStorageConfig + imageUrl: string + memo: string + ratio: number } -export interface DbConfig { - host: string - port: number - db: string - user: string - pass: string - disableCache?: boolean - extra?: any + +export interface Announcement { + id: string + createdAt: DateTimeWithTimeZone + text: string + title: string + imageUrl: string | null + updatedAt: DateTimeWithTimeZone | null + showPopup: boolean + isGoodNews: boolean } -export interface RedisConfig { - host: string - port: number - family?: number - user?: string - pass?: string - tls?: TlsConfig - db: number - prefix?: string + +export interface AnnouncementRead { + id: string + userId: string + announcementId: string + createdAt: DateTimeWithTimeZone } -export interface TlsConfig { - host: string - rejectUnauthorized: boolean + +export interface Antenna { + id: string + createdAt: DateTimeWithTimeZone + userId: string + name: string + src: AntennaSrc + userListId: string | null + withFile: boolean + expression: string | null + notify: boolean + caseSensitive: boolean + withReplies: boolean + userGroupJoiningId: string | null + users: Array + instances: Array + keywords: Array + excludeKeywords: Array } -export interface WorkerConfig { - web: number - queue: number + +export enum AntennaSrc { + All = 'all', + Group = 'group', + Home = 'home', + Instances = 'instances', + List = 'list', + Users = 'users' } -export interface WorkerConfigInternal { - web?: number - queue?: number + +export interface App { + id: string + createdAt: DateTimeWithTimeZone + userId: string | null + secret: string + name: string + description: string + permission: Array + callbackUrl: string | null } -export interface IdConfig { - length?: number - fingerprint?: string + +export interface AttestationChallenge { + id: string + userId: string + challenge: string + createdAt: DateTimeWithTimeZone + registrationChallenge: boolean } -export interface SysLogConfig { - host: string - port: number + +export interface AuthSession { + id: string + createdAt: DateTimeWithTimeZone + token: string + userId: string | null + appId: string } -export interface DeepLConfig { - managed?: boolean - authKey?: string - isPro?: boolean + +export interface Blocking { + id: string + createdAt: DateTimeWithTimeZone + blockeeId: string + blockerId: string } -export interface LibreTranslateConfig { - managed?: boolean - apiUrl?: string - apiKey?: string + +export interface Channel { + id: string + createdAt: DateTimeWithTimeZone + lastNotedAt: DateTimeWithTimeZone | null + userId: string | null + name: string + description: string | null + bannerId: string | null + notesCount: number + usersCount: number } -export interface EmailConfig { - managed?: boolean - address?: string - host?: string - port?: number - user?: string - pass?: string - useImplicitSslTls?: boolean + +export interface ChannelFollowing { + id: string + createdAt: DateTimeWithTimeZone + followeeId: string + followerId: string } -export interface ObjectStorageConfig { - managed?: boolean - baseUrl?: string - bucket?: string - prefix?: string - endpoint?: string - region?: string - accessKey?: string - secretKey?: string - useSsl?: boolean - connnectOverProxy?: boolean - setPublicReadOnUpload?: boolean - s3ForcePathStyle?: boolean + +export interface ChannelNotePining { + id: string + createdAt: DateTimeWithTimeZone + channelId: string + noteId: string } + +export enum ChatEvent { + Message = 0, + Read = 1, + Deleted = 2, + Typing = 3 +} + +export enum ChatIndexEvent { + Message = 0, + Read = 1 +} + +/** + * 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` : [PartialNoteToCheckWordMute] 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 declare function checkWordMute(note: PartialNoteToCheckWordMute, mutedWords: Array, mutedPatterns: Array): Promise + +export interface Clip { + id: string + createdAt: DateTimeWithTimeZone + userId: string + name: string + isPublic: boolean + description: string | null +} + +export interface ClipNote { + id: string + noteId: string + clipId: string +} + export interface Config { url: string port: number @@ -201,442 +262,44 @@ export interface Config { driveUrl: string userAgent: string } -export function loadConfig(): Config -export interface Acct { - username: string - host: string | null + +export declare function countReactions(reactions: Record): Record + +export interface Cpu { + model: string + cores: number } -export function stringToAcct(acct: string): Acct -export function acctToString(acct: Acct): string -/** Fetches and returns the NodeInfo of a remote server. */ -export function fetchNodeinfo(host: string): Promise -export function nodeinfo_2_1(): Promise -export function nodeinfo_2_0(): Promise -/** NodeInfo schema version 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 - /** 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 + +export declare function cpuInfo(): Cpu + +export declare function cpuUsage(): number + +export const DAY: number + +export interface DbConfig { + host: string + port: number + db: string + user: string + pass: string + disableCache?: boolean + extra?: 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 - /** The third party sites this server can publish messages to on the behalf of a user. */ - outbound: Array -} -/** 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 - * `host` - punycoded instance host - * - * # Example - * ```no_run - * # use backend_rs::misc::check_server_block::is_blocked_server; - * # async fn f() -> Result<(), Box> { - * 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 -/** - * Checks if a server is silenced. - * - * # Argument - * `host` - punycoded instance host - * - * # Example - * ```no_run - * # use backend_rs::misc::check_server_block::is_silenced_server; - * # async fn f() -> Result<(), Box> { - * 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 -/** - * Checks if a server is allowlisted. - * Returns `Ok(true)` if private mode is disabled. - * - * # Argument - * `host` - punycoded instance host - * - * # Example - * ```no_run - * # use backend_rs::misc::check_server_block::is_allowed_server; - * # async fn f() -> Result<(), Box> { - * 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 -/** - * 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` : [NoteLike] 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: NoteLike, mutedWords: Array, mutedPatterns: Array): Promise -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 -/** 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 -export interface NoteLikeForAllTexts { - fileIds: Array - userId: string - text: string | null - cw: string | null - renoteId: string | null - replyId: string | null -} -export interface NoteLikeForGetNoteSummary { - fileIds: Array - text: string | null - cw: string | null - hasPoll: boolean -} -export function getNoteSummary(note: NoteLikeForGetNoteSummary): string -export function isQuote(note: Note): boolean -export function isSafeUrl(url: string): boolean -/** Returns the latest Firefish version. */ -export function latestVersion(): Promise -export function toMastodonId(firefishId: string): string | null -export function fromMastodonId(mastodonId: string): string | null -export function fetchMeta(useCache: boolean): Promise -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: - * * - * * - * - * # 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 name: string | null host: string | null } -export function decodeReaction(reaction: string): DecodedReaction -export function countReactions(reactions: Record): Record -export function toDbReaction(reaction?: string | undefined | null, host?: string | undefined | null): Promise -/** Delete all entries in the [attestation_challenge] table created at more than 5 minutes ago */ -export function removeOldAttestationChallenges(): Promise -export interface Cpu { - model: string - cores: number -} -export interface Memory { - /** Total memory amount in bytes */ - total: number - /** Used memory amount in bytes */ - used: number - /** Available (for (re)use) memory amount in bytes */ - available: number -} -export interface Storage { - /** Total storage space in bytes */ - total: number - /** Used storage space in bytes */ - used: number -} -export function cpuInfo(): Cpu -export function cpuUsage(): number -export function memoryUsage(): Memory -export function storageUsage(): Storage | null -export interface AbuseUserReport { - id: string - createdAt: DateTimeWithTimeZone - targetUserId: string - reporterId: string - assigneeId: string | null - resolved: boolean - comment: string - targetUserHost: string | null - reporterHost: string | null - forwarded: boolean -} -export interface AccessToken { - id: string - createdAt: DateTimeWithTimeZone - token: string - hash: string - userId: string - appId: string | null - lastUsedAt: DateTimeWithTimeZone | null - session: string | null - name: string | null - description: string | null - iconUrl: string | null - permission: Array - fetched: boolean -} -export interface Ad { - id: string - createdAt: DateTimeWithTimeZone - expiresAt: DateTimeWithTimeZone - place: string - priority: string - url: string - imageUrl: string - memo: string - ratio: number -} -export interface Announcement { - id: string - createdAt: DateTimeWithTimeZone - text: string - title: string - imageUrl: string | null - updatedAt: DateTimeWithTimeZone | null - showPopup: boolean - isGoodNews: boolean -} -export interface AnnouncementRead { - id: string - userId: string - announcementId: string - createdAt: DateTimeWithTimeZone -} -export interface Antenna { - id: string - createdAt: DateTimeWithTimeZone - userId: string - name: string - src: AntennaSrc - userListId: string | null - withFile: boolean - expression: string | null - notify: boolean - caseSensitive: boolean - withReplies: boolean - userGroupJoiningId: string | null - users: Array - instances: Array - keywords: Array - excludeKeywords: Array -} -export interface App { - id: string - createdAt: DateTimeWithTimeZone - userId: string | null - secret: string - name: string - description: string - permission: Array - callbackUrl: string | null -} -export interface AttestationChallenge { - id: string - userId: string - challenge: string - createdAt: DateTimeWithTimeZone - registrationChallenge: boolean -} -export interface AuthSession { - id: string - createdAt: DateTimeWithTimeZone - token: string - userId: string | null - appId: string -} -export interface Blocking { - id: string - createdAt: DateTimeWithTimeZone - blockeeId: string - blockerId: string -} -export interface Channel { - id: string - createdAt: DateTimeWithTimeZone - lastNotedAt: DateTimeWithTimeZone | null - userId: string | null - name: string - description: string | null - bannerId: string | null - notesCount: number - usersCount: number -} -export interface ChannelFollowing { - id: string - createdAt: DateTimeWithTimeZone - followeeId: string - followerId: string -} -export interface ChannelNotePining { - id: string - createdAt: DateTimeWithTimeZone - channelId: string - noteId: string -} -export interface Clip { - id: string - createdAt: DateTimeWithTimeZone - userId: string - name: string - isPublic: boolean - description: string | null -} -export interface ClipNote { - id: string - noteId: string - clipId: string + +export declare function decodeReaction(reaction: string): DecodedReaction + +export interface DeepLConfig { + managed?: boolean + authKey?: string + isPro?: boolean } + export interface DriveFile { id: string createdAt: DateTimeWithTimeZone @@ -666,6 +329,18 @@ export interface DriveFile { requestIp: string | null usageHint: DriveFileUsageHint | null } + +export enum DriveFileEvent { + Create = 0, + Update = 1, + Delete = 2 +} + +export enum DriveFileUsageHint { + UserAvatar = 'userAvatar', + UserBanner = 'userBanner' +} + export interface DriveFolder { id: string createdAt: DateTimeWithTimeZone @@ -673,6 +348,23 @@ export interface DriveFolder { userId: string | null parentId: string | null } + +export enum DriveFolderEvent { + Create = 0, + Update = 1, + Delete = 2 +} + +export interface EmailConfig { + managed?: boolean + address?: string + host?: string + port?: number + user?: string + pass?: string + useImplicitSslTls?: boolean +} + export interface Emoji { id: string updatedAt: DateTimeWithTimeZone | null @@ -688,6 +380,38 @@ export interface Emoji { width: number | null height: number | null } + +export declare function extractHost(uri: string): string + +export declare function fetchMeta(): Promise + +/** Fetches and returns the NodeInfo (version 2.0) of a remote server. */ +export declare function fetchNodeinfo(host: string): Promise + +/** + * 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 (TODO: fix this and later allow it to be viewed directly) + * * + * * + * * + */ +export const FILE_TYPE_BROWSERSAFE: string[] + +export interface Following { + id: string + createdAt: DateTimeWithTimeZone + followeeId: string + followerId: string + followerHost: string | null + followerInbox: string | null + followerSharedInbox: string | null + followeeHost: string | null + followeeInbox: string | null + followeeSharedInbox: string | null +} + export interface FollowRequest { id: string createdAt: DateTimeWithTimeZone @@ -701,24 +425,19 @@ export interface FollowRequest { followeeInbox: string | null followeeSharedInbox: string | null } -export interface Following { - id: string - createdAt: DateTimeWithTimeZone - followeeId: string - followerId: string - followerHost: string | null - followerInbox: string | null - followerSharedInbox: string | null - followeeHost: string | null - followeeInbox: string | null - followeeSharedInbox: string | null -} + +/** Converts milliseconds to a human readable string. */ +export declare function formatMilliseconds(milliseconds: number): string + +export declare function fromMastodonId(mastodonId: string): string | null + export interface GalleryLike { id: string createdAt: DateTimeWithTimeZone userId: string postId: string } + export interface GalleryPost { id: string createdAt: DateTimeWithTimeZone @@ -731,6 +450,39 @@ export interface GalleryPost { likedCount: number tags: Array } + +/** Generates a random string based on [thread_rng] and [Alphanumeric]. */ +export declare function generateSecureRandomString(length: number): string + +export declare function generateUserToken(): string + +/** + * The generated ID results in the form of `[8 chars timestamp] + [cuid2]`. + * The minimum and maximum lengths are 16 and 24, respectively. + * With the length of 16, namely 8 for cuid2, roughly 1427399 IDs are needed + * in the same millisecond to reach 50% chance of collision. + * + * Ref: + */ +export declare function genId(): string + +/** Generate an ID using a specific datetime */ +export declare function genIdAt(date: Date): string + +export declare function getFullApAccount(username: string, host?: string | undefined | null): string + +export declare function getImageSizeFromUrl(url: string): Promise + +export declare function getNoteSummary(fileIds: Array, text: string | undefined | null, cw: string | undefined | null, hasPoll: boolean): string + +export declare function getTimestamp(id: string): number + +/** Prints the greeting message and the Firefish version to stdout. */ +export declare function greet(): void + +/** Hashes the given password using [argon2] algorithm. */ +export declare function hashPassword(password: string): string + export interface Hashtag { id: string name: string @@ -747,6 +499,34 @@ export interface Hashtag { attachedRemoteUserIds: Array attachedRemoteUsersCount: number } + +export const HOUR: number + +export interface IdConfig { + length?: number + fingerprint?: string +} + +export interface ImageSize { + width: number + height: number +} + +/** The third party sites this server can retrieve messages from for combined display with regular traffic. */ +export enum Inbound { + Atom1 = 0, + Gnusocial = 1, + Imap = 2, + Pnut = 3, + Pop3 = 4, + Pumpio = 5, + Rss2 = 6, + Twitter = 7 +} + +/** Initializes the [tracing] logger. */ +export declare function initializeRustLogger(): void + export interface Instance { id: string caughtAt: DateTimeWithTimeZone @@ -773,6 +553,103 @@ export interface Instance { themeColor: string | null faviconUrl: string | null } + +/** + * Checks if a server is allowlisted. + * Returns `Ok(true)` if private mode is disabled. + * + * # Argument + * `host` - punycoded instance host + * + * # Example + * ```no_run + * # use backend_rs::misc::check_server_block::is_allowed_server; + * # async fn f() -> Result<(), Box> { + * 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 declare function isAllowedServer(host: string): Promise + +/** + * Checks if a server is blocked. + * + * # Argument + * `host` - punycoded instance host + * + * # Example + * ```no_run + * # use backend_rs::misc::check_server_block::is_blocked_server; + * # async fn f() -> Result<(), Box> { + * 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 declare function isBlockedServer(host: string): Promise + +/** Returns whether the [bcrypt] algorithm is used for the password hash. */ +export declare function isOldPasswordAlgorithm(hash: string): boolean + +export declare function isQuote(note: NoteLikeForIsQuote): boolean + +export declare function isSafeUrl(url: string): boolean + +export declare function isSameOrigin(uri: string): boolean + +export declare function isSelfHost(host?: string | undefined | null): boolean + +/** + * Checks if a server is silenced. + * + * # Argument + * `host` - punycoded instance host + * + * # Example + * ```no_run + * # use backend_rs::misc::check_server_block::is_silenced_server; + * # async fn f() -> Result<(), Box> { + * 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 declare function isSilencedServer(host: string): Promise + +export declare function isUnicodeEmoji(s: string): boolean + +/** Returns the latest Firefish version. */ +export declare function latestVersion(): Promise + +export interface LibreTranslateConfig { + managed?: boolean + apiUrl?: string + apiKey?: string +} + +export declare function loadConfig(): Config + +export interface Memory { + /** Total memory amount in bytes */ + total: number + /** Used memory amount in bytes */ + used: number + /** Available (for (re)use) memory amount in bytes */ + available: number +} + +export declare function memoryUsage(): Memory + export interface MessagingMessage { id: string createdAt: DateTimeWithTimeZone @@ -785,6 +662,7 @@ export interface MessagingMessage { reads: Array uri: string | null } + export interface Meta { id: string name: string | null @@ -872,11 +750,17 @@ export interface Meta { markLocalFilesNsfwByDefault: boolean antennaLimit: number } + +export declare function metaToPugArgs(meta: Meta): PugArgs + export interface Migrations { id: number timestamp: number name: string } + +export const MINUTE: number + export interface ModerationLog { id: string createdAt: DateTimeWithTimeZone @@ -884,12 +768,21 @@ export interface ModerationLog { type: string info: Json } + export interface MutedNote { id: string noteId: string userId: string reason: MutedNoteReason } + +export enum MutedNoteReason { + Manual = 'manual', + Other = 'other', + Spam = 'spam', + Word = 'word' +} + export interface Muting { id: string createdAt: DateTimeWithTimeZone @@ -897,6 +790,27 @@ export interface Muting { muterId: string expiresAt: DateTimeWithTimeZone | null } + +/** NodeInfo schema version 2.0. */ +export interface Nodeinfo { + /** Metadata about server software in use. */ + software: Software20 + /** The protocols supported on this server. */ + protocols: Array + /** 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 +} + +export declare function nodeinfo_2_0(): Promise + +export declare function nodeinfo_2_1(): Promise + export interface Note { id: string createdAt: DateTimeWithTimeZone @@ -931,7 +845,9 @@ export interface Note { threadId: string | null updatedAt: DateTimeWithTimeZone | null lang: string | null + scheduledAt: DateTimeWithTimeZone | null } + export interface NoteEdit { id: string noteId: string @@ -941,17 +857,27 @@ export interface NoteEdit { updatedAt: DateTimeWithTimeZone emojis: Array } + export interface NoteFavorite { id: string createdAt: DateTimeWithTimeZone userId: string noteId: string } + export interface NoteFile { serialNo: number noteId: string fileId: string } + +export interface NoteLikeForIsQuote { + renoteId: string | null + text: string | null + hasPoll: boolean + fileIds: Array +} + export interface NoteReaction { id: string createdAt: DateTimeWithTimeZone @@ -959,12 +885,14 @@ export interface NoteReaction { noteId: string reaction: string } + export interface NoteThreadMuting { id: string createdAt: DateTimeWithTimeZone userId: string threadId: string } + export interface NoteUnread { id: string userId: string @@ -974,6 +902,15 @@ export interface NoteUnread { isMentioned: boolean noteChannelId: string | null } + +export enum NoteVisibility { + Followers = 'followers', + Hidden = 'hidden', + Home = 'home', + Public = 'public', + Specified = 'specified' +} + export interface NoteWatching { id: string createdAt: DateTimeWithTimeZone @@ -981,6 +918,7 @@ export interface NoteWatching { noteId: string noteUserId: string } + export interface Notification { id: string createdAt: DateTimeWithTimeZone @@ -998,6 +936,102 @@ export interface Notification { customIcon: string | null appAccessTokenId: string | null } + +export enum NotificationType { + App = 'app', + Follow = 'follow', + FollowRequestAccepted = 'followRequestAccepted', + GroupInvited = 'groupInvited', + Mention = 'mention', + PollEnded = 'pollEnded', + PollVote = 'pollVote', + Quote = 'quote', + Reaction = 'reaction', + ReceiveFollowRequest = 'receiveFollowRequest', + Renote = 'renote', + Reply = 'reply' +} + +/** + * Converts the given text into the cat language. + * + * refs: + * * + * * + * + * # 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 declare function nyaify(text: string, lang?: string | undefined | null): string + +export interface ObjectStorageConfig { + managed?: boolean + baseUrl?: string + bucket?: string + prefix?: string + endpoint?: string + region?: string + accessKey?: string + secretKey?: string + useSsl?: boolean + connnectOverProxy?: boolean + setPublicReadOnUpload?: boolean + s3ForcePathStyle?: boolean +} + +/** The third party sites this server can publish messages to on the behalf of a user. */ +export enum Outbound { + Atom1 = 0, + Blogger = 1, + Buddycloud = 2, + Diaspora = 3, + Dreamwidth = 4, + Drupal = 5, + Facebook = 6, + Friendica = 7, + Gnusocial = 8, + Google = 9, + Insanejournal = 10, + Libertree = 11, + Linkedin = 12, + Livejournal = 13, + Mediagoblin = 14, + Myspace = 15, + Pinterest = 16, + Pnut = 17, + Posterous = 18, + Pumpio = 19, + Redmatrix = 20, + Rss2 = 21, + Smtp = 22, + Tent = 23, + Tumblr = 24, + Twitter = 25, + Wordpress = 26, + Xmpp = 27 +} + +export interface PackedEmoji { + id: string + aliases: Array + name: string + category: string | null + host: string | null + url: string + license: string | null + width: number | null + height: number | null +} + export interface Page { id: string createdAt: DateTimeWithTimeZone @@ -1018,18 +1052,35 @@ export interface Page { script: string isPublic: boolean } + export interface PageLike { id: string createdAt: DateTimeWithTimeZone userId: string pageId: string } + +export enum PageVisibility { + Followers = 'followers', + Public = 'public', + Specified = 'specified' +} + +export interface PartialNoteToCheckWordMute { + fileIds: Array + text: string | null + cw: string | null + renoteId: string | null + replyId: string | null +} + export interface PasswordResetRequest { id: string createdAt: DateTimeWithTimeZone token: string userId: string } + export interface Poll { noteId: string expiresAt: DateTimeWithTimeZone | null @@ -1040,6 +1091,14 @@ export interface Poll { userId: string userHost: string | null } + +export enum PollNoteVisibility { + Followers = 'followers', + Home = 'home', + Public = 'public', + Specified = 'specified' +} + export interface PollVote { id: string createdAt: DateTimeWithTimeZone @@ -1047,22 +1106,90 @@ export interface PollVote { noteId: string choice: number } + export interface PromoNote { noteId: string expiresAt: DateTimeWithTimeZone userId: string } + export interface PromoRead { id: string createdAt: DateTimeWithTimeZone userId: string noteId: string } + +export enum Protocol { + Activitypub = 0, + Buddycloud = 1, + Dfrn = 2, + Diaspora = 3, + Libertree = 4, + Ostatus = 5, + Pumpio = 6, + Tent = 7, + Xmpp = 8, + Zot = 9 +} + +export declare function publishToBroadcastStream(emoji: PackedEmoji): Promise + +export declare function publishToChannelStream(channelId: string, userId: string): Promise + +export declare function publishToChatIndexStream(userId: string, kind: ChatIndexEvent, object: any): Promise + +export declare function publishToChatStream(senderUserId: string, receiverUserId: string, kind: ChatEvent, object: any): Promise + +export declare function publishToDriveFileStream(userId: string, kind: DriveFileEvent, object: any): Promise + +export declare function publishToDriveFolderStream(userId: string, kind: DriveFolderEvent, object: any): Promise + +export declare function publishToGroupChatStream(groupId: string, kind: ChatEvent, object: any): Promise + +export declare function publishToModerationStream(moderatorId: string, report: AbuseUserReportLike): Promise + +export declare function publishToNotesStream(note: Note): Promise + +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 enum PushNotificationKind { + Generic = 0, + Chat = 1, + ReadAllChats = 2, + ReadAllChatsInTheRoom = 3, + ReadNotifications = 4, + ReadAllNotifications = 5, + Mastodon = 6 +} + +export interface RedisConfig { + host: string + port: number + family?: number + user?: string + pass?: string + tls?: TlsConfig + db: number + prefix?: string +} + export interface RegistrationTicket { id: string createdAt: DateTimeWithTimeZone code: string } + export interface RegistryItem { id: string createdAt: DateTimeWithTimeZone @@ -1073,109 +1200,104 @@ export interface RegistryItem { domain: string | null value: Json | null } + export interface Relay { id: string inbox: string status: RelayStatus } + +export enum RelayStatus { + Accepted = 'accepted', + Rejected = 'rejected', + Requesting = 'requesting' +} + +/** Delete all entries in the [attestation_challenge] table created at more than 5 minutes ago */ +export declare function removeOldAttestationChallenges(): Promise + export interface RenoteMuting { id: string createdAt: DateTimeWithTimeZone muteeId: string muterId: string } + export interface ReplyMuting { id: string createdAt: DateTimeWithTimeZone muteeId: string muterId: string } -export interface ScheduledNote { - id: string - noteId: string - userId: string - scheduledAt: DateTimeWithTimeZone + +/** Returns `true` if `src` does not contain suspicious characters like `%`. */ +export declare function safeForSql(src: string): boolean + +export const SECOND: number + +export declare function sendPushNotification(receiverUserId: string, kind: PushNotificationKind, content: any): Promise + +export interface ServerConfig { + url: string + port: number + /** the host address to bind to */ + bind?: string + disableHsts?: boolean + /** PostgreSQL configurations */ + db: DbConfig + /** Redis configurations */ + redis: RedisConfig + /** secondary Redis server configurations */ + cacheServer?: RedisConfig + /** proxy host used for HTTP requests */ + proxy?: string + /** proxy host used for SMTP requests */ + proxySmtp?: string + /** hosts to bypass the proxy */ + proxyBypassHosts?: Array + allowedPrivateNetworks?: Array + /** maximum file size that can be uploaded to the drive (in bytes) */ + maxFileSize?: number + accessLog?: string + clusterLimits?: WorkerConfigInternal + cuid?: IdConfig + outgoingAddress?: string + deliverJobConcurrency?: number + inboxJobConcurrency?: number + deliverJobPerSec?: number + inboxJobPerSec?: number + deliverJobMaxAttempts?: number + inboxJobMaxAttempts?: number + /** deprecated in favor of `max_log_level` */ + logLevel?: Array + /** verbosity of the server log. `error`, `warn`, `info`, `debug`, or `trace` */ + maxLogLevel?: string + syslog?: SysLogConfig + proxyRemoteFiles?: boolean + mediaProxy?: string + summalyProxyUrl?: string + reservedUsernames?: Array + maxUserSignups?: number + isManagedHosting?: boolean + maxNoteLength?: number + maxCaptionLength?: number + deepl?: DeepLConfig + libreTranslate?: LibreTranslateConfig + email?: EmailConfig + objectStorage?: ObjectStorageConfig } -export enum AntennaSrc { - All = 'all', - Group = 'group', - Home = 'home', - Instances = 'instances', - List = 'list', - Users = 'users' -} -export enum DriveFileUsageHint { - UserAvatar = 'userAvatar', - UserBanner = 'userBanner' -} -export enum MutedNoteReason { - Manual = 'manual', - Other = 'other', - Spam = 'spam', - Word = 'word' -} -export enum NoteVisibility { - Followers = 'followers', - Hidden = 'hidden', - Home = 'home', - Public = 'public', - Specified = 'specified' -} -export enum NotificationType { - App = 'app', - Follow = 'follow', - FollowRequestAccepted = 'followRequestAccepted', - GroupInvited = 'groupInvited', - Mention = 'mention', - PollEnded = 'pollEnded', - PollVote = 'pollVote', - Quote = 'quote', - Reaction = 'reaction', - ReceiveFollowRequest = 'receiveFollowRequest', - Renote = 'renote', - Reply = 'reply' -} -export enum PageVisibility { - Followers = 'followers', - Public = 'public', - Specified = 'specified' -} -export enum PollNoteVisibility { - Followers = 'followers', - Home = 'home', - Public = 'public', - Specified = 'specified' -} -export enum RelayStatus { - Accepted = 'accepted', - Rejected = 'rejected', - Requesting = 'requesting' -} -export enum UserEmojiModPerm { - Add = 'add', - Full = 'full', - Mod = 'mod', - Unauthorized = 'unauthorized' -} -export enum UserProfileFfvisibility { - Followers = 'followers', - Private = 'private', - Public = 'public' -} -export enum UserProfileMutingNotificationTypes { - App = 'app', - Follow = 'follow', - FollowRequestAccepted = 'followRequestAccepted', - GroupInvited = 'groupInvited', - Mention = 'mention', - PollEnded = 'pollEnded', - PollVote = 'pollVote', - Quote = 'quote', - Reaction = 'reaction', - ReceiveFollowRequest = 'receiveFollowRequest', - Renote = 'renote', - Reply = 'reply' + +/** 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 + /** The third party sites this server can publish messages to on the behalf of a user. */ + outbound: Array } + +/** Prints the server hardware information as the server info log. */ +export declare function showServerInfo(): void + export interface Signin { id: string createdAt: DateTimeWithTimeZone @@ -1184,6 +1306,29 @@ export interface Signin { headers: Json success: boolean } + +/** 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 +} + +/** Escapes `%` and `\` in the given string. */ +export declare function sqlLikeEscape(src: string): string + +export interface Storage { + /** Total storage space in bytes */ + total: number + /** Used storage space in bytes */ + used: number +} + +export declare function storageUsage(): Storage | null + +export declare function stringToAcct(acct: string): Acct + export interface SwSubscription { id: string createdAt: DateTimeWithTimeZone @@ -1193,10 +1338,45 @@ export interface SwSubscription { publickey: string sendReadMessage: boolean } + +export interface SysLogConfig { + host: string + port: number +} + +export interface TlsConfig { + host: string + rejectUnauthorized: boolean +} + +export declare function toDbReaction(reaction?: string | undefined | null, host?: string | undefined | null): Promise + +export declare function toMastodonId(firefishId: string): string | null + +export declare function toPuny(host: string): string + +export declare function unwatchNote(watcherId: string, noteId: string): Promise + +export declare function updateAntennaCache(): Promise + +export declare function updateAntennasOnNewNote(note: Note, noteAuthor: Acct, noteMutedUsers: Array): Promise + +export declare function updateMetaCache(): Promise + +export declare function updateNodeinfoCache(): Promise + +/** Usage statistics for this server. */ +export interface Usage { + users: Users + localPosts: number | null + localComments: number | null +} + export interface UsedUsername { username: string createdAt: DateTimeWithTimeZone } + export interface User { id: string createdAt: DateTimeWithTimeZone @@ -1237,6 +1417,18 @@ export interface User { isIndexable: boolean alsoKnownAs: Array | null } + +export const USER_ACTIVE_THRESHOLD: number + +export const USER_ONLINE_THRESHOLD: number + +export enum UserEmojiModPerm { + Add = 'add', + Full = 'full', + Mod = 'mod', + Unauthorized = 'unauthorized' +} + export interface UserGroup { id: string createdAt: DateTimeWithTimeZone @@ -1244,53 +1436,62 @@ export interface UserGroup { userId: string isPrivate: boolean } + export interface UserGroupInvitation { id: string createdAt: DateTimeWithTimeZone userId: string userGroupId: string } + export interface UserGroupInvite { id: string createdAt: DateTimeWithTimeZone userId: string userGroupId: string } + export interface UserGroupJoining { id: string createdAt: DateTimeWithTimeZone userId: string userGroupId: string } + export interface UserIp { id: number createdAt: DateTimeWithTimeZone userId: string ip: string } + export interface UserKeypair { userId: string publicKey: string privateKey: string } + export interface UserList { id: string createdAt: DateTimeWithTimeZone userId: string name: string } + export interface UserListJoining { id: string createdAt: DateTimeWithTimeZone userId: string userListId: string } + export interface UserNotePining { id: string createdAt: DateTimeWithTimeZone userId: string noteId: string } + export interface UserPending { id: string createdAt: DateTimeWithTimeZone @@ -1299,6 +1500,7 @@ export interface UserPending { email: string password: string } + export interface UserProfile { userId: string location: string | null @@ -1336,11 +1538,41 @@ export interface UserProfile { mutedWords: Array lang: string | null } + +export enum UserProfileFfvisibility { + Followers = 'followers', + Private = 'private', + Public = 'public' +} + +export enum UserProfileMutingNotificationTypes { + App = 'app', + Follow = 'follow', + FollowRequestAccepted = 'followRequestAccepted', + GroupInvited = 'groupInvited', + Mention = 'mention', + PollEnded = 'pollEnded', + PollVote = 'pollVote', + Quote = 'quote', + Reaction = 'reaction', + ReceiveFollowRequest = 'receiveFollowRequest', + Renote = 'renote', + Reply = 'reply' +} + export interface UserPublickey { userId: string keyId: string keyPem: string } + +/** statistics about the users of this server. */ +export interface Users { + total: number | null + activeHalfyear: number | null + activeMonth: number | null +} + export interface UserSecurityKey { id: string userId: string @@ -1348,6 +1580,12 @@ export interface UserSecurityKey { lastUsed: DateTimeWithTimeZone name: string } + +/** Checks whether the given password and hash match. */ +export declare function verifyPassword(password: string, hash: string): boolean + +export declare function watchNote(watcherId: string, noteAuthorId: string, noteId: string): Promise + export interface Webhook { id: string createdAt: DateTimeWithTimeZone @@ -1360,64 +1598,14 @@ export interface Webhook { latestSentAt: DateTimeWithTimeZone | null latestStatus: number | null } -export function updateAntennasOnNewNote(note: Note, noteAuthor: Acct, noteMutedUsers: Array): Promise -export function watchNote(watcherId: string, noteAuthorId: string, noteId: string): Promise -export function unwatchNote(watcherId: string, noteId: string): Promise -export enum PushNotificationKind { - Generic = 'generic', - Chat = 'chat', - ReadAllChats = 'readAllChats', - ReadAllChatsInTheRoom = 'readAllChatsInTheRoom', - ReadNotifications = 'readNotifications', - ReadAllNotifications = 'readAllNotifications', - Mastodon = 'mastodon' + +export interface WorkerConfig { + web: number + queue: number } -export function sendPushNotification(receiverUserId: string, kind: PushNotificationKind, content: any): Promise -export function publishToChannelStream(channelId: string, userId: string): Promise -export enum ChatEvent { - Message = 'message', - Read = 'read', - Deleted = 'deleted', - Typing = 'typing' + +export interface WorkerConfigInternal { + web?: number + queue?: number } -export function publishToChatStream(senderUserId: string, receiverUserId: string, kind: ChatEvent, object: any): Promise -export enum ChatIndexEvent { - Message = 'message', - Read = 'read' -} -export function publishToChatIndexStream(userId: string, kind: ChatIndexEvent, object: any): Promise -export interface PackedEmoji { - id: string - aliases: Array - name: string - category: string | null - host: string | null - url: string - license: string | null - width: number | null - height: number | null -} -export function publishToBroadcastStream(emoji: PackedEmoji): Promise -export function publishToGroupChatStream(groupId: string, kind: ChatEvent, object: any): Promise -export interface AbuseUserReportLike { - id: string - targetUserId: string - reporterId: string - comment: string -} -export function publishToModerationStream(moderatorId: string, report: AbuseUserReportLike): Promise -export function getTimestamp(id: string): number -/** - * The generated ID results in the form of `[8 chars timestamp] + [cuid2]`. - * The minimum and maximum lengths are 16 and 24, respectively. - * With the length of 16, namely 8 for cuid2, roughly 1427399 IDs are needed - * in the same millisecond to reach 50% chance of collision. - * - * Ref: - */ -export function genId(): string -/** Generate an ID using a specific datetime */ -export function genIdAt(date: Date): string -/** Generates a random string based on [thread_rng] and [Alphanumeric]. */ -export function generateSecureRandomString(length: number): string -export function generateUserToken(): string + diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js index 1da5b752d7..9cb1779766 100644 --- a/packages/backend-rs/index.js +++ b/packages/backend-rs/index.js @@ -1,397 +1,453 @@ -/* tslint:disable */ +// prettier-ignore /* eslint-disable */ -/* prettier-ignore */ - /* auto-generated by NAPI-RS */ -const { existsSync, readFileSync } = require('fs') -const { join } = require('path') - -const { platform, arch } = process +const { readFileSync } = require('fs') let nativeBinding = null -let localFileExisted = false -let loadError = null +const loadErrors = [] -function isMusl() { - // For Node 10 - if (!process.report || typeof process.report.getReport !== 'function') { - try { - const lddPath = require('child_process').execSync('which ldd').toString().trim() - return readFileSync(lddPath, 'utf8').includes('musl') - } catch (e) { - return true +const isMusl = () => { + let musl = false + if (process.platform === 'linux') { + musl = isMuslFromFilesystem() + if (musl === null) { + musl = isMuslFromReport() } - } else { - const { glibcVersionRuntime } = process.report.getReport().header - return !glibcVersionRuntime + if (musl === null) { + musl = isMuslFromChildProcess() + } + } + return musl +} + +const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-') + +const isMuslFromFilesystem = () => { + try { + return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl') + } catch { + return null } } -switch (platform) { - case 'android': - switch (arch) { - case 'arm64': - localFileExisted = existsSync(join(__dirname, 'backend-rs.android-arm64.node')) - try { - if (localFileExisted) { - nativeBinding = require('./backend-rs.android-arm64.node') - } else { - nativeBinding = require('backend-rs-android-arm64') - } - } catch (e) { - loadError = e - } - break - case 'arm': - localFileExisted = existsSync(join(__dirname, 'backend-rs.android-arm-eabi.node')) - try { - if (localFileExisted) { - nativeBinding = require('./backend-rs.android-arm-eabi.node') - } else { - nativeBinding = require('backend-rs-android-arm-eabi') - } - } catch (e) { - loadError = e - } - break - default: - throw new Error(`Unsupported architecture on Android ${arch}`) +const isMuslFromReport = () => { + const report = typeof process.report.getReport === 'function' ? process.report.getReport() : null + if (!report) { + return null + } + if (report.header && report.header.glibcVersionRuntime) { + return false + } + if (Array.isArray(report.sharedObjects)) { + if (report.sharedObjects.some(isFileMusl)) { + return true } - break - case 'win32': - switch (arch) { - case 'x64': - localFileExisted = existsSync( - join(__dirname, 'backend-rs.win32-x64-msvc.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./backend-rs.win32-x64-msvc.node') - } else { - nativeBinding = require('backend-rs-win32-x64-msvc') - } - } catch (e) { - loadError = e - } - break - case 'ia32': - localFileExisted = existsSync( - join(__dirname, 'backend-rs.win32-ia32-msvc.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./backend-rs.win32-ia32-msvc.node') - } else { - nativeBinding = require('backend-rs-win32-ia32-msvc') - } - } catch (e) { - loadError = e - } - break - case 'arm64': - localFileExisted = existsSync( - join(__dirname, 'backend-rs.win32-arm64-msvc.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./backend-rs.win32-arm64-msvc.node') - } else { - nativeBinding = require('backend-rs-win32-arm64-msvc') - } - } catch (e) { - loadError = e - } - break - default: - throw new Error(`Unsupported architecture on Windows: ${arch}`) - } - break - case 'darwin': - localFileExisted = existsSync(join(__dirname, 'backend-rs.darwin-universal.node')) - try { - if (localFileExisted) { - nativeBinding = require('./backend-rs.darwin-universal.node') - } else { - nativeBinding = require('backend-rs-darwin-universal') + } + return false +} + +const isMuslFromChildProcess = () => { + try { + return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl') + } catch (e) { + // If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false + return false + } +} + +function requireNative() { + if (process.platform === 'android') { + if (process.arch === 'arm64') { + try { + return require('./backend-rs.android-arm64.node') + } catch (e) { + loadErrors.push(e) } - break - } catch {} - switch (arch) { - case 'x64': - localFileExisted = existsSync(join(__dirname, 'backend-rs.darwin-x64.node')) - try { - if (localFileExisted) { - nativeBinding = require('./backend-rs.darwin-x64.node') - } else { - nativeBinding = require('backend-rs-darwin-x64') - } - } catch (e) { - loadError = e - } - break - case 'arm64': - localFileExisted = existsSync( - join(__dirname, 'backend-rs.darwin-arm64.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./backend-rs.darwin-arm64.node') - } else { - nativeBinding = require('backend-rs-darwin-arm64') - } - } catch (e) { - loadError = e - } - break - default: - throw new Error(`Unsupported architecture on macOS: ${arch}`) - } - break - case 'freebsd': - if (arch !== 'x64') { - throw new Error(`Unsupported architecture on FreeBSD: ${arch}`) - } - localFileExisted = existsSync(join(__dirname, 'backend-rs.freebsd-x64.node')) - try { - if (localFileExisted) { - nativeBinding = require('./backend-rs.freebsd-x64.node') - } else { - nativeBinding = require('backend-rs-freebsd-x64') + try { + return require('backend-rs-android-arm64') + } catch (e) { + loadErrors.push(e) } - } catch (e) { - loadError = e + + } else if (process.arch === 'arm') { + try { + return require('./backend-rs.android-arm-eabi.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('backend-rs-android-arm-eabi') + } catch (e) { + loadErrors.push(e) + } + + } else { + loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`)) } - break - case 'linux': - switch (arch) { - case 'x64': - if (isMusl()) { - localFileExisted = existsSync( - join(__dirname, 'backend-rs.linux-x64-musl.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./backend-rs.linux-x64-musl.node') - } else { - nativeBinding = require('backend-rs-linux-x64-musl') - } - } catch (e) { - loadError = e - } - } else { - localFileExisted = existsSync( - join(__dirname, 'backend-rs.linux-x64-gnu.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./backend-rs.linux-x64-gnu.node') - } else { - nativeBinding = require('backend-rs-linux-x64-gnu') - } - } catch (e) { - loadError = e - } - } - break - case 'arm64': - if (isMusl()) { - localFileExisted = existsSync( - join(__dirname, 'backend-rs.linux-arm64-musl.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./backend-rs.linux-arm64-musl.node') - } else { - nativeBinding = require('backend-rs-linux-arm64-musl') - } - } catch (e) { - loadError = e - } - } else { - localFileExisted = existsSync( - join(__dirname, 'backend-rs.linux-arm64-gnu.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./backend-rs.linux-arm64-gnu.node') - } else { - nativeBinding = require('backend-rs-linux-arm64-gnu') - } - } catch (e) { - loadError = e - } - } - break - case 'arm': - if (isMusl()) { - localFileExisted = existsSync( - join(__dirname, 'backend-rs.linux-arm-musleabihf.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./backend-rs.linux-arm-musleabihf.node') - } else { - nativeBinding = require('backend-rs-linux-arm-musleabihf') - } - } catch (e) { - loadError = e - } - } else { - localFileExisted = existsSync( - join(__dirname, 'backend-rs.linux-arm-gnueabihf.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./backend-rs.linux-arm-gnueabihf.node') - } else { - nativeBinding = require('backend-rs-linux-arm-gnueabihf') - } - } catch (e) { - loadError = e - } - } - break - case 'riscv64': - if (isMusl()) { - localFileExisted = existsSync( - join(__dirname, 'backend-rs.linux-riscv64-musl.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./backend-rs.linux-riscv64-musl.node') - } else { - nativeBinding = require('backend-rs-linux-riscv64-musl') - } - } catch (e) { - loadError = e - } - } else { - localFileExisted = existsSync( - join(__dirname, 'backend-rs.linux-riscv64-gnu.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./backend-rs.linux-riscv64-gnu.node') - } else { - nativeBinding = require('backend-rs-linux-riscv64-gnu') - } - } catch (e) { - loadError = e - } - } - break - case 's390x': - localFileExisted = existsSync( - join(__dirname, 'backend-rs.linux-s390x-gnu.node') - ) + } else if (process.platform === 'win32') { + if (process.arch === 'x64') { + try { + return require('./backend-rs.win32-x64-msvc.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('backend-rs-win32-x64-msvc') + } catch (e) { + loadErrors.push(e) + } + + } else if (process.arch === 'ia32') { + try { + return require('./backend-rs.win32-ia32-msvc.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('backend-rs-win32-ia32-msvc') + } catch (e) { + loadErrors.push(e) + } + + } else if (process.arch === 'arm64') { + try { + return require('./backend-rs.win32-arm64-msvc.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('backend-rs-win32-arm64-msvc') + } catch (e) { + loadErrors.push(e) + } + + } else { + loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`)) + } + } else if (process.platform === 'darwin') { + try { + return require('./backend-rs.darwin-universal.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('backend-rs-darwin-universal') + } catch (e) { + loadErrors.push(e) + } + + if (process.arch === 'x64') { + try { + return require('./backend-rs.darwin-x64.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('backend-rs-darwin-x64') + } catch (e) { + loadErrors.push(e) + } + + } else if (process.arch === 'arm64') { + try { + return require('./backend-rs.darwin-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('backend-rs-darwin-arm64') + } catch (e) { + loadErrors.push(e) + } + + } else { + loadErrors.push(new Error(`Unsupported architecture on macOS: ${process.arch}`)) + } + } else if (process.platform === 'freebsd') { + if (process.arch === 'x64') { + try { + return require('./backend-rs.freebsd-x64.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('backend-rs-freebsd-x64') + } catch (e) { + loadErrors.push(e) + } + + } else if (process.arch === 'arm64') { + try { + return require('./backend-rs.freebsd-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('backend-rs-freebsd-arm64') + } catch (e) { + loadErrors.push(e) + } + + } else { + loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`)) + } + } else if (process.platform === 'linux') { + if (process.arch === 'x64') { + if (isMusl()) { try { - if (localFileExisted) { - nativeBinding = require('./backend-rs.linux-s390x-gnu.node') - } else { - nativeBinding = require('backend-rs-linux-s390x-gnu') - } - } catch (e) { - loadError = e - } - break - default: - throw new Error(`Unsupported architecture on Linux: ${arch}`) + return require('./backend-rs.linux-x64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('backend-rs-linux-x64-musl') + } catch (e) { + loadErrors.push(e) + } + + } else { + try { + return require('./backend-rs.linux-x64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('backend-rs-linux-x64-gnu') + } catch (e) { + loadErrors.push(e) + } + + } + } else if (process.arch === 'arm64') { + if (isMusl()) { + try { + return require('./backend-rs.linux-arm64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('backend-rs-linux-arm64-musl') + } catch (e) { + loadErrors.push(e) + } + + } else { + try { + return require('./backend-rs.linux-arm64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('backend-rs-linux-arm64-gnu') + } catch (e) { + loadErrors.push(e) + } + + } + } else if (process.arch === 'arm') { + if (isMusl()) { + try { + return require('./backend-rs.linux-arm-musleabihf.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('backend-rs-linux-arm-musleabihf') + } catch (e) { + loadErrors.push(e) + } + + } else { + try { + return require('./backend-rs.linux-arm-gnueabihf.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('backend-rs-linux-arm-gnueabihf') + } catch (e) { + loadErrors.push(e) + } + + } + } else if (process.arch === 'riscv64') { + if (isMusl()) { + try { + return require('./backend-rs.linux-riscv64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('backend-rs-linux-riscv64-musl') + } catch (e) { + loadErrors.push(e) + } + + } else { + try { + return require('./backend-rs.linux-riscv64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('backend-rs-linux-riscv64-gnu') + } catch (e) { + loadErrors.push(e) + } + + } + } else if (process.arch === 'ppc64') { + try { + return require('./backend-rs.linux-ppc64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('backend-rs-linux-ppc64-gnu') + } catch (e) { + loadErrors.push(e) + } + + } else if (process.arch === 's390x') { + try { + return require('./backend-rs.linux-s390x-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('backend-rs-linux-s390x-gnu') + } catch (e) { + loadErrors.push(e) + } + + } else { + loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`)) } - break - default: - throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`) + } else { + loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`)) + } +} + +nativeBinding = requireNative() + +if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) { + try { + nativeBinding = require('./backend-rs.wasi.cjs') + } catch (err) { + if (process.env.NAPI_RS_FORCE_WASI) { + console.error(err) + } + } + if (!nativeBinding) { + try { + nativeBinding = require('backend-rs-wasm32-wasi') + } catch (err) { + if (process.env.NAPI_RS_FORCE_WASI) { + console.error(err) + } + } + } } if (!nativeBinding) { - if (loadError) { - throw loadError + if (loadErrors.length > 0) { + // TODO Link to documentation with potential fixes + // - The package owner could build/publish bindings for this arch + // - The user may need to bundle the correct files + // - The user may need to re-install node_modules to get new packages + throw new Error('Failed to load native binding', { cause: loadErrors }) } 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, 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, 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, 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 -module.exports.HOUR = HOUR -module.exports.DAY = DAY -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.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 -module.exports.isBlockedServer = isBlockedServer -module.exports.isSilencedServer = isSilencedServer -module.exports.isAllowedServer = isAllowedServer -module.exports.checkWordMute = checkWordMute -module.exports.getFullApAccount = getFullApAccount -module.exports.isSelfHost = isSelfHost -module.exports.isSameOrigin = isSameOrigin -module.exports.extractHost = extractHost -module.exports.toPuny = toPuny -module.exports.isUnicodeEmoji = isUnicodeEmoji -module.exports.sqlLikeEscape = sqlLikeEscape -module.exports.safeForSql = safeForSql -module.exports.formatMilliseconds = formatMilliseconds -module.exports.getImageSizeFromUrl = getImageSizeFromUrl -module.exports.getNoteSummary = getNoteSummary -module.exports.isQuote = isQuote -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 -module.exports.isOldPasswordAlgorithm = isOldPasswordAlgorithm -module.exports.decodeReaction = decodeReaction -module.exports.countReactions = countReactions -module.exports.toDbReaction = toDbReaction -module.exports.removeOldAttestationChallenges = removeOldAttestationChallenges -module.exports.cpuInfo = cpuInfo -module.exports.cpuUsage = cpuUsage -module.exports.memoryUsage = memoryUsage -module.exports.storageUsage = storageUsage -module.exports.AntennaSrc = AntennaSrc -module.exports.DriveFileUsageHint = DriveFileUsageHint -module.exports.MutedNoteReason = MutedNoteReason -module.exports.NoteVisibility = NoteVisibility -module.exports.NotificationType = NotificationType -module.exports.PageVisibility = PageVisibility -module.exports.PollNoteVisibility = PollNoteVisibility -module.exports.RelayStatus = RelayStatus -module.exports.UserEmojiModPerm = UserEmojiModPerm -module.exports.UserProfileFfvisibility = UserProfileFfvisibility -module.exports.UserProfileMutingNotificationTypes = UserProfileMutingNotificationTypes -module.exports.updateAntennasOnNewNote = updateAntennasOnNewNote -module.exports.watchNote = watchNote -module.exports.unwatchNote = unwatchNote -module.exports.PushNotificationKind = PushNotificationKind -module.exports.sendPushNotification = sendPushNotification -module.exports.publishToChannelStream = publishToChannelStream -module.exports.ChatEvent = ChatEvent -module.exports.publishToChatStream = publishToChatStream -module.exports.ChatIndexEvent = ChatIndexEvent -module.exports.publishToChatIndexStream = publishToChatIndexStream -module.exports.publishToBroadcastStream = publishToBroadcastStream -module.exports.publishToGroupChatStream = publishToGroupChatStream -module.exports.publishToModerationStream = publishToModerationStream -module.exports.getTimestamp = getTimestamp -module.exports.genId = genId -module.exports.genIdAt = genIdAt -module.exports.generateSecureRandomString = generateSecureRandomString -module.exports.generateUserToken = generateUserToken +module.exports.acctToString = nativeBinding.acctToString +module.exports.AntennaSrc = nativeBinding.AntennaSrc +module.exports.ChatEvent = nativeBinding.ChatEvent +module.exports.ChatIndexEvent = nativeBinding.ChatIndexEvent +module.exports.checkWordMute = nativeBinding.checkWordMute +module.exports.countReactions = nativeBinding.countReactions +module.exports.cpuInfo = nativeBinding.cpuInfo +module.exports.cpuUsage = nativeBinding.cpuUsage +module.exports.DAY = nativeBinding.DAY +module.exports.decodeReaction = nativeBinding.decodeReaction +module.exports.DriveFileEvent = nativeBinding.DriveFileEvent +module.exports.DriveFileUsageHint = nativeBinding.DriveFileUsageHint +module.exports.DriveFolderEvent = nativeBinding.DriveFolderEvent +module.exports.extractHost = nativeBinding.extractHost +module.exports.fetchMeta = nativeBinding.fetchMeta +module.exports.fetchNodeinfo = nativeBinding.fetchNodeinfo +module.exports.FILE_TYPE_BROWSERSAFE = nativeBinding.FILE_TYPE_BROWSERSAFE +module.exports.formatMilliseconds = nativeBinding.formatMilliseconds +module.exports.fromMastodonId = nativeBinding.fromMastodonId +module.exports.generateSecureRandomString = nativeBinding.generateSecureRandomString +module.exports.generateUserToken = nativeBinding.generateUserToken +module.exports.genId = nativeBinding.genId +module.exports.genIdAt = nativeBinding.genIdAt +module.exports.getFullApAccount = nativeBinding.getFullApAccount +module.exports.getImageSizeFromUrl = nativeBinding.getImageSizeFromUrl +module.exports.getNoteSummary = nativeBinding.getNoteSummary +module.exports.getTimestamp = nativeBinding.getTimestamp +module.exports.greet = nativeBinding.greet +module.exports.hashPassword = nativeBinding.hashPassword +module.exports.HOUR = nativeBinding.HOUR +module.exports.Inbound = nativeBinding.Inbound +module.exports.initializeRustLogger = nativeBinding.initializeRustLogger +module.exports.isAllowedServer = nativeBinding.isAllowedServer +module.exports.isBlockedServer = nativeBinding.isBlockedServer +module.exports.isOldPasswordAlgorithm = nativeBinding.isOldPasswordAlgorithm +module.exports.isQuote = nativeBinding.isQuote +module.exports.isSafeUrl = nativeBinding.isSafeUrl +module.exports.isSameOrigin = nativeBinding.isSameOrigin +module.exports.isSelfHost = nativeBinding.isSelfHost +module.exports.isSilencedServer = nativeBinding.isSilencedServer +module.exports.isUnicodeEmoji = nativeBinding.isUnicodeEmoji +module.exports.latestVersion = nativeBinding.latestVersion +module.exports.loadConfig = nativeBinding.loadConfig +module.exports.memoryUsage = nativeBinding.memoryUsage +module.exports.metaToPugArgs = nativeBinding.metaToPugArgs +module.exports.MINUTE = nativeBinding.MINUTE +module.exports.MutedNoteReason = nativeBinding.MutedNoteReason +module.exports.nodeinfo_2_0 = nativeBinding.nodeinfo_2_0 +module.exports.nodeinfo_2_1 = nativeBinding.nodeinfo_2_1 +module.exports.NoteVisibility = nativeBinding.NoteVisibility +module.exports.NotificationType = nativeBinding.NotificationType +module.exports.nyaify = nativeBinding.nyaify +module.exports.Outbound = nativeBinding.Outbound +module.exports.PageVisibility = nativeBinding.PageVisibility +module.exports.PollNoteVisibility = nativeBinding.PollNoteVisibility +module.exports.Protocol = nativeBinding.Protocol +module.exports.publishToBroadcastStream = nativeBinding.publishToBroadcastStream +module.exports.publishToChannelStream = nativeBinding.publishToChannelStream +module.exports.publishToChatIndexStream = nativeBinding.publishToChatIndexStream +module.exports.publishToChatStream = nativeBinding.publishToChatStream +module.exports.publishToDriveFileStream = nativeBinding.publishToDriveFileStream +module.exports.publishToDriveFolderStream = nativeBinding.publishToDriveFolderStream +module.exports.publishToGroupChatStream = nativeBinding.publishToGroupChatStream +module.exports.publishToModerationStream = nativeBinding.publishToModerationStream +module.exports.publishToNotesStream = nativeBinding.publishToNotesStream +module.exports.PushNotificationKind = nativeBinding.PushNotificationKind +module.exports.RelayStatus = nativeBinding.RelayStatus +module.exports.removeOldAttestationChallenges = nativeBinding.removeOldAttestationChallenges +module.exports.safeForSql = nativeBinding.safeForSql +module.exports.SECOND = nativeBinding.SECOND +module.exports.sendPushNotification = nativeBinding.sendPushNotification +module.exports.showServerInfo = nativeBinding.showServerInfo +module.exports.sqlLikeEscape = nativeBinding.sqlLikeEscape +module.exports.storageUsage = nativeBinding.storageUsage +module.exports.stringToAcct = nativeBinding.stringToAcct +module.exports.toDbReaction = nativeBinding.toDbReaction +module.exports.toMastodonId = nativeBinding.toMastodonId +module.exports.toPuny = nativeBinding.toPuny +module.exports.unwatchNote = nativeBinding.unwatchNote +module.exports.updateAntennaCache = nativeBinding.updateAntennaCache +module.exports.updateAntennasOnNewNote = nativeBinding.updateAntennasOnNewNote +module.exports.updateMetaCache = nativeBinding.updateMetaCache +module.exports.updateNodeinfoCache = nativeBinding.updateNodeinfoCache +module.exports.USER_ACTIVE_THRESHOLD = nativeBinding.USER_ACTIVE_THRESHOLD +module.exports.USER_ONLINE_THRESHOLD = nativeBinding.USER_ONLINE_THRESHOLD +module.exports.UserEmojiModPerm = nativeBinding.UserEmojiModPerm +module.exports.UserProfileFfvisibility = nativeBinding.UserProfileFfvisibility +module.exports.UserProfileMutingNotificationTypes = nativeBinding.UserProfileMutingNotificationTypes +module.exports.verifyPassword = nativeBinding.verifyPassword +module.exports.watchNote = nativeBinding.watchNote diff --git a/packages/backend-rs/npm/android-arm-eabi/README.md b/packages/backend-rs/npm/android-arm-eabi/README.md deleted file mode 100644 index 26e84675de..0000000000 --- a/packages/backend-rs/npm/android-arm-eabi/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `backend-rs-android-arm-eabi` - -This is the **armv7-linux-androideabi** binary for `backend-rs` diff --git a/packages/backend-rs/npm/android-arm-eabi/package.json b/packages/backend-rs/npm/android-arm-eabi/package.json deleted file mode 100644 index ca06ebf7f3..0000000000 --- a/packages/backend-rs/npm/android-arm-eabi/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "backend-rs-android-arm-eabi", - "version": "0.0.0", - "os": [ - "android" - ], - "cpu": [ - "arm" - ], - "main": "backend-rs.android-arm-eabi.node", - "files": [ - "backend-rs.android-arm-eabi.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - } -} diff --git a/packages/backend-rs/npm/android-arm64/README.md b/packages/backend-rs/npm/android-arm64/README.md deleted file mode 100644 index f18da53ca3..0000000000 --- a/packages/backend-rs/npm/android-arm64/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `backend-rs-android-arm64` - -This is the **aarch64-linux-android** binary for `backend-rs` diff --git a/packages/backend-rs/npm/android-arm64/package.json b/packages/backend-rs/npm/android-arm64/package.json deleted file mode 100644 index b37c2f5b7c..0000000000 --- a/packages/backend-rs/npm/android-arm64/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "backend-rs-android-arm64", - "version": "0.0.0", - "os": [ - "android" - ], - "cpu": [ - "arm64" - ], - "main": "backend-rs.android-arm64.node", - "files": [ - "backend-rs.android-arm64.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - } -} diff --git a/packages/backend-rs/npm/darwin-arm64/README.md b/packages/backend-rs/npm/darwin-arm64/README.md deleted file mode 100644 index 4c2bb5f7de..0000000000 --- a/packages/backend-rs/npm/darwin-arm64/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `backend-rs-darwin-arm64` - -This is the **aarch64-apple-darwin** binary for `backend-rs` diff --git a/packages/backend-rs/npm/darwin-arm64/package.json b/packages/backend-rs/npm/darwin-arm64/package.json deleted file mode 100644 index 2ed7ccd061..0000000000 --- a/packages/backend-rs/npm/darwin-arm64/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "backend-rs-darwin-arm64", - "version": "0.0.0", - "os": [ - "darwin" - ], - "cpu": [ - "arm64" - ], - "main": "backend-rs.darwin-arm64.node", - "files": [ - "backend-rs.darwin-arm64.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - } -} diff --git a/packages/backend-rs/npm/darwin-universal/README.md b/packages/backend-rs/npm/darwin-universal/README.md deleted file mode 100644 index fe9e88d1e4..0000000000 --- a/packages/backend-rs/npm/darwin-universal/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `backend-rs-darwin-universal` - -This is the **universal-apple-darwin** binary for `backend-rs` diff --git a/packages/backend-rs/npm/darwin-universal/package.json b/packages/backend-rs/npm/darwin-universal/package.json deleted file mode 100644 index 125f60807a..0000000000 --- a/packages/backend-rs/npm/darwin-universal/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "backend-rs-darwin-universal", - "version": "0.0.0", - "os": [ - "darwin" - ], - "main": "backend-rs.darwin-universal.node", - "files": [ - "backend-rs.darwin-universal.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - } -} diff --git a/packages/backend-rs/npm/darwin-x64/README.md b/packages/backend-rs/npm/darwin-x64/README.md deleted file mode 100644 index e7cb3c7341..0000000000 --- a/packages/backend-rs/npm/darwin-x64/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `backend-rs-darwin-x64` - -This is the **x86_64-apple-darwin** binary for `backend-rs` diff --git a/packages/backend-rs/npm/darwin-x64/package.json b/packages/backend-rs/npm/darwin-x64/package.json deleted file mode 100644 index 6a90d0628e..0000000000 --- a/packages/backend-rs/npm/darwin-x64/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "backend-rs-darwin-x64", - "version": "0.0.0", - "os": [ - "darwin" - ], - "cpu": [ - "x64" - ], - "main": "backend-rs.darwin-x64.node", - "files": [ - "backend-rs.darwin-x64.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - } -} diff --git a/packages/backend-rs/npm/freebsd-x64/README.md b/packages/backend-rs/npm/freebsd-x64/README.md deleted file mode 100644 index df06915dff..0000000000 --- a/packages/backend-rs/npm/freebsd-x64/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `backend-rs-freebsd-x64` - -This is the **x86_64-unknown-freebsd** binary for `backend-rs` diff --git a/packages/backend-rs/npm/freebsd-x64/package.json b/packages/backend-rs/npm/freebsd-x64/package.json deleted file mode 100644 index 2ba1244845..0000000000 --- a/packages/backend-rs/npm/freebsd-x64/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "backend-rs-freebsd-x64", - "version": "0.0.0", - "os": [ - "freebsd" - ], - "cpu": [ - "x64" - ], - "main": "backend-rs.freebsd-x64.node", - "files": [ - "backend-rs.freebsd-x64.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - } -} diff --git a/packages/backend-rs/npm/linux-arm-gnueabihf/README.md b/packages/backend-rs/npm/linux-arm-gnueabihf/README.md deleted file mode 100644 index fc4132cd0e..0000000000 --- a/packages/backend-rs/npm/linux-arm-gnueabihf/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `backend-rs-linux-arm-gnueabihf` - -This is the **armv7-unknown-linux-gnueabihf** binary for `backend-rs` diff --git a/packages/backend-rs/npm/linux-arm-gnueabihf/package.json b/packages/backend-rs/npm/linux-arm-gnueabihf/package.json deleted file mode 100644 index 6e878bc516..0000000000 --- a/packages/backend-rs/npm/linux-arm-gnueabihf/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "backend-rs-linux-arm-gnueabihf", - "version": "0.0.0", - "os": [ - "linux" - ], - "cpu": [ - "arm" - ], - "main": "backend-rs.linux-arm-gnueabihf.node", - "files": [ - "backend-rs.linux-arm-gnueabihf.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - } -} diff --git a/packages/backend-rs/npm/linux-arm64-gnu/README.md b/packages/backend-rs/npm/linux-arm64-gnu/README.md deleted file mode 100644 index 9126a63170..0000000000 --- a/packages/backend-rs/npm/linux-arm64-gnu/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `backend-rs-linux-arm64-gnu` - -This is the **aarch64-unknown-linux-gnu** binary for `backend-rs` diff --git a/packages/backend-rs/npm/linux-arm64-gnu/package.json b/packages/backend-rs/npm/linux-arm64-gnu/package.json deleted file mode 100644 index 27b87ae1d4..0000000000 --- a/packages/backend-rs/npm/linux-arm64-gnu/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "backend-rs-linux-arm64-gnu", - "version": "0.0.0", - "os": [ - "linux" - ], - "cpu": [ - "arm64" - ], - "main": "backend-rs.linux-arm64-gnu.node", - "files": [ - "backend-rs.linux-arm64-gnu.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - }, - "libc": [ - "glibc" - ] -} diff --git a/packages/backend-rs/npm/linux-arm64-musl/README.md b/packages/backend-rs/npm/linux-arm64-musl/README.md deleted file mode 100644 index 970d094e46..0000000000 --- a/packages/backend-rs/npm/linux-arm64-musl/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `backend-rs-linux-arm64-musl` - -This is the **aarch64-unknown-linux-musl** binary for `backend-rs` diff --git a/packages/backend-rs/npm/linux-arm64-musl/package.json b/packages/backend-rs/npm/linux-arm64-musl/package.json deleted file mode 100644 index 49f83fb046..0000000000 --- a/packages/backend-rs/npm/linux-arm64-musl/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "backend-rs-linux-arm64-musl", - "version": "0.0.0", - "os": [ - "linux" - ], - "cpu": [ - "arm64" - ], - "main": "backend-rs.linux-arm64-musl.node", - "files": [ - "backend-rs.linux-arm64-musl.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - }, - "libc": [ - "musl" - ] -} diff --git a/packages/backend-rs/npm/linux-x64-gnu/README.md b/packages/backend-rs/npm/linux-x64-gnu/README.md deleted file mode 100644 index 6a947c5e7f..0000000000 --- a/packages/backend-rs/npm/linux-x64-gnu/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `backend-rs-linux-x64-gnu` - -This is the **x86_64-unknown-linux-gnu** binary for `backend-rs` diff --git a/packages/backend-rs/npm/linux-x64-gnu/package.json b/packages/backend-rs/npm/linux-x64-gnu/package.json deleted file mode 100644 index 7ba56d5212..0000000000 --- a/packages/backend-rs/npm/linux-x64-gnu/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "backend-rs-linux-x64-gnu", - "version": "0.0.0", - "os": [ - "linux" - ], - "cpu": [ - "x64" - ], - "main": "backend-rs.linux-x64-gnu.node", - "files": [ - "backend-rs.linux-x64-gnu.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - }, - "libc": [ - "glibc" - ] -} diff --git a/packages/backend-rs/npm/linux-x64-musl/README.md b/packages/backend-rs/npm/linux-x64-musl/README.md deleted file mode 100644 index 4344d648ae..0000000000 --- a/packages/backend-rs/npm/linux-x64-musl/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `backend-rs-linux-x64-musl` - -This is the **x86_64-unknown-linux-musl** binary for `backend-rs` diff --git a/packages/backend-rs/npm/linux-x64-musl/package.json b/packages/backend-rs/npm/linux-x64-musl/package.json deleted file mode 100644 index fcfa783c83..0000000000 --- a/packages/backend-rs/npm/linux-x64-musl/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "backend-rs-linux-x64-musl", - "version": "0.0.0", - "os": [ - "linux" - ], - "cpu": [ - "x64" - ], - "main": "backend-rs.linux-x64-musl.node", - "files": [ - "backend-rs.linux-x64-musl.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - }, - "libc": [ - "musl" - ] -} diff --git a/packages/backend-rs/npm/win32-arm64-msvc/README.md b/packages/backend-rs/npm/win32-arm64-msvc/README.md deleted file mode 100644 index a89398e514..0000000000 --- a/packages/backend-rs/npm/win32-arm64-msvc/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `backend-rs-win32-arm64-msvc` - -This is the **aarch64-pc-windows-msvc** binary for `backend-rs` diff --git a/packages/backend-rs/npm/win32-arm64-msvc/package.json b/packages/backend-rs/npm/win32-arm64-msvc/package.json deleted file mode 100644 index 1783c23b00..0000000000 --- a/packages/backend-rs/npm/win32-arm64-msvc/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "backend-rs-win32-arm64-msvc", - "version": "0.0.0", - "os": [ - "win32" - ], - "cpu": [ - "arm64" - ], - "main": "backend-rs.win32-arm64-msvc.node", - "files": [ - "backend-rs.win32-arm64-msvc.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - } -} diff --git a/packages/backend-rs/npm/win32-ia32-msvc/README.md b/packages/backend-rs/npm/win32-ia32-msvc/README.md deleted file mode 100644 index 43335cfad0..0000000000 --- a/packages/backend-rs/npm/win32-ia32-msvc/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `backend-rs-win32-ia32-msvc` - -This is the **i686-pc-windows-msvc** binary for `backend-rs` diff --git a/packages/backend-rs/npm/win32-ia32-msvc/package.json b/packages/backend-rs/npm/win32-ia32-msvc/package.json deleted file mode 100644 index 854de42b14..0000000000 --- a/packages/backend-rs/npm/win32-ia32-msvc/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "backend-rs-win32-ia32-msvc", - "version": "0.0.0", - "os": [ - "win32" - ], - "cpu": [ - "ia32" - ], - "main": "backend-rs.win32-ia32-msvc.node", - "files": [ - "backend-rs.win32-ia32-msvc.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - } -} diff --git a/packages/backend-rs/npm/win32-x64-msvc/README.md b/packages/backend-rs/npm/win32-x64-msvc/README.md deleted file mode 100644 index c881bdd12d..0000000000 --- a/packages/backend-rs/npm/win32-x64-msvc/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `backend-rs-win32-x64-msvc` - -This is the **x86_64-pc-windows-msvc** binary for `backend-rs` diff --git a/packages/backend-rs/npm/win32-x64-msvc/package.json b/packages/backend-rs/npm/win32-x64-msvc/package.json deleted file mode 100644 index eb01ab78ef..0000000000 --- a/packages/backend-rs/npm/win32-x64-msvc/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "backend-rs-win32-x64-msvc", - "version": "0.0.0", - "os": [ - "win32" - ], - "cpu": [ - "x64" - ], - "main": "backend-rs.win32-x64-msvc.node", - "files": [ - "backend-rs.win32-x64-msvc.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - } -} diff --git a/packages/backend-rs/package.json b/packages/backend-rs/package.json index 4923f269b3..7b5ad60703 100644 --- a/packages/backend-rs/package.json +++ b/packages/backend-rs/package.json @@ -4,32 +4,13 @@ "main": "built/index.js", "types": "built/index.d.ts", "napi": { - "name": "backend-rs", - "triples": { - "additional": [ - "aarch64-apple-darwin", - "aarch64-linux-android", - "aarch64-unknown-linux-gnu", - "aarch64-unknown-linux-musl", - "aarch64-pc-windows-msvc", - "armv7-unknown-linux-gnueabihf", - "x86_64-unknown-linux-musl", - "x86_64-unknown-freebsd", - "i686-pc-windows-msvc", - "armv7-linux-androideabi", - "universal-apple-darwin" - ] - } + "binaryName": "backend-rs" }, "devDependencies": { - "@napi-rs/cli": "2.18.3" + "@napi-rs/cli": "3.0.0-alpha.55" }, "scripts": { - "artifacts": "napi artifacts", - "build": "napi build --features napi --no-const-enum --platform --release ./built/", - "build:debug": "napi build --features napi --no-const-enum --platform ./built/", - "prepublishOnly": "napi prepublish -t npm", - "universal": "napi universal", - "version": "napi version" + "build": "napi build --features napi --no-const-enum --platform --release --output-dir ./built/", + "build:debug": "napi build --features napi --no-const-enum --platform --output-dir ./built/ --dts-header '/* auto-generated by NAPI-RS */\n/* Do NOT edit this file manually */\n\ntype DateTimeWithTimeZone = Date;\n\ntype Json = any;\n\n'" } } diff --git a/packages/backend-rs/src/config/constant.rs b/packages/backend-rs/src/config/constant.rs index db6e7bed00..7e6b50fa52 100644 --- a/packages/backend-rs/src/config/constant.rs +++ b/packages/backend-rs/src/config/constant.rs @@ -1,15 +1,17 @@ -#[crate::ts_export] +//! This module is used in the TypeScript backend only. + +#[macros::ts_export] pub const SECOND: i32 = 1000; -#[crate::ts_export] +#[macros::ts_export] pub const MINUTE: i32 = 60 * SECOND; -#[crate::ts_export] +#[macros::ts_export] pub const HOUR: i32 = 60 * MINUTE; -#[crate::ts_export] +#[macros::ts_export] pub const DAY: i32 = 24 * HOUR; -#[crate::ts_export] +#[macros::ts_export] pub const USER_ONLINE_THRESHOLD: i32 = 10 * MINUTE; -#[crate::ts_export] +#[macros::ts_export] pub const USER_ACTIVE_THRESHOLD: i32 = 3 * DAY; /// List of file types allowed to be viewed directly in the browser @@ -19,7 +21,7 @@ pub const USER_ACTIVE_THRESHOLD: i32 = 3 * DAY; /// * /// * /// * -#[crate::ts_export] +#[macros::ts_export] pub const FILE_TYPE_BROWSERSAFE: [&str; 41] = [ // Images "image/png", diff --git a/packages/backend-rs/src/config/environment.rs b/packages/backend-rs/src/config/environment.rs deleted file mode 100644 index 1825af326a..0000000000 --- a/packages/backend-rs/src/config/environment.rs +++ /dev/null @@ -1,27 +0,0 @@ -// FIXME: Are these options used? -#[crate::export(object)] -pub struct EnvConfig { - pub only_queue: bool, - pub only_server: bool, - pub no_daemons: bool, - pub disable_clustering: bool, - pub verbose: bool, - pub with_log_time: bool, - pub slow: bool, -} - -#[crate::export] -pub fn load_env() -> EnvConfig { - let node_env = std::env::var("NODE_ENV").unwrap_or_default().to_lowercase(); - let is_testing = node_env == "test"; - - EnvConfig { - only_queue: std::env::var("MK_ONLY_QUEUE").is_ok(), - only_server: std::env::var("MK_ONLY_SERVER").is_ok(), - no_daemons: is_testing || std::env::var("MK_NO_DAEMONS").is_ok(), - disable_clustering: is_testing || std::env::var("MK_DISABLE_CLUSTERING").is_ok(), - verbose: std::env::var("MK_VERBOSE").is_ok(), - with_log_time: std::env::var("MK_WITH_LOG_TIME").is_ok(), - slow: std::env::var("MK_SLOW").is_ok(), - } -} diff --git a/packages/backend-rs/src/misc/meta.rs b/packages/backend-rs/src/config/meta.rs similarity index 76% rename from packages/backend-rs/src/misc/meta.rs rename to packages/backend-rs/src/config/meta.rs index 5aed617038..969d9d35f5 100644 --- a/packages/backend-rs/src/misc/meta.rs +++ b/packages/backend-rs/src/config/meta.rs @@ -1,18 +1,28 @@ -use crate::database::db_conn; -use crate::model::entity::meta; -use rand::prelude::*; +//! Server information + +use crate::{database::db_conn, model::entity::meta}; use sea_orm::{prelude::*, ActiveValue}; use std::sync::Mutex; type Meta = meta::Model; static CACHE: Mutex> = 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 { +#[macros::export(js_name = "fetchMeta")] +pub async fn local_server_info() -> Result { + local_server_info_impl(true).await +} + +#[macros::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 { // try using cache if use_cache { if let Some(cache) = CACHE.lock().ok().and_then(|cache| cache.clone()) { @@ -24,7 +34,7 @@ pub async fn fetch_meta(use_cache: bool) -> Result { 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,11 +45,11 @@ pub async fn fetch_meta(use_cache: bool) -> Result { }) .exec_with_returning(db) .await?; - update_cache(&meta); + set_cache(&meta); Ok(meta) } -#[crate::export(object)] +#[macros::export(object)] pub struct PugArgs { pub img: Option, pub title: String, @@ -52,8 +62,9 @@ pub struct PugArgs { pub private_mode: Option, } -#[crate::export] +#[macros::ts_export] pub fn meta_to_pug_args(meta: Meta) -> PugArgs { + use rand::prelude::*; let mut rng = rand::thread_rng(); let splash_icon = meta diff --git a/packages/backend-rs/src/config/mod.rs b/packages/backend-rs/src/config/mod.rs index 0e8056a894..a1cfb7fd75 100644 --- a/packages/backend-rs/src/config/mod.rs +++ b/packages/backend-rs/src/config/mod.rs @@ -1,7 +1,8 @@ //! 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; diff --git a/packages/backend-rs/src/config/server.rs b/packages/backend-rs/src/config/server.rs index 26babb4953..81f2baedb0 100644 --- a/packages/backend-rs/src/config/server.rs +++ b/packages/backend-rs/src/config/server.rs @@ -1,13 +1,14 @@ +//! Server configuration + use once_cell::sync::Lazy; use serde::Deserialize; -use std::env; -use std::fs; +use std::{env, fs}; -pub const VERSION: &str = macro_rs::read_version_from_package_json!(); +pub const VERSION: &str = macros::read_version_from_package_json!(); -#[derive(Clone, Debug, PartialEq, Deserialize)] +#[derive(Deserialize)] #[serde(rename_all = "camelCase")] -#[crate::export(object, use_nullable = false)] +#[macros::export(object, use_nullable = false)] struct ServerConfig { pub url: String, pub port: u16, @@ -70,9 +71,9 @@ struct ServerConfig { pub object_storage: Option, } -#[derive(Clone, Debug, PartialEq, Deserialize)] +#[derive(Deserialize)] #[serde(rename_all = "camelCase")] -#[crate::export(object, use_nullable = false)] +#[macros::export(object, use_nullable = false)] pub struct DbConfig { pub host: String, pub port: u16, @@ -83,9 +84,9 @@ pub struct DbConfig { pub extra: Option, } -#[derive(Clone, Debug, PartialEq, Deserialize)] +#[derive(Deserialize)] #[serde(rename_all = "camelCase")] -#[crate::export(object, use_nullable = false)] +#[macros::export(object, use_nullable = false)] pub struct RedisConfig { pub host: String, pub port: u16, @@ -98,65 +99,65 @@ pub struct RedisConfig { pub prefix: Option, } -#[derive(Clone, Debug, PartialEq, Deserialize)] +#[derive(Deserialize)] #[serde(rename_all = "camelCase")] -#[crate::export(object, use_nullable = false)] +#[macros::export(object, use_nullable = false)] pub struct TlsConfig { pub host: String, pub reject_unauthorized: bool, } -#[crate::export(object, use_nullable = false)] +#[macros::export(object, use_nullable = false)] pub struct WorkerConfig { pub web: u32, pub queue: u32, } -#[derive(Clone, Debug, PartialEq, Deserialize)] +#[derive(Deserialize)] #[serde(rename_all = "camelCase")] -#[crate::export(object, use_nullable = false)] +#[macros::export(object, use_nullable = false)] pub struct WorkerConfigInternal { pub web: Option, pub queue: Option, } -#[derive(Clone, Debug, PartialEq, Deserialize)] +#[derive(Deserialize)] #[serde(rename_all = "camelCase")] -#[crate::export(object, use_nullable = false)] +#[macros::export(object, use_nullable = false)] pub struct IdConfig { pub length: Option, pub fingerprint: Option, } -#[derive(Clone, Debug, PartialEq, Deserialize)] +#[derive(Deserialize)] #[serde(rename_all = "camelCase")] -#[crate::export(object, use_nullable = false)] +#[macros::export(object, use_nullable = false)] pub struct SysLogConfig { pub host: String, pub port: u16, } -#[derive(Clone, Debug, PartialEq, Deserialize)] +#[derive(Deserialize)] #[serde(rename_all = "camelCase")] -#[crate::export(object, use_nullable = false)] +#[macros::export(object, use_nullable = false)] pub struct DeepLConfig { pub managed: Option, pub auth_key: Option, pub is_pro: Option, } -#[derive(Clone, Debug, PartialEq, Deserialize)] +#[derive(Deserialize)] #[serde(rename_all = "camelCase")] -#[crate::export(object, use_nullable = false)] +#[macros::export(object, use_nullable = false)] pub struct LibreTranslateConfig { pub managed: Option, pub api_url: Option, pub api_key: Option, } -#[derive(Clone, Debug, PartialEq, Deserialize)] +#[derive(Deserialize)] #[serde(rename_all = "camelCase")] -#[crate::export(object, use_nullable = false)] +#[macros::export(object, use_nullable = false)] pub struct EmailConfig { pub managed: Option, pub address: Option, @@ -167,9 +168,9 @@ pub struct EmailConfig { pub use_implicit_ssl_tls: Option, } -#[derive(Clone, Debug, PartialEq, Deserialize)] +#[derive(Deserialize)] #[serde(rename_all = "camelCase")] -#[crate::export(object, use_nullable = false)] +#[macros::export(object, use_nullable = false)] pub struct ObjectStorageConfig { pub managed: Option, pub base_url: Option, @@ -185,7 +186,7 @@ pub struct ObjectStorageConfig { pub s3_force_path_style: Option, } -#[crate::export(object, use_nullable = false)] +#[macros::export(object, use_nullable = false)] pub struct Config { // ServerConfig (from default.yml) pub url: String, @@ -262,7 +263,7 @@ fn read_config_file() -> ServerConfig { data } -#[crate::export] +#[macros::export] pub fn load_config() -> Config { let server_config = read_config_file(); let version = VERSION.to_owned(); @@ -276,7 +277,10 @@ pub fn load_config() -> Config { None => hostname.clone(), }; let scheme = url.scheme().to_owned(); - let ws_scheme = scheme.replace("http", "ws"); + let ws_scheme = match scheme.as_str() { + "http" => "ws", + _ => "wss", + }; let cluster_limits = match server_config.cluster_limits { Some(cl) => WorkerConfig { @@ -291,7 +295,7 @@ pub fn load_config() -> Config { } else { server_config.redis.prefix.clone() } - .unwrap_or(hostname.clone()); + .unwrap_or_else(|| hostname.clone()); Config { url: server_config.url, @@ -342,7 +346,7 @@ pub fn load_config() -> Config { hostname, redis_key_prefix, scheme, - ws_scheme, + ws_scheme: ws_scheme.to_string(), } } diff --git a/packages/backend-rs/src/database/cache.rs b/packages/backend-rs/src/database/cache.rs index bf675efa38..2fe192cb1e 100644 --- a/packages/backend-rs/src/database/cache.rs +++ b/packages/backend-rs/src/database/cache.rs @@ -4,29 +4,23 @@ use crate::database::{redis_conn, redis_key, RedisConnError}; use redis::{AsyncCommands, RedisError}; use serde::{Deserialize, Serialize}; -#[derive(strum::Display, Debug)] +#[cfg_attr(test, derive(Debug))] pub enum Category { - #[strum(serialize = "fetchUrl")] FetchUrl, - #[strum(serialize = "blocking")] Block, - #[strum(serialize = "following")] Follow, #[cfg(test)] - #[strum(serialize = "usedOnlyForTesting")] Test, } #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("Redis error: {0}")] + #[error("failed to execute Redis command")] Redis(#[from] RedisError), - #[error("Redis connection error: {0}")] + #[error("bad Redis connection")] 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 data for Redis")] + Encode(#[from] rmp_serde::encode::Error), } #[inline] @@ -34,9 +28,15 @@ fn prefix_key(key: &str) -> String { redis_key(format!("cache:{}", key)) } -#[inline] fn categorize(category: Category, key: &str) -> String { - format!("{}:{}", category, key) + let prefix = match category { + Category::FetchUrl => "fetchUrl", + Category::Block => "blocking", + Category::Follow => "following", + #[cfg(test)] + Category::Test => "usedOnlyForTesting", + }; + format!("{}:{}", prefix, key) } #[inline] @@ -50,9 +50,9 @@ fn wildcard(category: Category) -> String { /// /// # Arguments /// -/// - `key` : key (prefixed automatically) -/// - `value` : (de)serializable value -/// - `expire_seconds` : TTL +/// * `key` : key (prefixed automatically) +/// * `value` : (de)serializable value +/// * `expire_seconds` : TTL /// /// # Example /// @@ -96,7 +96,7 @@ pub async fn set Deserialize<'a> + Serialize>( /// /// # Argument /// -/// - `key` : key (will be prefixed automatically) +/// * `key` : key (will be prefixed automatically) /// /// # Example /// @@ -123,7 +123,7 @@ pub async fn set Deserialize<'a> + Serialize>( pub async fn get Deserialize<'a> + Serialize>(key: &str) -> Result, Error> { let serialized_value: Option> = redis_conn().await?.get(prefix_key(key)).await?; Ok(match serialized_value { - Some(v) => Some(rmp_serde::from_slice::(v.as_ref())?), + Some(v) => rmp_serde::from_slice::(v.as_ref()).ok(), None => None, }) } @@ -135,9 +135,9 @@ pub async fn get Deserialize<'a> + Serialize>(key: &str) -> Result Result<(), Error> { /// /// # Arguments /// -/// - `category` : one of [Category] -/// - `key` : key (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 Deserialize<'a> + Serialize>( category: Category, key: &str, @@ -188,8 +188,8 @@ pub async fn set_one Deserialize<'a> + Serialize>( /// /// # Arguments /// -/// - `category` : one of [Category] -/// - `key` : key (prefixed automatically) +/// * `category` : one of [Category] +/// * `key` : key (prefixed automatically) pub async fn get_one Deserialize<'a> + Serialize>( category: Category, key: &str, @@ -213,7 +213,7 @@ pub async fn delete_one(category: Category, key: &str) -> Result<(), Error> { /// /// # 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> = redis.keys(wildcard(category)).await?; @@ -234,6 +234,7 @@ mod unit_test { use pretty_assertions::assert_eq; #[tokio::test] + #[cfg_attr(miri, ignore)] // can't call foreign function `getaddrinfo` on OS `linux` async fn set_get_expire() { #[derive(serde::Deserialize, serde::Serialize, PartialEq, Debug)] struct Data { @@ -278,6 +279,7 @@ mod unit_test { } #[tokio::test] + #[cfg_attr(miri, ignore)] // can't call foreign function `getaddrinfo` on OS `linux` async fn use_category() { let key_1 = "fire"; let key_2 = "fish"; diff --git a/packages/backend-rs/src/database/mod.rs b/packages/backend-rs/src/database/mod.rs index 24710ca792..03315c1080 100644 --- a/packages/backend-rs/src/database/mod.rs +++ b/packages/backend-rs/src/database/mod.rs @@ -1,8 +1,9 @@ //! Interfaces for accessing PostgreSQL and Redis -pub use postgresql::db_conn; +pub use postgresql::get_conn as db_conn; + +pub use redis::get_conn as redis_conn; pub use redis::key as redis_key; -pub use redis::redis_conn; pub use redis::RedisConnError; pub mod cache; diff --git a/packages/backend-rs/src/database/postgresql.rs b/packages/backend-rs/src/database/postgresql.rs index ed68227419..fbc7fe7018 100644 --- a/packages/backend-rs/src/database/postgresql.rs +++ b/packages/backend-rs/src/database/postgresql.rs @@ -3,6 +3,7 @@ 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 = OnceCell::new(); @@ -18,6 +19,7 @@ async fn init_conn() -> 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"); @@ -27,7 +29,7 @@ async fn init_conn() -> Result<&'static DbConn, DbErr> { } /// Returns an async PostgreSQL connection that can be used with [sea_orm] utilities. -pub async fn db_conn() -> Result<&'static DbConn, DbErr> { +pub async fn get_conn() -> Result<&'static DbConn, DbErr> { match DB_CONN.get() { Some(conn) => Ok(conn), None => init_conn().await, @@ -36,11 +38,52 @@ pub async fn db_conn() -> Result<&'static DbConn, DbErr> { #[cfg(test)] mod unit_test { - use super::db_conn; + use super::get_conn; + use sea_orm::{prelude::*, DbBackend, Statement}; #[tokio::test] - async fn connect() { - assert!(db_conn().await.is_ok()); - assert!(db_conn().await.is_ok()); + #[cfg_attr(miri, ignore)] // can't call foreign function `geteuid` on OS `linux` + async fn connect_sequential() { + get_conn().await.unwrap(); + get_conn().await.unwrap(); + get_conn().await.unwrap(); + get_conn().await.unwrap(); + get_conn().await.unwrap(); + } + + #[tokio::test] + #[cfg_attr(miri, ignore)] // can't call foreign function `geteuid` on OS `linux` + async fn connect_concurrent() { + let [c1, c2, c3, c4, c5] = [get_conn(), get_conn(), get_conn(), get_conn(), get_conn()]; + let _ = tokio::try_join!(c1, c2, c3, c4, c5).unwrap(); + } + + #[tokio::test] + #[cfg_attr(miri, ignore)] // can't call foreign function `geteuid` on OS `linux` + async fn connect_spawn() { + let mut tasks = Vec::new(); + + for _ in 0..5 { + tasks.push(tokio::spawn(get_conn())); + } + for task in tasks { + task.await.unwrap().unwrap(); + } + } + + #[tokio::test] + #[cfg_attr(miri, ignore)] // can't call foreign function `geteuid` on OS `linux` + async fn access() { + // DO NOT write any raw SQL query in the actual program + // (with the exception of PGroonga features) + get_conn() + .await + .unwrap() + .execute(Statement::from_string( + DbBackend::Postgres, + "SELECT version()", + )) + .await + .unwrap(); } } diff --git a/packages/backend-rs/src/database/redis.rs b/packages/backend-rs/src/database/redis.rs index d88d61d514..1b989da08b 100644 --- a/packages/backend-rs/src/database/redis.rs +++ b/packages/backend-rs/src/database/redis.rs @@ -81,15 +81,15 @@ async fn init_conn_pool() -> Result<(), RedisError> { #[derive(thiserror::Error, Debug)] pub enum RedisConnError { - #[error("Failed to initialize Redis connection pool: {0}")] + #[error("failed to initialize Redis connection pool")] Redis(RedisError), - #[error("Redis connection pool error: {0}")] + #[error("bad Redis connection pool")] Bb8Pool(RunError), } /// Returns an async [redis] connection managed by a [bb8] connection pool. -pub async fn redis_conn( -) -> Result, RedisConnError> { +pub async fn get_conn() -> Result, RedisConnError> +{ if !CONN_POOL.initialized() { let init_res = init_conn_pool().await; @@ -114,19 +114,44 @@ pub fn key(key: impl ToString) -> String { #[cfg(test)] mod unit_test { - use super::redis_conn; + use super::get_conn; use pretty_assertions::assert_eq; use redis::AsyncCommands; #[tokio::test] - async fn connect() { - assert!(redis_conn().await.is_ok()); - assert!(redis_conn().await.is_ok()); + #[cfg_attr(miri, ignore)] // can't call foreign function `getaddrinfo` on OS `linux` + async fn connect_sequential() { + get_conn().await.unwrap(); + get_conn().await.unwrap(); + get_conn().await.unwrap(); + get_conn().await.unwrap(); + get_conn().await.unwrap(); } #[tokio::test] + #[cfg_attr(miri, ignore)] // can't call foreign function `getaddrinfo` on OS `linux` + async fn connect_concurrent() { + let [c1, c2, c3, c4, c5] = [get_conn(), get_conn(), get_conn(), get_conn(), get_conn()]; + let _ = tokio::try_join!(c1, c2, c3, c4, c5).unwrap(); + } + + #[tokio::test] + #[cfg_attr(miri, ignore)] // can't call foreign function `getaddrinfo` on OS `linux` + async fn connect_spawn() { + let mut tasks = Vec::new(); + + for _ in 0..5 { + tasks.push(tokio::spawn(get_conn())); + } + for task in tasks { + task.await.unwrap().unwrap(); + } + } + + #[tokio::test] + #[cfg_attr(miri, ignore)] // can't call foreign function `getaddrinfo` on OS `linux` async fn access() { - let mut redis = redis_conn().await.unwrap(); + let mut redis = get_conn().await.unwrap(); let key = "CARGO_UNIT_TEST_KEY"; let value = "CARGO_UNIT_TEST_VALUE"; diff --git a/packages/backend-rs/src/federation/acct.rs b/packages/backend-rs/src/federation/acct.rs index 21e067c04e..816e75baf2 100644 --- a/packages/backend-rs/src/federation/acct.rs +++ b/packages/backend-rs/src/federation/acct.rs @@ -1,17 +1,20 @@ -use std::fmt; -use std::str::FromStr; +use std::{fmt, str::FromStr}; -#[derive(Debug, PartialEq)] -#[crate::export(object)] +#[cfg_attr(test, derive(Debug, PartialEq))] +#[macros::export(object)] pub struct Acct { pub username: String, pub host: Option, } -impl FromStr for Acct { - type Err = (); +#[derive(thiserror::Error, Debug)] +#[doc = "Error type to indicate a string-to-[`Acct`] conversion failure"] +#[error("failed to convert string '{0}' into acct")] +pub struct InvalidAcctString(String); + +impl FromStr for Acct { + type Err = InvalidAcctString; - /// This never throw errors. Feel free to `.unwrap()` the result. fn from_str(value: &str) -> Result { let split: Vec<&str> = if let Some(stripped) = value.strip_prefix('@') { stripped @@ -48,12 +51,12 @@ impl From for String { } } -#[crate::ts_export] +#[macros::ts_export] pub fn string_to_acct(acct: &str) -> Acct { Acct::from_str(acct).unwrap() } -#[crate::ts_export] +#[macros::ts_export] pub fn acct_to_string(acct: &Acct) -> String { acct.to_string() } @@ -65,7 +68,7 @@ mod unit_test { use std::str::FromStr; #[test] - fn test_acct_to_string() { + fn acct_to_string() { let remote_acct = Acct { username: "firefish".to_string(), host: Some("example.com".to_string()), @@ -82,7 +85,7 @@ mod unit_test { } #[test] - fn test_string_to_acct() { + fn string_to_acct() { let remote_acct = Acct { username: "firefish".to_string(), host: Some("example.com".to_string()), diff --git a/packages/backend-rs/src/federation/nodeinfo/fetch.rs b/packages/backend-rs/src/federation/nodeinfo/fetch.rs index 14ed838912..4f11821054 100644 --- a/packages/backend-rs/src/federation/nodeinfo/fetch.rs +++ b/packages/backend-rs/src/federation/nodeinfo/fetch.rs @@ -1,45 +1,46 @@ //! NodeInfo fetcher +//! +//! ref: -use crate::federation::nodeinfo::schema::*; -use crate::util::http_client; +use crate::{federation::nodeinfo::schema::*, 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}")] + #[error("failed to acquire an HTTP client")] HttpClient(#[from] http_client::Error), - #[error("HTTP error: {0}")] + #[error("HTTP request failed")] Http(#[from] isahc::Error), - #[error("Bad status: {0}")] + #[doc = "bad HTTP status"] + #[error("bad HTTP status ({0})")] BadStatus(String), - #[error("Failed to parse response body as text: {0}")] + #[error("failed to parse HTTP response body as text")] Response(#[from] std::io::Error), - #[error("Failed to parse response body as json: {0}")] + #[error("failed to parse HTTP response body as json")] Json(#[from] serde_json::Error), - #[error("No nodeinfo provided")] + #[error("nodeinfo is missing")] MissingNodeinfo, } -#[derive(Deserialize, Serialize, Debug)] +/// Represents the schema of `/.well-known/nodeinfo`. +#[derive(Deserialize)] pub struct NodeinfoLinks { links: Vec, } -#[derive(Deserialize, Serialize, Debug)] +/// Represents one entry of `/.well-known/nodeinfo`. +#[derive(Deserialize)] 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 { 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() { @@ -54,6 +55,9 @@ async fn fetch_nodeinfo_links(host: &str) -> Result { Ok(serde_json::from_str(&wellknown_response.text().await?)?) } +/// Check if any of the following relations is present in the given [NodeinfoLinks]. +/// * +/// * fn check_nodeinfo_link(links: NodeinfoLinks) -> Result { for link in links.links { if link.rel == "http://nodeinfo.diaspora.software/ns/schema/2.1" @@ -66,6 +70,7 @@ fn check_nodeinfo_link(links: NodeinfoLinks) -> Result { Err(Error::MissingNodeinfo) } +/// Fetches the nodeinfo from the given URL and parses the result. async fn fetch_nodeinfo_impl(nodeinfo_link: &str) -> Result { let client = http_client::client()?; let mut response = client.get_async(nodeinfo_link).await?; @@ -85,8 +90,8 @@ async fn fetch_nodeinfo_impl(nodeinfo_link: &str) -> Result { // for napi export type Nodeinfo = Nodeinfo20; -/// Fetches and returns the NodeInfo of a remote server. -#[crate::export] +/// Fetches and returns the NodeInfo (version 2.0) of a remote server. +#[macros::export] pub async fn fetch_nodeinfo(host: &str) -> Result { tracing::info!("fetching from {}", host); let links = fetch_nodeinfo_links(host).await?; @@ -96,11 +101,11 @@ pub async fn fetch_nodeinfo(host: &str) -> Result { #[cfg(test)] mod unit_test { - use super::{check_nodeinfo_link, fetch_nodeinfo, NodeinfoLink, NodeinfoLinks}; + use super::{NodeinfoLink, NodeinfoLinks}; use pretty_assertions::assert_eq; #[test] - fn test_check_nodeinfo_link() { + fn check_nodeinfo_link() { let links_1 = NodeinfoLinks { links: vec![ NodeinfoLink { @@ -114,7 +119,7 @@ mod unit_test { ], }; assert_eq!( - check_nodeinfo_link(links_1).unwrap(), + super::check_nodeinfo_link(links_1).unwrap(), "https://example.com/real" ); @@ -131,7 +136,7 @@ mod unit_test { ], }; assert_eq!( - check_nodeinfo_link(links_2).unwrap(), + super::check_nodeinfo_link(links_2).unwrap(), "https://example.com/real" ); @@ -147,13 +152,14 @@ mod unit_test { }, ], }; - check_nodeinfo_link(links_3).expect_err("No nodeinfo"); + super::check_nodeinfo_link(links_3).expect_err("No nodeinfo"); } #[tokio::test] - async fn test_fetch_nodeinfo() { + #[cfg_attr(miri, ignore)] // can't call foreign function `curl_global_init` on OS `linux` + async fn fetch_nodeinfo() { assert_eq!( - fetch_nodeinfo("info.firefish.dev") + super::fetch_nodeinfo("info.firefish.dev") .await .unwrap() .software diff --git a/packages/backend-rs/src/federation/nodeinfo/generate.rs b/packages/backend-rs/src/federation/nodeinfo/generate.rs index b14c02643f..7b1c858095 100644 --- a/packages/backend-rs/src/federation/nodeinfo/generate.rs +++ b/packages/backend-rs/src/federation/nodeinfo/generate.rs @@ -1,24 +1,31 @@ //! NodeInfo generator -use crate::config::CONFIG; -use crate::database::{cache, db_conn}; -use crate::federation::nodeinfo::schema::*; -use crate::misc::meta::fetch_meta; -use crate::model::entity::{note, user}; -use sea_orm::{ColumnTrait, DbErr, EntityTrait, PaginatorTrait, QueryFilter}; +use crate::{ + config::{local_server_info, CONFIG}, + database::db_conn, + federation::nodeinfo::schema::*, + model::entity::{note, user}, +}; +use sea_orm::prelude::*; use serde_json::json; -use std::collections::HashMap; +use std::{collections::HashMap, sync::Mutex}; -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("Database error: {0}")] - Db(#[from] DbErr), - #[error("Cache error: {0}")] - Cache(#[from] cache::Error), - #[error("Failed to serialize nodeinfo to JSON: {0}")] - Json(#[from] serde_json::Error), +static CACHE: Mutex> = Mutex::new(None); + +fn set_cache(nodeinfo: &Nodeinfo21) { + let _ = CACHE + .lock() + .map(|mut cache| *cache = Some(nodeinfo.to_owned())); } +/// 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?; @@ -49,14 +56,18 @@ async fn statistics() -> Result<(u64, u64, u64, u64), DbErr> { ) } -async fn generate_nodeinfo_2_1() -> Result { +/// 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 { + tracing::info!("generating NodeInfo"); + let (local_users, local_active_halfyear, local_active_month, local_posts) = statistics().await?; - let meta = fetch_meta(true).await?; - let metadata = HashMap::from([ + let meta = local_server_info().await?; + let mut metadata = HashMap::from([ ( "nodeName".to_string(), - json!(meta.name.unwrap_or(CONFIG.host.clone())), + json!(meta.name.unwrap_or_else(|| CONFIG.host.clone())), ), ("nodeDescription".to_string(), json!(meta.description)), ("repositoryUrl".to_string(), json!(meta.repository_url)), @@ -83,12 +94,12 @@ async fn generate_nodeinfo_2_1() -> Result { ("proxyAccountName".to_string(), json!(meta.proxy_account_id)), ( "themeColor".to_string(), - json!(meta.theme_color.unwrap_or("#31748f".to_string())), + json!(meta.theme_color.unwrap_or_else(|| "#31748f".to_string())), ), ]); + metadata.shrink_to_fit(); Ok(Nodeinfo21 { - version: "2.1".to_string(), software: Software21 { name: "firefish".to_string(), version: CONFIG.version.clone(), @@ -114,32 +125,53 @@ async fn generate_nodeinfo_2_1() -> Result { }) } -/// Returns NodeInfo (version 2.1) of the local server. -pub async fn nodeinfo_2_1() -> Result { - const NODEINFO_2_1_CACHE_KEY: &str = "nodeinfo_2_1"; - - let cached = cache::get::(NODEINFO_2_1_CACHE_KEY).await?; - - if let Some(nodeinfo) = cached { - Ok(nodeinfo) - } else { - let nodeinfo = generate_nodeinfo_2_1().await?; - cache::set(NODEINFO_2_1_CACHE_KEY, &nodeinfo, 60 * 60).await?; - Ok(nodeinfo) +async fn nodeinfo_2_1_impl(use_cache: bool) -> Result { + if use_cache { + if let Some(nodeinfo) = CACHE.lock().ok().and_then(|cache| cache.to_owned()) { + return Ok(nodeinfo); + } } + + let nodeinfo = generate_nodeinfo_2_1().await?; + + tracing::info!("updating cache"); + set_cache(&nodeinfo); + + Ok(nodeinfo) +} + +/// Returns NodeInfo (version 2.1) of the local server. +pub async fn nodeinfo_2_1() -> Result { + nodeinfo_2_1_impl(true).await } /// Returns NodeInfo (version 2.0) of the local server. -pub async fn nodeinfo_2_0() -> Result { +pub async fn nodeinfo_2_0() -> Result { Ok(nodeinfo_2_1().await?.into()) } -#[crate::ts_export(js_name = "nodeinfo_2_1")] +#[cfg(feature = "napi")] +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[doc = "database error"] + #[error(transparent)] + Db(#[from] DbErr), + #[error("failed to serialize nodeinfo into JSON")] + Json(#[from] serde_json::Error), +} + +#[macros::ts_export(js_name = "nodeinfo_2_1")] pub async fn nodeinfo_2_1_as_json() -> Result { Ok(serde_json::to_value(nodeinfo_2_1().await?)?) } -#[crate::ts_export(js_name = "nodeinfo_2_0")] +#[macros::ts_export(js_name = "nodeinfo_2_0")] pub async fn nodeinfo_2_0_as_json() -> Result { Ok(serde_json::to_value(nodeinfo_2_0().await?)?) } + +#[macros::ts_export(js_name = "updateNodeinfoCache")] +pub async fn update_cache() -> Result<(), DbErr> { + nodeinfo_2_1_impl(false).await?; + Ok(()) +} diff --git a/packages/backend-rs/src/federation/nodeinfo/mod.rs b/packages/backend-rs/src/federation/nodeinfo/mod.rs index d1e3fc2ed5..4d27a63caf 100644 --- a/packages/backend-rs/src/federation/nodeinfo/mod.rs +++ b/packages/backend-rs/src/federation/nodeinfo/mod.rs @@ -1,4 +1,6 @@ //! NodeInfo handler +//! +//! ref: pub mod fetch; pub mod generate; diff --git a/packages/backend-rs/src/federation/nodeinfo/schema.rs b/packages/backend-rs/src/federation/nodeinfo/schema.rs index 33ab79e2f0..d31d2e56e4 100644 --- a/packages/backend-rs/src/federation/nodeinfo/schema.rs +++ b/packages/backend-rs/src/federation/nodeinfo/schema.rs @@ -1,18 +1,16 @@ //! Schema definitions of NodeInfo version 2.0 and 2.1 +//! +//! ref: use serde::{Deserialize, Serialize}; use std::collections::HashMap; -// TODO: I want to use these macros but they don't work with rmp_serde -// * #[serde(skip_serializing_if = "Option::is_none")] (https://github.com/3Hren/msgpack-rust/issues/86) -// * #[serde(tag = "version", rename = "2.1")] (https://github.com/3Hren/msgpack-rust/issues/318) - /// NodeInfo schema version 2.1. -#[derive(Deserialize, Serialize, Debug, PartialEq)] +#[cfg_attr(test, derive(Debug, PartialEq, Deserialize))] +#[derive(Clone, Serialize)] #[serde(rename_all = "camelCase")] +#[serde(tag = "version", rename = "2.1")] pub struct Nodeinfo21 { - /// The schema version, must be 2.1. - pub version: String, /// Metadata about server software in use. pub software: Software21, /// The protocols supported on this server. @@ -28,12 +26,12 @@ pub struct Nodeinfo21 { } /// NodeInfo schema version 2.0. -#[derive(Deserialize, Serialize, Debug, PartialEq)] +#[cfg_attr(test, derive(Debug, PartialEq))] +#[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -#[crate::export(object, js_name = "Nodeinfo")] +#[macros::export(object, js_name = "Nodeinfo")] +#[serde(tag = "version", rename = "2.0")] pub struct Nodeinfo20 { - /// The schema version, must be 2.0. - pub version: String, /// Metadata about server software in use. pub software: Software20, /// The protocols supported on this server. @@ -49,7 +47,8 @@ pub struct Nodeinfo20 { } /// Metadata about server software in use (version 2.1). -#[derive(Deserialize, Serialize, Debug, PartialEq)] +#[cfg_attr(test, derive(Debug, PartialEq, Deserialize))] +#[derive(Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct Software21 { /// The canonical name of this server software. @@ -57,15 +56,18 @@ pub struct Software21 { /// The version of this server software. pub version: String, /// The url of the source code repository of this server software. + #[serde(skip_serializing_if = "Option::is_none")] pub repository: Option, /// The url of the homepage of this server software. + #[serde(skip_serializing_if = "Option::is_none")] pub homepage: Option, } /// Metadata about server software in use (version 2.0). -#[derive(Deserialize, Serialize, Debug, PartialEq)] +#[cfg_attr(test, derive(Debug, PartialEq))] +#[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -#[crate::export(object)] +#[macros::export(object)] pub struct Software20 { /// The canonical name of this server software. pub name: String, @@ -73,9 +75,10 @@ pub struct Software20 { pub version: String, } -#[derive(Deserialize, Serialize, Debug, PartialEq)] +#[cfg_attr(test, derive(Debug, PartialEq))] +#[derive(Deserialize, Serialize)] #[serde(rename_all = "lowercase")] -#[crate::export(string_enum = "lowercase")] +#[macros::derive_clone_and_export] pub enum Protocol { Activitypub, Buddycloud, @@ -90,9 +93,10 @@ pub enum Protocol { } /// The third party sites this server can connect to via their application API. -#[derive(Deserialize, Serialize, Debug, PartialEq)] +#[cfg_attr(test, derive(Debug, PartialEq))] +#[derive(Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -#[crate::export(object)] +#[macros::export(object)] pub struct Services { /// The third party sites this server can retrieve messages from for combined display with regular traffic. pub inbound: Vec, @@ -101,9 +105,10 @@ pub struct Services { } /// The third party sites this server can retrieve messages from for combined display with regular traffic. -#[derive(Deserialize, Serialize, Debug, PartialEq)] +#[cfg_attr(test, derive(Debug, PartialEq))] +#[derive(Deserialize, Serialize)] #[serde(rename_all = "lowercase")] -#[crate::export(string_enum = "lowercase")] +#[macros::derive_clone_and_export] pub enum Inbound { #[serde(rename = "atom1.0")] Atom1, @@ -119,9 +124,10 @@ pub enum Inbound { } /// The third party sites this server can publish messages to on the behalf of a user. -#[derive(Deserialize, Serialize, Debug, PartialEq)] +#[cfg_attr(test, derive(Debug, PartialEq))] +#[derive(Deserialize, Serialize)] #[serde(rename_all = "lowercase")] -#[crate::export(string_enum = "lowercase")] +#[macros::derive_clone_and_export] pub enum Outbound { #[serde(rename = "atom1.0")] Atom1, @@ -156,22 +162,29 @@ pub enum Outbound { } /// Usage statistics for this server. -#[derive(Deserialize, Serialize, Debug, PartialEq)] +#[cfg_attr(test, derive(Debug, PartialEq))] +#[derive(Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -#[crate::export(object)] +#[macros::export(object)] pub struct Usage { pub users: Users, + #[serde(skip_serializing_if = "Option::is_none")] pub local_posts: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub local_comments: Option, } /// statistics about the users of this server. -#[derive(Deserialize, Serialize, Debug, PartialEq)] +#[cfg_attr(test, derive(Debug, PartialEq))] +#[derive(Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -#[crate::export(object)] +#[macros::export(object)] pub struct Users { + #[serde(skip_serializing_if = "Option::is_none")] pub total: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub active_halfyear: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub active_month: Option, } @@ -187,7 +200,6 @@ impl From for Software20 { impl From for Nodeinfo20 { fn from(nodeinfo: Nodeinfo21) -> Self { Self { - version: "2.0".to_string(), software: nodeinfo.software.into(), protocols: nodeinfo.protocols, services: nodeinfo.services, diff --git a/packages/backend-rs/src/init/greet.rs b/packages/backend-rs/src/init/greet.rs index 323548e59c..01f795ed00 100644 --- a/packages/backend-rs/src/init/greet.rs +++ b/packages/backend-rs/src/init/greet.rs @@ -12,7 +12,7 @@ const GREETING_MESSAGE: &str = "\ "; /// Prints the greeting message and the Firefish version to stdout. -#[crate::export] +#[macros::export] pub fn greet() { println!("{}", GREETING_MESSAGE); diff --git a/packages/backend-rs/src/init/log.rs b/packages/backend-rs/src/init/log.rs index beb49decdc..47466735e3 100644 --- a/packages/backend-rs/src/init/log.rs +++ b/packages/backend-rs/src/init/log.rs @@ -3,7 +3,7 @@ use tracing::Level; use tracing_subscriber::FmtSubscriber; /// Initializes the [tracing] logger. -#[crate::export(js_name = "initializeRustLogger")] +#[macros::export(js_name = "initializeRustLogger")] pub fn initialize_logger() { let mut builder = FmtSubscriber::builder(); diff --git a/packages/backend-rs/src/init/system_info.rs b/packages/backend-rs/src/init/system_info.rs index 138e7486e0..7fde335e85 100644 --- a/packages/backend-rs/src/init/system_info.rs +++ b/packages/backend-rs/src/init/system_info.rs @@ -20,25 +20,25 @@ pub fn system_info() -> &'static std::sync::Mutex { } /// Prints the server hardware information as the server info log. -#[crate::export] +#[macros::export] pub fn show_server_info() -> Result<(), SysinfoPoisonError> { let system_info = system_info().lock()?; tracing::info!( "Hostname: {}", - System::host_name().unwrap_or("unknown".to_string()) + System::host_name().unwrap_or_else(|| "unknown".to_string()) ); tracing::info!( "OS: {}", - System::long_os_version().unwrap_or("unknown".to_string()) + System::long_os_version().unwrap_or_else(|| "unknown".to_string()) ); tracing::info!( "Kernel: {}", - System::kernel_version().unwrap_or("unknown".to_string()) + System::kernel_version().unwrap_or_else(|| "unknown".to_string()) ); tracing::info!( "CPU architecture: {}", - System::cpu_arch().unwrap_or("unknown".to_string()) + System::cpu_arch().unwrap_or_else(|| "unknown".to_string()) ); tracing::info!("CPU threads: {}", system_info.cpus().len()); tracing::info!("Total memory: {} MiB", system_info.total_memory() / 1048576); diff --git a/packages/backend-rs/src/lib.rs b/packages/backend-rs/src/lib.rs index d62ab792d6..1b9933fb8a 100644 --- a/packages/backend-rs/src/lib.rs +++ b/packages/backend-rs/src/lib.rs @@ -1,5 +1,3 @@ -use macro_rs::{export, ts_export}; - pub mod config; pub mod database; pub mod federation; diff --git a/packages/backend-rs/src/misc/check_server_block.rs b/packages/backend-rs/src/misc/check_server_block.rs index fe0c118521..a6355f5e94 100644 --- a/packages/backend-rs/src/misc/check_server_block.rs +++ b/packages/backend-rs/src/misc/check_server_block.rs @@ -18,9 +18,9 @@ /// # Ok(()) /// # } /// ``` -#[crate::ts_export] +#[macros::ts_export] pub async fn is_blocked_server(host: &str) -> Result { - Ok(crate::misc::meta::fetch_meta(true) + Ok(crate::config::local_server_info() .await? .blocked_hosts .iter() @@ -45,9 +45,9 @@ pub async fn is_blocked_server(host: &str) -> Result { /// # Ok(()) /// # } /// ``` -#[crate::ts_export] +#[macros::ts_export] pub async fn is_silenced_server(host: &str) -> Result { - Ok(crate::misc::meta::fetch_meta(true) + Ok(crate::config::local_server_info() .await? .silenced_hosts .iter() @@ -73,9 +73,9 @@ pub async fn is_silenced_server(host: &str) -> Result { /// # Ok(()) /// # } /// ``` -#[crate::ts_export] +#[macros::ts_export] pub async fn is_allowed_server(host: &str) -> Result { - let meta = crate::misc::meta::fetch_meta(true).await?; + let meta = crate::config::local_server_info().await?; if !meta.private_mode.unwrap_or(false) { return Ok(true); diff --git a/packages/backend-rs/src/misc/check_word_mute.rs b/packages/backend-rs/src/misc/check_word_mute.rs index 4109cb9a36..fdf4e1a8a0 100644 --- a/packages/backend-rs/src/misc/check_word_mute.rs +++ b/packages/backend-rs/src/misc/check_word_mute.rs @@ -1,8 +1,17 @@ -use crate::misc::get_note_all_texts::{all_texts, NoteLike}; +use crate::misc::note::elaborate; use once_cell::sync::Lazy; use regex::Regex; use sea_orm::DbErr; +#[macros::export(object)] +pub struct PartialNoteToCheckWordMute { + pub file_ids: Vec, + pub text: Option, + pub cw: Option, + pub renote_id: Option, + pub reply_id: Option, +} + fn convert_regex(js_regex: &str) -> String { static RE: Lazy = Lazy::new(|| Regex::new(r"^/(.+)/(.*)$").unwrap()); RE.replace(js_regex, "(?$2)$1").to_string() @@ -37,12 +46,12 @@ fn check_word_mute_impl( /// /// # Arguments /// -/// * `note` : [NoteLike] object +/// * `note` : [PartialNoteToCheckWordMute] 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] +#[macros::export] pub async fn check_word_mute( - note: NoteLike, + note: PartialNoteToCheckWordMute, muted_words: &[String], muted_patterns: &[String], ) -> Result { @@ -50,7 +59,7 @@ pub async fn check_word_mute( Ok(false) } else { Ok(check_word_mute_impl( - &all_texts(note, true).await?, + &elaborate!(note, true).await?, muted_words, muted_patterns, )) @@ -62,18 +71,18 @@ mod unit_test { use super::check_word_mute_impl; #[test] - fn test_word_mute_match() { - let texts = vec![ + fn word_mute_match() { + let texts = [ "The quick brown fox jumps over the lazy dog.".to_string(), "色は匂へど 散りぬるを 我が世誰ぞ 常ならむ".to_string(), "😇".to_string(), ]; - let hiragana_1 = r#"/[\u{3040}-\u{309f}]/u"#.to_string(); - let hiragana_2 = r#"/[あ-ん]/u"#.to_string(); - let katakana_1 = r#"/[\u{30a1}-\u{30ff}]/u"#.to_string(); - let katakana_2 = r#"/[ア-ン]/u"#.to_string(); - let emoji = r#"/[\u{1f300}-\u{1f5ff}\u{1f900}-\u{1f9ff}\u{1f600}-\u{1f64f}\u{1f680}-\u{1f6ff}\u{2600}-\u{26ff}\u{2700}-\u{27bf}\u{1f1e6}-\u{1f1ff}\u{1f191}-\u{1f251}\u{1f004}\u{1f0cf}\u{1f170}-\u{1f171}\u{1f17e}-\u{1f17f}\u{1f18e}\u{3030}\u{2b50}\u{2b55}\u{2934}-\u{2935}\u{2b05}-\u{2b07}\u{2b1b}-\u{2b1c}\u{3297}\u{3299}\u{303d}\u{00a9}\u{00ae}\u{2122}\u{23f3}\u{24c2}\u{23e9}-\u{23ef}\u{25b6}\u{23f8}-\u{23fa}]/u"#.to_string(); + let hiragana_1 = r"/[\u{3040}-\u{309f}]/u".to_string(); + let hiragana_2 = r"/[あ-ん]/u".to_string(); + let katakana_1 = r"/[\u{30a1}-\u{30ff}]/u".to_string(); + let katakana_2 = r"/[ア-ン]/u".to_string(); + let emoji = r"/[\u{1f300}-\u{1f5ff}\u{1f900}-\u{1f9ff}\u{1f600}-\u{1f64f}\u{1f680}-\u{1f6ff}\u{2600}-\u{26ff}\u{2700}-\u{27bf}\u{1f1e6}-\u{1f1ff}\u{1f191}-\u{1f251}\u{1f004}\u{1f0cf}\u{1f170}-\u{1f171}\u{1f17e}-\u{1f17f}\u{1f18e}\u{3030}\u{2b50}\u{2b55}\u{2934}-\u{2935}\u{2b05}-\u{2b07}\u{2b1b}-\u{2b1c}\u{3297}\u{3299}\u{303d}\u{00a9}\u{00ae}\u{2122}\u{23f3}\u{24c2}\u{23e9}-\u{23ef}\u{25b6}\u{23f8}-\u{23fa}]/u".to_string(); assert!(check_word_mute_impl(&texts, &[], &["/the/i".to_string()])); diff --git a/packages/backend-rs/src/misc/convert_host.rs b/packages/backend-rs/src/misc/convert_host.rs index c086285af1..0de4d1912d 100644 --- a/packages/backend-rs/src/misc/convert_host.rs +++ b/packages/backend-rs/src/misc/convert_host.rs @@ -4,15 +4,16 @@ #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("Idna error: {0}")] + #[doc = "UTS #46 process has failed"] + #[error(transparent)] Idna(#[from] idna::Errors), - #[error("Url parse error: {0}")] + #[error("failed to parse a URL")] UrlParse(#[from] url::ParseError), - #[error("Hostname is missing")] + #[error("hostname is missing")] NoHostname, } -#[crate::ts_export] +#[macros::ts_export] pub fn get_full_ap_account(username: &str, host: Option<&str>) -> Result { Ok(match host { Some(host) => format!("{}@{}", username, to_puny(host)?), @@ -20,7 +21,7 @@ pub fn get_full_ap_account(username: &str, host: Option<&str>) -> Result) -> Result { Ok(match host { Some(host) => extract_host(&crate::config::CONFIG.url)? == to_puny(host)?, @@ -28,12 +29,12 @@ pub fn is_self_host(host: Option<&str>) -> Result { }) } -#[crate::ts_export] +#[macros::ts_export] pub fn is_same_origin(uri: &str) -> Result { Ok(url::Url::parse(uri)?.origin().ascii_serialization() == crate::config::CONFIG.url) } -#[crate::ts_export] +#[macros::ts_export] pub fn extract_host(uri: &str) -> Result { url::Url::parse(uri)? .host_str() @@ -41,7 +42,7 @@ pub fn extract_host(uri: &str) -> Result { .and_then(|v| Ok(to_puny(v)?)) } -#[crate::ts_export] +#[macros::ts_export] pub fn to_puny(host: &str) -> Result { idna::domain_to_ascii(host) } diff --git a/packages/backend-rs/src/misc/emoji.rs b/packages/backend-rs/src/misc/emoji.rs index a19fc6229d..47c2e0debe 100644 --- a/packages/backend-rs/src/misc/emoji.rs +++ b/packages/backend-rs/src/misc/emoji.rs @@ -1,6 +1,6 @@ //! This module is used in the TypeScript backend only. -#[crate::ts_export] +#[macros::ts_export] pub fn is_unicode_emoji(s: &str) -> bool { emojis::get(s).is_some() } diff --git a/packages/backend-rs/src/misc/escape_sql.rs b/packages/backend-rs/src/misc/escape_sql.rs index 31b8b964ab..747ceb51c7 100644 --- a/packages/backend-rs/src/misc/escape_sql.rs +++ b/packages/backend-rs/src/misc/escape_sql.rs @@ -1,11 +1,11 @@ /// Escapes `%` and `\` in the given string. -#[crate::export] +#[macros::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] +#[macros::export] pub fn safe_for_sql(src: &str) -> bool { !src.contains([ '\0', '\x08', '\x09', '\x1a', '\n', '\r', '"', '\'', '\\', '%', @@ -14,25 +14,24 @@ pub fn safe_for_sql(src: &str) -> bool { #[cfg(test)] mod unit_test { - use super::{safe_for_sql, sql_like_escape}; use pretty_assertions::assert_eq; #[test] - fn sql_like_escape_test() { - assert_eq!(sql_like_escape(""), ""); - assert_eq!(sql_like_escape("abc"), "abc"); - assert_eq!(sql_like_escape("a%bc"), r"a\%bc"); - assert_eq!(sql_like_escape("a呼%吸bc"), r"a呼\%吸bc"); - assert_eq!(sql_like_escape("a呼%吸b%_c"), r"a呼\%吸b\%\_c"); - assert_eq!(sql_like_escape("_اللغة العربية"), r"\_اللغة العربية"); + fn sql_like_escape() { + assert_eq!(super::sql_like_escape(""), ""); + assert_eq!(super::sql_like_escape("abc"), "abc"); + assert_eq!(super::sql_like_escape("a%bc"), r"a\%bc"); + assert_eq!(super::sql_like_escape("a呼%吸bc"), r"a呼\%吸bc"); + assert_eq!(super::sql_like_escape("a呼%吸b%_c"), r"a呼\%吸b\%\_c"); + assert_eq!(super::sql_like_escape("_اللغة العربية"), r"\_اللغة العربية"); } #[test] - fn safe_for_sql_test() { - assert!(safe_for_sql("123")); - assert!(safe_for_sql("人間")); - assert!(!safe_for_sql("人間\x09")); - assert!(!safe_for_sql("abc\ndef")); - assert!(!safe_for_sql("%something%")); + fn safe_for_sql() { + assert!(super::safe_for_sql("123")); + assert!(super::safe_for_sql("人間")); + assert!(!super::safe_for_sql("人間\x09")); + assert!(!super::safe_for_sql("abc\ndef")); + assert!(!super::safe_for_sql("%something%")); } } diff --git a/packages/backend-rs/src/misc/format_milliseconds.rs b/packages/backend-rs/src/misc/format_milliseconds.rs index 20c67a773a..148e5791b5 100644 --- a/packages/backend-rs/src/misc/format_milliseconds.rs +++ b/packages/backend-rs/src/misc/format_milliseconds.rs @@ -1,5 +1,5 @@ /// Converts milliseconds to a human readable string. -#[crate::export] +#[macros::export] pub fn format_milliseconds(milliseconds: u32) -> String { let mut seconds = milliseconds / 1000; let mut minutes = seconds / 60; @@ -30,16 +30,21 @@ pub fn format_milliseconds(milliseconds: u32) -> String { #[cfg(test)] mod unit_test { - use super::format_milliseconds; use pretty_assertions::assert_eq; #[test] - fn format_milliseconds_test() { - assert_eq!(format_milliseconds(1000), "1 second(s)"); - assert_eq!(format_milliseconds(1387938), "23 minute(s), 7 second(s)"); - assert_eq!(format_milliseconds(34200457), "9 hour(s), 30 minute(s)"); + fn format_milliseconds() { + assert_eq!(super::format_milliseconds(1000), "1 second(s)"); assert_eq!( - format_milliseconds(998244353), + super::format_milliseconds(1387938), + "23 minute(s), 7 second(s)" + ); + assert_eq!( + super::format_milliseconds(34200457), + "9 hour(s), 30 minute(s)" + ); + assert_eq!( + super::format_milliseconds(998244353), "11 day(s), 13 hour(s), 17 minute(s), 24 second(s)" ); } diff --git a/packages/backend-rs/src/misc/get_image_size.rs b/packages/backend-rs/src/misc/get_image_size.rs index c1348e62c3..1964c9cbd9 100644 --- a/packages/backend-rs/src/misc/get_image_size.rs +++ b/packages/backend-rs/src/misc/get_image_size.rs @@ -1,30 +1,32 @@ -use crate::database::cache; -use crate::util::http_client; +use crate::{database::cache, util::http_client}; use image::{io::Reader, ImageError, ImageFormat}; -use isahc::ReadResponseExt; +use isahc::AsyncReadResponseExt; use nom_exif::{parse_jpeg_exif, EntryValue, ExifTag}; use std::io::Cursor; use tokio::sync::Mutex; #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("Redis cache error: {0}")] + #[error("Redis cache operation has failed")] Cache(#[from] cache::Error), - #[error("HTTP client aquisition error: {0}")] + #[error("failed to acquire an HTTP client")] HttpClient(#[from] http_client::Error), - #[error("Isahc error: {0}")] + #[error("HTTP request failed")] Isahc(#[from] isahc::Error), - #[error("HTTP error: {0}")] - Http(String), - #[error("Image decoding error: {0}")] + #[doc = "bad HTTP status"] + #[error("bad HTTP status ({0})")] + BadStatus(String), + #[error("failed to decode an image")] Image(#[from] ImageError), - #[error("Image decoding error: {0}")] + #[error("failed to decode an image")] Io(#[from] std::io::Error), - #[error("Exif extraction error: {0}")] + #[error("failed to extract the exif data")] Exif(#[from] nom_exif::Error), - #[error("Emoji meta attempt limit exceeded: {0}")] + #[doc = "too many fetch attempts"] + #[error("too many fetch attempts for {0}")] TooManyAttempts(String), - #[error("Unsupported image type: {0}")] + #[doc = "unsupported image type"] + #[error("unsupported image type ({0})")] UnsupportedImage(String), } @@ -41,14 +43,14 @@ const BROWSER_SAFE_IMAGE_TYPES: [ImageFormat; 8] = [ static MTX_GUARD: Mutex<()> = Mutex::const_new(()); -#[derive(Debug, PartialEq)] -#[crate::export(object)] +#[cfg_attr(test, derive(Debug, PartialEq))] +#[macros::export(object)] pub struct ImageSize { pub width: u32, pub height: u32, } -#[crate::export] +#[macros::export] pub async fn get_image_size_from_url(url: &str) -> Result { let attempted: bool; @@ -71,15 +73,19 @@ pub async fn get_image_size_from_url(url: &str) -> Result { tracing::info!("retrieving image from {}", url); - let mut response = http_client::client()?.get(url)?; + let mut response = http_client::client()?.get_async(url).await?; if !response.status().is_success() { tracing::info!("status: {}", response.status()); tracing::debug!("response body: {:#?}", response.body()); - return Err(Error::Http(format!("Failed to get image from {}", url))); + return Err(Error::BadStatus(format!( + "{} returned {}", + url, + response.status() + ))); } - let image_bytes = response.bytes()?; + let image_bytes = response.bytes().await?; let reader = Reader::new(Cursor::new(&image_bytes)).with_guessed_format()?; @@ -123,12 +129,13 @@ pub async fn get_image_size_from_url(url: &str) -> Result { #[cfg(test)] mod unit_test { - use super::{get_image_size_from_url, ImageSize}; + use super::ImageSize; use crate::database::cache; use pretty_assertions::assert_eq; #[tokio::test] - async fn test_get_image_size() { + #[cfg_attr(miri, ignore)] // can't call foreign function `getaddrinfo` on OS `linux` + async fn get_image_size_from_url() { let png_url_1 = "https://firefish.dev/firefish/firefish/-/raw/5891a90f71a8b9d5ea99c683ade7e485c685d642/packages/backend/assets/splash.png"; let png_url_2 = "https://firefish.dev/firefish/firefish/-/raw/5891a90f71a8b9d5ea99c683ade7e485c685d642/packages/backend/assets/notification-badges/at.png"; let png_url_3 = "https://firefish.dev/firefish/firefish/-/raw/5891a90f71a8b9d5ea99c683ade7e485c685d642/packages/backend/assets/api-doc.png"; @@ -177,34 +184,43 @@ mod unit_test { assert_eq!( png_size_1, - get_image_size_from_url(png_url_1).await.unwrap() + super::get_image_size_from_url(png_url_1).await.unwrap() ); assert_eq!( png_size_2, - get_image_size_from_url(png_url_2).await.unwrap() + super::get_image_size_from_url(png_url_2).await.unwrap() ); assert_eq!( png_size_3, - get_image_size_from_url(png_url_3).await.unwrap() + super::get_image_size_from_url(png_url_3).await.unwrap() ); assert_eq!( rotated_jpeg_size, - get_image_size_from_url(rotated_jpeg_url).await.unwrap() + super::get_image_size_from_url(rotated_jpeg_url) + .await + .unwrap() ); assert_eq!( webp_size_1, - get_image_size_from_url(webp_url_1).await.unwrap() + super::get_image_size_from_url(webp_url_1).await.unwrap() ); assert_eq!( webp_size_2, - get_image_size_from_url(webp_url_2).await.unwrap() + super::get_image_size_from_url(webp_url_2).await.unwrap() ); - assert_eq!(ico_size, get_image_size_from_url(ico_url).await.unwrap()); - assert_eq!(gif_size, get_image_size_from_url(gif_url).await.unwrap()); - assert!(get_image_size_from_url(mp3_url).await.is_err()); + assert_eq!( + ico_size, + super::get_image_size_from_url(ico_url).await.unwrap() + ); + assert_eq!( + gif_size, + super::get_image_size_from_url(gif_url).await.unwrap() + ); + assert!(super::get_image_size_from_url(mp3_url).await.is_err()); } #[tokio::test] + #[cfg_attr(miri, ignore)] // can't call foreign function `getaddrinfo` on OS `linux` async fn too_many_attempts() { let url = "https://firefish.dev/firefish/firefish/-/raw/5891a90f71a8b9d5ea99c683ade7e485c685d642/packages/backend/assets/splash.png"; @@ -213,7 +229,7 @@ mod unit_test { .await .unwrap(); - assert!(get_image_size_from_url(url).await.is_ok()); - assert!(get_image_size_from_url(url).await.is_err()); + assert!(super::get_image_size_from_url(url).await.is_ok()); + assert!(super::get_image_size_from_url(url).await.is_err()); } } diff --git a/packages/backend-rs/src/misc/get_note_all_texts.rs b/packages/backend-rs/src/misc/get_note_all_texts.rs deleted file mode 100644 index 8199085a53..0000000000 --- a/packages/backend-rs/src/misc/get_note_all_texts.rs +++ /dev/null @@ -1,93 +0,0 @@ -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 { - pub file_ids: Vec, - pub user_id: String, - pub text: Option, - pub cw: Option, - pub renote_id: Option, - pub reply_id: Option, -} - -/// Returns [`Vec`] containing the post text, content warning, -/// those of the "parent" (replied/quoted) posts, and alt texts of attached files. -/// -/// ## 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, DbErr> { - let db = db_conn().await?; - - let mut texts: Vec = vec![]; - let is_renote: bool; - - if let Some(text) = note.text { - is_renote = false; - texts.push(text); - } else { - is_renote = true; - } - - if let Some(cw) = note.cw { - texts.push(cw); - } - - texts.extend( - drive_file::Entity::find() - .select_only() - .column(drive_file::Column::Comment) - .filter(drive_file::Column::Id.is_in(note.file_ids)) - .into_tuple::>() - .all(db) - .await? - .into_iter() - .flatten(), - ); - - if note.renote_id.is_some() && (include_parent || is_renote) { - let renote_id = note.renote_id.unwrap(); - - if let Some((text, cw)) = note::Entity::find_by_id(&renote_id) - .select_only() - .columns([note::Column::Text, note::Column::Cw]) - .into_tuple::<(Option, Option)>() - .one(db) - .await? - { - if let Some(t) = text { - texts.push(t); - } - if let Some(c) = cw { - texts.push(c); - } - } else { - tracing::warn!("nonexistent renote id: {}", renote_id); - } - } - - if include_parent && note.reply_id.is_some() { - if let Some((text, cw)) = note::Entity::find_by_id(note.reply_id.as_ref().unwrap()) - .select_only() - .columns([note::Column::Text, note::Column::Cw]) - .into_tuple::<(Option, Option)>() - .one(db) - .await? - { - if let Some(t) = text { - texts.push(t); - } - if let Some(c) = cw { - texts.push(c); - } - } else { - tracing::warn!("nonexistent reply id: {}", note.reply_id.unwrap()); - } - } - - Ok(texts) -} diff --git a/packages/backend-rs/src/misc/get_note_summary.rs b/packages/backend-rs/src/misc/get_note_summary.rs deleted file mode 100644 index 64a0b52da2..0000000000 --- a/packages/backend-rs/src/misc/get_note_summary.rs +++ /dev/null @@ -1,94 +0,0 @@ -use serde::{Deserialize, Serialize}; - -// TODO?: handle name collisions -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -#[crate::export(object, js_name = "NoteLikeForGetNoteSummary")] -pub struct NoteLike { - pub file_ids: Vec, - pub text: Option, - pub cw: Option, - pub has_poll: bool, -} - -#[crate::export] -pub fn get_note_summary(note: NoteLike) -> String { - let mut buf: Vec = vec![]; - - if let Some(cw) = note.cw { - buf.push(cw) - } else if let Some(text) = note.text { - buf.push(text) - } - - match note.file_ids.len() { - 0 => (), - 1 => buf.push("📎".to_string()), - n => buf.push(format!("📎 ({})", n)), - }; - - if note.has_poll { - buf.push("📊".to_string()) - } - - buf.join(" ") -} - -#[cfg(test)] -mod unit_test { - use super::{get_note_summary, NoteLike}; - use pretty_assertions::assert_eq; - - #[test] - fn test_note_summary() { - let note = NoteLike { - file_ids: vec![], - text: Some("Hello world!".to_string()), - cw: None, - has_poll: false, - }; - assert_eq!(get_note_summary(note), "Hello world!"); - - let note_with_cw = NoteLike { - file_ids: vec![], - text: Some("Hello world!".to_string()), - cw: Some("Content warning".to_string()), - has_poll: false, - }; - assert_eq!(get_note_summary(note_with_cw), "Content warning"); - - let note_with_file_and_cw = NoteLike { - file_ids: vec!["9s7fmcqogiq4igin".to_string()], - text: None, - cw: Some("Selfie, no ec".to_string()), - has_poll: false, - }; - assert_eq!(get_note_summary(note_with_file_and_cw), "Selfie, no ec 📎"); - - let note_with_files_only = NoteLike { - file_ids: vec![ - "9s7fmcqogiq4igin".to_string(), - "9s7qrld5u14cey98".to_string(), - "9s7gebs5zgts4kca".to_string(), - "9s5z3e4vefqd29ee".to_string(), - ], - text: None, - cw: None, - has_poll: false, - }; - assert_eq!(get_note_summary(note_with_files_only), "📎 (4)"); - - let note_all = NoteLike { - file_ids: vec![ - "9s7fmcqogiq4igin".to_string(), - "9s7qrld5u14cey98".to_string(), - "9s7gebs5zgts4kca".to_string(), - "9s5z3e4vefqd29ee".to_string(), - ], - text: Some("Hello world!".to_string()), - cw: Some("Content warning".to_string()), - has_poll: true, - }; - assert_eq!(get_note_summary(note_all), "Content warning 📎 (4) 📊"); - } -} diff --git a/packages/backend-rs/src/misc/is_quote.rs b/packages/backend-rs/src/misc/is_quote.rs index 42e792f956..e754b82936 100644 --- a/packages/backend-rs/src/misc/is_quote.rs +++ b/packages/backend-rs/src/misc/is_quote.rs @@ -1,10 +1,12 @@ -use crate::model::entity::note; +#[macros::export(object, js_name = "NoteLikeForIsQuote")] +pub struct NoteLike { + pub renote_id: Option, + pub text: Option, + pub has_poll: bool, + pub file_ids: Vec, +} -// for napi export -// https://github.com/napi-rs/napi-rs/issues/2060 -type Note = note::Model; - -#[crate::export] -pub fn is_quote(note: Note) -> bool { +#[macros::export] +pub fn is_quote(note: &NoteLike) -> bool { note.renote_id.is_some() && (note.text.is_some() || note.has_poll || !note.file_ids.is_empty()) } diff --git a/packages/backend-rs/src/misc/is_safe_url.rs b/packages/backend-rs/src/misc/is_safe_url.rs index 2600366ab5..603710310f 100644 --- a/packages/backend-rs/src/misc/is_safe_url.rs +++ b/packages/backend-rs/src/misc/is_safe_url.rs @@ -1,4 +1,4 @@ -#[crate::export] +#[macros::export] pub fn is_safe_url(url: &str) -> bool { if let Ok(url) = url.parse::() { if url.host_str().unwrap_or_default() == "unix" @@ -15,20 +15,28 @@ pub fn is_safe_url(url: &str) -> bool { #[cfg(test)] mod unit_test { - use super::is_safe_url; - #[test] - fn safe_url() { - assert!(is_safe_url("http://firefish.dev/firefish/firefish")); - assert!(is_safe_url("https://firefish.dev/firefish/firefish")); - assert!(is_safe_url("http://firefish.dev:80/firefish/firefish")); - assert!(is_safe_url("https://firefish.dev:80/firefish/firefish")); - assert!(is_safe_url("http://firefish.dev:443/firefish/firefish")); - assert!(is_safe_url("https://firefish.dev:443/firefish/firefish")); - assert!(!is_safe_url("https://unix/firefish/firefish")); - assert!(!is_safe_url("https://firefish.dev:35/firefish/firefish")); - assert!(!is_safe_url("ftp://firefish.dev/firefish/firefish")); - assert!(!is_safe_url("nyaa")); - assert!(!is_safe_url("")); + fn is_safe_url() { + assert!(super::is_safe_url("http://firefish.dev/firefish/firefish")); + assert!(super::is_safe_url("https://firefish.dev/firefish/firefish")); + assert!(super::is_safe_url( + "http://firefish.dev:80/firefish/firefish" + )); + assert!(super::is_safe_url( + "https://firefish.dev:80/firefish/firefish" + )); + assert!(super::is_safe_url( + "http://firefish.dev:443/firefish/firefish" + )); + assert!(super::is_safe_url( + "https://firefish.dev:443/firefish/firefish" + )); + assert!(!super::is_safe_url("https://unix/firefish/firefish")); + assert!(!super::is_safe_url( + "https://firefish.dev:35/firefish/firefish" + )); + assert!(!super::is_safe_url("ftp://firefish.dev/firefish/firefish")); + assert!(!super::is_safe_url("nyaa")); + assert!(!super::is_safe_url("")); } } diff --git a/packages/backend-rs/src/misc/latest_version.rs b/packages/backend-rs/src/misc/latest_version.rs index 8c361aa5bf..1f330f4d2e 100644 --- a/packages/backend-rs/src/misc/latest_version.rs +++ b/packages/backend-rs/src/misc/latest_version.rs @@ -1,23 +1,23 @@ //! 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 crate::{database::cache, util::http_client}; +use isahc::AsyncReadResponseExt; +use serde::Deserialize; #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("Cache error: {0}")] + #[error("Redis cache operation has failed")] Cache(#[from] cache::Error), - #[error("Isahc error: {0}")] + #[error("HTTP request failed")] Isahc(#[from] isahc::Error), - #[error("HTTP client aquisition error: {0}")] + #[error("failed to acquire an HTTP client")] HttpClient(#[from] http_client::Error), - #[error("HTTP error: {0}")] - Http(String), - #[error("Response parsing error: {0}")] + #[doc = "firefish.dev returned bad HTTP status"] + #[error("firefish.dev returned bad HTTP status ({0})")] + BadStatus(String), + #[error("failed to parse the HTTP response")] Io(#[from] std::io::Error), - #[error("Failed to deserialize JSON: {0}")] + #[error("failed to parse the HTTP response as JSON")] Json(#[from] serde_json::Error), } @@ -25,28 +25,28 @@ const UPSTREAM_PACKAGE_JSON_URL: &str = "https://firefish.dev/firefish/firefish/-/raw/main/package.json"; async fn get_latest_version() -> Result { - #[derive(Debug, Deserialize, Serialize)] + #[derive(Debug, Deserialize)] struct Response { version: String, } - let mut response = http_client::client()?.get(UPSTREAM_PACKAGE_JSON_URL)?; + let mut response = http_client::client()? + .get_async(UPSTREAM_PACKAGE_JSON_URL) + .await?; if !response.status().is_success() { tracing::info!("status: {}", response.status()); tracing::debug!("response body: {:#?}", response.body()); - return Err(Error::Http( - "Failed to fetch version from Firefish GitLab".to_string(), - )); + return Err(Error::BadStatus(response.status().to_string())); } - let res_parsed: Response = serde_json::from_str(&response.text()?)?; + let res_parsed: Response = serde_json::from_str(&response.text().await?)?; Ok(res_parsed.version) } /// Returns the latest Firefish version. -#[crate::export] +#[macros::export] pub async fn latest_version() -> Result { let version: Option = cache::get_one(cache::Category::FetchUrl, UPSTREAM_PACKAGE_JSON_URL).await?; @@ -99,7 +99,8 @@ mod unit_test { } #[tokio::test] - async fn check_version() { + #[cfg_attr(miri, ignore)] // can't call foreign function `getaddrinfo` on OS `linux` + async fn get_latest_version() { // delete caches in case you run this test multiple times cache::delete_one(cache::Category::FetchUrl, UPSTREAM_PACKAGE_JSON_URL) .await diff --git a/packages/backend-rs/src/misc/mastodon_id.rs b/packages/backend-rs/src/misc/mastodon_id.rs index 9cf5d8d5f5..65fbd4d057 100644 --- a/packages/backend-rs/src/misc/mastodon_id.rs +++ b/packages/backend-rs/src/misc/mastodon_id.rs @@ -1,10 +1,10 @@ -#[crate::export] +#[macros::export] pub fn to_mastodon_id(firefish_id: &str) -> Option { let decoded: [u8; 16] = basen::BASE36.decode_var_len(firefish_id)?; Some(basen::BASE10.encode_var_len(&decoded)) } -#[crate::export] +#[macros::export] pub fn from_mastodon_id(mastodon_id: &str) -> Option { let decoded: [u8; 16] = basen::BASE10.decode_var_len(mastodon_id)?; Some(basen::BASE36.encode_var_len(&decoded)) @@ -12,24 +12,23 @@ pub fn from_mastodon_id(mastodon_id: &str) -> Option { #[cfg(test)] mod unit_test { - use super::{from_mastodon_id, to_mastodon_id}; use pretty_assertions::assert_eq; #[test] - fn to_mastodon_id_test() { + fn to_mastodon_id() { assert_eq!( - to_mastodon_id("9pdqi3rjl4lxirq3").unwrap(), + super::to_mastodon_id("9pdqi3rjl4lxirq3").unwrap(), "2145531976185871567229403" ); - assert_eq!(to_mastodon_id("9pdqi3r*irq3"), None); + assert_eq!(super::to_mastodon_id("9pdqi3r*irq3"), None); } #[test] - fn from_mastodon_id_test() { + fn from_mastodon_id() { assert_eq!( - from_mastodon_id("2145531976185871567229403").unwrap(), + super::from_mastodon_id("2145531976185871567229403").unwrap(), "9pdqi3rjl4lxirq3" ); - assert_eq!(from_mastodon_id("9pdqi3rjl4lxirq3"), None); + assert_eq!(super::from_mastodon_id("9pdqi3rjl4lxirq3"), None); } } diff --git a/packages/backend-rs/src/misc/mod.rs b/packages/backend-rs/src/misc/mod.rs index 23782b11a0..e3eb23a2eb 100644 --- a/packages/backend-rs/src/misc/mod.rs +++ b/packages/backend-rs/src/misc/mod.rs @@ -7,13 +7,11 @@ pub mod emoji; pub mod escape_sql; pub mod format_milliseconds; pub mod get_image_size; -pub mod get_note_all_texts; -pub mod get_note_summary; pub mod is_quote; pub mod is_safe_url; pub mod latest_version; pub mod mastodon_id; -pub mod meta; +pub mod note; pub mod nyaify; pub mod password; pub mod reaction; diff --git a/packages/backend-rs/src/misc/note/elaborate.rs b/packages/backend-rs/src/misc/note/elaborate.rs new file mode 100644 index 0000000000..c117dca7c9 --- /dev/null +++ b/packages/backend-rs/src/misc/note/elaborate.rs @@ -0,0 +1,134 @@ +use crate::{ + database::db_conn, + model::entity::{drive_file, note}, +}; +use sea_orm::{prelude::*, QuerySelect}; + +/// Returns [`Vec`] containing the post text, content warning, +/// those of the "parent" (replied/quoted) posts, and alt texts of attached files. +/// Consider using [`elaborate`] macro instead +/// when dealing with a note ([`note::Model`])-like instance. +/// +/// # Arguments +/// +/// * `file_ids` : IDs of attached files ([`drive_file::Model`]) +/// * `text`, `cw`, `renote_id`, `reply_id` : note ([`note::Model`]) fields +/// * `include_parent` : whether to take the reply-to post and quoted post into account +pub async fn elaborate_impl( + file_ids: &[String], + text: Option, + cw: Option, + renote_id: Option<&String>, + reply_id: Option<&String>, + include_parent: bool, +) -> Result, DbErr> { + let db = db_conn().await?; + + let mut texts: Vec = vec![]; + let is_renote = text.is_none(); + + if let Some(text) = text { + texts.push(text); + } + if let Some(cw) = cw { + texts.push(cw); + } + + texts.extend( + drive_file::Entity::find() + .select_only() + .column(drive_file::Column::Comment) + .filter(drive_file::Column::Id.is_in(file_ids)) + .into_tuple::>() + .all(db) + .await? + .into_iter() + .flatten(), + ); + + let mut query_note_ids = Vec::<&str>::with_capacity(2); + if let Some(renote_id) = renote_id { + if include_parent || is_renote { + query_note_ids.push(renote_id); + } + } + if let Some(reply_id) = reply_id { + if include_parent { + query_note_ids.push(reply_id); + } + } + if !query_note_ids.is_empty() { + texts.extend( + note::Entity::find() + .filter(note::Column::Id.is_in(query_note_ids)) + .select_only() + .columns([note::Column::Text, note::Column::Cw]) + .into_tuple::<(Option, Option)>() + .one(db) + .await? + .into_iter() + .flat_map(|(text, cw)| [text, cw]) + .flatten(), + ); + } + + Ok(texts) +} + +/// Returns [`Vec`] containing the post text, content warning, +/// those of the "parent" (replied/quoted) posts, and alt texts of attached files. +/// +/// # Arguments +/// +/// * `note_like` : a note ([`note::Model`])-like instance containing +/// `file_ids`, `text`, `cw`, `renote_id`, `reply_id` fields +/// * `include_parent` ([bool]) : whether to take the reply-to post and quoted post into account +/// +/// # Caveats +/// +/// The `note_like` argument should not contain function calls +/// (e.g., `elaborate!(note.clone(), false)`) +/// since the function will be called multiple times after macro expansion. +/// +/// # Examples +/// +/// ``` +/// # use backend_rs::misc::note::elaborate; +/// // note-like struct +/// struct NoteLike { +/// // required fields +/// file_ids: Vec, +/// text: Option, +/// cw: Option, +/// renote_id: Option, +/// reply_id: Option, +/// // arbitrary extra fields +/// extra_field_1: u32, +/// extra_field_2: Vec, +/// } +/// +/// async fn print_all_related_texts( +/// note: &NoteLike +/// ) -> Result<(), sea_orm::DbErr> { +/// let all_texts = elaborate!(note, true).await?; +/// all_texts.iter().map(|text| println!("{}", text)); +/// Ok(()) +/// } +/// ``` +#[doc(hidden)] // hide the macro in the top doc page +#[macro_export] +macro_rules! elaborate { + ($note_like:expr, $include_parent:expr) => { + $crate::misc::note::elaborate::elaborate_impl( + &$note_like.file_ids, + $note_like.text.clone(), + $note_like.cw.clone(), + $note_like.renote_id.as_ref(), + $note_like.reply_id.as_ref(), + $include_parent, + ) + }; +} + +#[doc(inline)] // show the macro in the module doc page +pub use elaborate; diff --git a/packages/backend-rs/src/misc/note/mod.rs b/packages/backend-rs/src/misc/note/mod.rs new file mode 100644 index 0000000000..84e2560af8 --- /dev/null +++ b/packages/backend-rs/src/misc/note/mod.rs @@ -0,0 +1,5 @@ +pub use elaborate::elaborate; +pub use summarize::summarize; + +pub mod elaborate; +pub mod summarize; diff --git a/packages/backend-rs/src/misc/note/summarize.rs b/packages/backend-rs/src/misc/note/summarize.rs new file mode 100644 index 0000000000..9649fc9345 --- /dev/null +++ b/packages/backend-rs/src/misc/note/summarize.rs @@ -0,0 +1,145 @@ +#[macros::export(js_name = "getNoteSummary")] +pub fn summarize_impl( + file_ids: &[String], + text: Option, + cw: Option, + has_poll: bool, +) -> String { + let mut buf: Vec = vec![]; + + if let Some(cw) = cw { + buf.push(cw) + } else if let Some(text) = text { + buf.push(text) + } + + match file_ids.len() { + 0 => (), + 1 => buf.push("📎".to_string()), + n => buf.push(format!("📎 ({})", n)), + }; + + if has_poll { + buf.push("📊".to_string()) + } + + buf.join(" ") +} + +/// Returns the summary of a post, which can be used to display posts in small spaces +/// such as push notifications. +/// +/// # Arguments +/// +/// * `note_like` : a note ([`note::Model`](crate::model::entity::note::Model))-like instance containing +/// `file_ids`, `text`, `cw`, `has_poll` fields +/// +/// # Caveats +/// +/// The `note_like` argument should not contain function calls +/// (e.g., `summarize!(note.clone())`) +/// since the function will be called multiple times after macro expansion. +/// +/// # Examples +/// +/// ``` +/// # use backend_rs::misc::note::summarize; +/// // note-like struct +/// struct NoteLike { +/// // required fields +/// file_ids: Vec, +/// text: Option, +/// cw: Option, +/// has_poll: bool, +/// // arbitrary extra fields +/// renote_id: Option, +/// reply_id: Option, +/// extra_field_1: u32, +/// extra_field_2: Vec, +/// } +/// +/// fn print_note_summary(note: &NoteLike) { +/// println!("{}", summarize!(note)); +/// } +/// ``` +#[doc(hidden)] // hide the macro in the top doc page +#[macro_export] +macro_rules! summarize { + ($note_like:expr) => { + $crate::misc::note::summarize::summarize_impl( + &$note_like.file_ids, + $note_like.text.to_owned(), + $note_like.cw.to_owned(), + $note_like.has_poll.to_owned(), + ) + }; +} + +#[doc(inline)] // show the macro in the module doc page +pub use summarize; + +#[cfg(test)] +mod unit_test { + use super::summarize; + use pretty_assertions::assert_eq; + + struct NoteLike { + file_ids: Vec, + text: Option, + cw: Option, + has_poll: bool, + } + + #[test] + fn summarize_note() { + let note = NoteLike { + file_ids: vec![], + text: Some("Hello world!".to_string()), + cw: None, + has_poll: false, + }; + assert_eq!(summarize!(note), "Hello world!"); + + let note_with_cw = NoteLike { + file_ids: vec![], + text: Some("Hello world!".to_string()), + cw: Some("Content warning".to_string()), + has_poll: false, + }; + assert_eq!(summarize!(note_with_cw), "Content warning"); + + let note_with_file_and_cw = NoteLike { + file_ids: vec!["9s7fmcqogiq4igin".to_string()], + text: None, + cw: Some("Selfie, no ec".to_string()), + has_poll: false, + }; + assert_eq!(summarize!(note_with_file_and_cw), "Selfie, no ec 📎"); + + let note_with_files_only = NoteLike { + file_ids: vec![ + "9s7fmcqogiq4igin".to_string(), + "9s7qrld5u14cey98".to_string(), + "9s7gebs5zgts4kca".to_string(), + "9s5z3e4vefqd29ee".to_string(), + ], + text: None, + cw: None, + has_poll: false, + }; + assert_eq!(summarize!(note_with_files_only), "📎 (4)"); + + let note_all = NoteLike { + file_ids: vec![ + "9s7fmcqogiq4igin".to_string(), + "9s7qrld5u14cey98".to_string(), + "9s7gebs5zgts4kca".to_string(), + "9s5z3e4vefqd29ee".to_string(), + ], + text: Some("Hello world!".to_string()), + cw: Some("Content warning".to_string()), + has_poll: true, + }; + assert_eq!(summarize!(note_all), "Content warning 📎 (4) 📊"); + } +} diff --git a/packages/backend-rs/src/misc/nyaify.rs b/packages/backend-rs/src/misc/nyaify.rs index e9c5f414ca..c58795f798 100644 --- a/packages/backend-rs/src/misc/nyaify.rs +++ b/packages/backend-rs/src/misc/nyaify.rs @@ -6,7 +6,7 @@ use regex::{Captures, Regex}; /// Converts the given text into the cat language. /// /// refs: -/// * +/// * /// * /// /// # Arguments @@ -20,7 +20,7 @@ use regex::{Captures, Regex}; /// # use backend_rs::misc::nyaify::nyaify; /// assert_eq!(nyaify("I'll take a nap.", Some("en")), "I'll take a nyap."); /// ``` -#[crate::export] +#[macros::export] pub fn nyaify(text: &str, lang: Option<&str>) -> String { let mut to_return = text.to_owned(); @@ -100,17 +100,28 @@ pub fn nyaify(text: &str, lang: Option<&str>) -> String { #[cfg(test)] mod unit_test { - use super::nyaify; use pretty_assertions::assert_eq; #[test] - fn can_nyaify() { - assert_eq!(nyaify("Hello everyone!", Some("en")), "Hello everynyan!"); - assert_eq!(nyaify("Nonbinary people", None), "Nyanbinyary people"); - assert_eq!(nyaify("1分鐘是60秒", Some("zh-TW")), "1分鐘是60喵"); - assert_eq!(nyaify("1分間は60秒です", Some("ja-JP")), "1分間は60秒です"); - assert_eq!(nyaify("あなたは誰ですか", None), "あにゃたは誰ですか"); - assert_eq!(nyaify("Ναυτικός", Some("el-GR")), "Νιαυτικός"); - assert_eq!(nyaify("일어나다", None), "일어냐다냥"); + fn nyaify() { + assert_eq!( + super::nyaify("Hello everyone!", Some("en")), + "Hello everynyan!" + ); + assert_eq!( + super::nyaify("Nonbinary people", None), + "Nyanbinyary people" + ); + assert_eq!(super::nyaify("1分鐘是60秒", Some("zh-TW")), "1分鐘是60喵"); + assert_eq!( + super::nyaify("1分間は60秒です", Some("ja-JP")), + "1分間は60秒です" + ); + assert_eq!( + super::nyaify("あなたは誰ですか", None), + "あにゃたは誰ですか" + ); + assert_eq!(super::nyaify("Ναυτικός", Some("el-GR")), "Νιαυτικός"); + assert_eq!(super::nyaify("일어나다", None), "일어냐다냥"); } } diff --git a/packages/backend-rs/src/misc/password.rs b/packages/backend-rs/src/misc/password.rs index c91de82562..bc2025f275 100644 --- a/packages/backend-rs/src/misc/password.rs +++ b/packages/backend-rs/src/misc/password.rs @@ -6,8 +6,8 @@ use argon2::{ Argon2, }; -/// Hashes the given password using [Argon2] algorithm. -#[crate::export] +/// Hashes the given password using [argon2] algorithm. +#[macros::export] pub fn hash_password(password: &str) -> Result { let salt = SaltString::generate(&mut OsRng); Ok(Argon2::default() @@ -17,16 +17,16 @@ pub fn hash_password(password: &str) -> Result Result { if is_old_password_algorithm(hash) { Ok(bcrypt::verify(password, hash)?) @@ -40,7 +40,7 @@ pub fn verify_password(password: &str, hash: &str) -> Result { /// Returns whether the [bcrypt] algorithm is used for the password hash. #[inline] -#[crate::export] +#[macros::export] pub fn is_old_password_algorithm(hash: &str) -> bool { // bcrypt hashes start with $2[ab]$ hash.starts_with("$2") @@ -48,14 +48,15 @@ pub fn is_old_password_algorithm(hash: &str) -> bool { #[cfg(test)] mod unit_test { - use super::{hash_password, is_old_password_algorithm, verify_password}; + use super::{hash_password, is_old_password_algorithm}; #[test] - fn verify_password_test() { + #[cfg_attr(miri, ignore)] // too slow + fn verify_password() { let password = "omWc*%sD^fn7o2cXmc9e2QasBdrbRuhNB*gx!J5"; let hash = hash_password(password).unwrap(); - assert!(verify_password(password, hash.as_str()).unwrap()); + assert!(super::verify_password(password, hash.as_str()).unwrap()); let argon2_hash = "$argon2id$v=19$m=19456,t=2,p=1$jty3puDFd4ENv/lgHn3ROQ$kRHDdEoVv2rruvnF731E74NxnYlvj5FMgePdGIIq3Jk"; let argon2_invalid_hash = "$argon2id$v=19$m=19456,t=2,p=1$jty3puDFd4ENv/lgHn3ROQ$kRHDdEoVv2rruvnF731E74NxnYlvj4FMgePdGIIq3Jk"; @@ -65,10 +66,10 @@ mod unit_test { assert!(!is_old_password_algorithm(argon2_hash)); assert!(is_old_password_algorithm(bcrypt_hash)); - assert!(verify_password(password, argon2_hash).unwrap()); - assert!(verify_password(password, bcrypt_hash).unwrap()); + assert!(super::verify_password(password, argon2_hash).unwrap()); + assert!(super::verify_password(password, bcrypt_hash).unwrap()); - assert!(!verify_password(password, argon2_invalid_hash).unwrap()); - assert!(!verify_password(password, bcrypt_invalid_hash).unwrap()); + assert!(!super::verify_password(password, argon2_invalid_hash).unwrap()); + assert!(!super::verify_password(password, bcrypt_invalid_hash).unwrap()); } } diff --git a/packages/backend-rs/src/misc/reaction.rs b/packages/backend-rs/src/misc/reaction.rs index f07d4761cb..01441b90a3 100644 --- a/packages/backend-rs/src/misc/reaction.rs +++ b/packages/backend-rs/src/misc/reaction.rs @@ -1,20 +1,18 @@ -use crate::database::db_conn; -use crate::misc::meta::fetch_meta; -use crate::model::entity::emoji; +use crate::{config::local_server_info, database::db_conn, model::entity::emoji}; use once_cell::sync::Lazy; use regex::Regex; use sea_orm::prelude::*; use std::collections::HashMap; -#[derive(PartialEq, Debug)] -#[crate::export(object)] +#[cfg_attr(test, derive(PartialEq, Debug))] +#[macros::export(object)] pub struct DecodedReaction { pub reaction: String, pub name: Option, pub host: Option, } -#[crate::export] +#[macros::export] pub fn decode_reaction(reaction: &str) -> DecodedReaction { // Misskey allows you to include "+" and "-" in emoji shortcodes // MFM spec: https://github.com/misskey-dev/mfm.js/blob/6aaf68089023c6adebe44123eebbc4dcd75955e0/docs/syntax.md?plain=1#L583 @@ -40,7 +38,7 @@ pub fn decode_reaction(reaction: &str) -> DecodedReaction { } } -#[crate::export] +#[macros::export] pub fn count_reactions(reactions: &HashMap) -> HashMap { let mut res = HashMap::::new(); @@ -57,13 +55,14 @@ pub fn count_reactions(reactions: &HashMap) -> HashMap #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("Idna error: {0}")] + #[doc = "UTS #46 process has failed"] + #[error(transparent)] Idna(#[from] idna::Errors), - #[error("Database error: {0}")] + #[error(transparent)] Db(#[from] DbErr), } -#[crate::export] +#[macros::export] pub async fn to_db_reaction(reaction: Option<&str>, host: Option<&str>) -> Result { if let Some(reaction) = reaction { // FIXME: Is it okay to do this only here? @@ -118,16 +117,16 @@ 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)] mod unit_test { - use super::{decode_reaction, DecodedReaction}; + use super::DecodedReaction; use pretty_assertions::{assert_eq, assert_ne}; #[test] - fn test_decode_reaction() { + fn decode_reaction() { let unicode_emoji_1 = DecodedReaction { reaction: "⭐".to_string(), name: None, @@ -139,25 +138,25 @@ mod unit_test { host: None, }; - assert_eq!(decode_reaction("⭐"), unicode_emoji_1); - assert_eq!(decode_reaction("🩷"), unicode_emoji_2); + assert_eq!(super::decode_reaction("⭐"), unicode_emoji_1); + assert_eq!(super::decode_reaction("🩷"), unicode_emoji_2); - assert_ne!(decode_reaction("⭐"), unicode_emoji_2); - assert_ne!(decode_reaction("🩷"), unicode_emoji_1); + assert_ne!(super::decode_reaction("⭐"), unicode_emoji_2); + assert_ne!(super::decode_reaction("🩷"), unicode_emoji_1); let unicode_emoji_3 = DecodedReaction { reaction: "🖖🏿".to_string(), name: None, host: None, }; - assert_eq!(decode_reaction("🖖🏿"), unicode_emoji_3); + assert_eq!(super::decode_reaction("🖖🏿"), unicode_emoji_3); let local_emoji = DecodedReaction { reaction: ":meow_melt_tears@.:".to_string(), name: Some("meow_melt_tears".to_string()), host: None, }; - assert_eq!(decode_reaction(":meow_melt_tears:"), local_emoji); + assert_eq!(super::decode_reaction(":meow_melt_tears:"), local_emoji); let remote_emoji_1 = DecodedReaction { reaction: ":meow_uwu@some-domain.example.org:".to_string(), @@ -165,7 +164,7 @@ mod unit_test { host: Some("some-domain.example.org".to_string()), }; assert_eq!( - decode_reaction(":meow_uwu@some-domain.example.org:"), + super::decode_reaction(":meow_uwu@some-domain.example.org:"), remote_emoji_1 ); @@ -175,7 +174,7 @@ mod unit_test { host: Some("xn--eckwd4c7c.example.org".to_string()), }; assert_eq!( - decode_reaction(":C++23@xn--eckwd4c7c.example.org:"), + super::decode_reaction(":C++23@xn--eckwd4c7c.example.org:"), remote_emoji_2 ); @@ -184,13 +183,16 @@ mod unit_test { name: None, host: None, }; - assert_eq!(decode_reaction(":foo"), invalid_reaction_1); + assert_eq!(super::decode_reaction(":foo"), invalid_reaction_1); let invalid_reaction_2 = DecodedReaction { reaction: ":foo&@example.com:".to_string(), name: None, host: None, }; - assert_eq!(decode_reaction(":foo&@example.com:"), invalid_reaction_2); + assert_eq!( + super::decode_reaction(":foo&@example.com:"), + invalid_reaction_2 + ); } } diff --git a/packages/backend-rs/src/misc/remove_old_attestation_challenges.rs b/packages/backend-rs/src/misc/remove_old_attestation_challenges.rs index 58c0ea8f3a..725f6ac481 100644 --- a/packages/backend-rs/src/misc/remove_old_attestation_challenges.rs +++ b/packages/backend-rs/src/misc/remove_old_attestation_challenges.rs @@ -1,12 +1,11 @@ -// TODO: We want to get rid of this +// TODO: Migrate to Redis -use crate::database::db_conn; -use crate::model::entity::attestation_challenge; +use crate::{database::db_conn, 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 -#[crate::export] +#[macros::export] pub async fn remove_old_attestation_challenges() -> Result<(), DbErr> { let res = attestation_challenge::Entity::delete_many() .filter(attestation_challenge::Column::CreatedAt.lt(Utc::now() - Duration::minutes(5))) diff --git a/packages/backend-rs/src/misc/system_info.rs b/packages/backend-rs/src/misc/system_info.rs index 95bd0dc490..4b31ceaaee 100644 --- a/packages/backend-rs/src/misc/system_info.rs +++ b/packages/backend-rs/src/misc/system_info.rs @@ -5,14 +5,14 @@ use sysinfo::{Disks, MemoryRefreshKind}; // TODO: i64 -> u64 (we can't export u64 to Node.js) -#[crate::export(object)] +#[macros::export(object)] pub struct Cpu { pub model: String, // TODO: u16 -> usize (we can't export usize to Node.js) pub cores: u16, } -#[crate::export(object)] +#[macros::export(object)] pub struct Memory { /// Total memory amount in bytes pub total: i64, @@ -22,7 +22,7 @@ pub struct Memory { pub available: i64, } -#[crate::export(object)] +#[macros::export(object)] pub struct Storage { /// Total storage space in bytes pub total: i64, @@ -30,7 +30,7 @@ pub struct Storage { pub used: i64, } -#[crate::export] +#[macros::export] pub fn cpu_info() -> Result { let system_info = system_info().lock()?; @@ -46,7 +46,7 @@ pub fn cpu_info() -> Result { }) } -#[crate::export] +#[macros::export] pub fn cpu_usage() -> Result { let mut system_info = system_info().lock()?; system_info.refresh_cpu_usage(); @@ -57,7 +57,7 @@ pub fn cpu_usage() -> Result { Ok(total_cpu_usage / (cpu_threads as f32)) } -#[crate::export] +#[macros::export] pub fn memory_usage() -> Result { let mut system_info = system_info().lock()?; @@ -70,23 +70,19 @@ pub fn memory_usage() -> Result { }) } -#[crate::export] +#[macros::export] pub fn storage_usage() -> Option { - // Get the first disk that is actualy used. + // Get the first disk that is actualy used (has available space & has at least 1 GB total space). let disks = Disks::new_with_refreshed_list(); let disk = disks .iter() - .find(|disk| disk.available_space() > 0 && disk.total_space() > disk.available_space()); + .find(|disk| disk.available_space() > 0 && disk.total_space() > 1024 * 1024 * 1024)?; - if let Some(disk) = disk { - let total = disk.total_space() as i64; - let available = disk.available_space() as i64; - return Some(Storage { - total, - used: total - available, - }); - } + let total = disk.total_space() as i64; + let available = disk.available_space() as i64; - tracing::debug!("failed to get stats"); - None + Some(Storage { + total, + used: total - available, + }) } diff --git a/packages/backend-rs/src/model/entity/abuse_user_report.rs b/packages/backend-rs/src/model/entity/abuse_user_report.rs index 3846b0f2ec..70ab552eaa 100644 --- a/packages/backend-rs/src/model/entity/abuse_user_report.rs +++ b/packages/backend-rs/src/model/entity/abuse_user_report.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "abuse_user_report")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "AbuseUserReport", use_nullable = true) -)] +#[macros::export(object, js_name = "AbuseUserReport")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/access_token.rs b/packages/backend-rs/src/model/entity/access_token.rs index beebdb2d2c..e3767b4408 100644 --- a/packages/backend-rs/src/model/entity/access_token.rs +++ b/packages/backend-rs/src/model/entity/access_token.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "access_token")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "AccessToken", use_nullable = true) -)] +#[macros::export(object, js_name = "AccessToken")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/ad.rs b/packages/backend-rs/src/model/entity/ad.rs index ab8abcb9a8..54495037d4 100644 --- a/packages/backend-rs/src/model/entity/ad.rs +++ b/packages/backend-rs/src/model/entity/ad.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "ad")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "Ad", use_nullable = true) -)] +#[macros::export(object, js_name = "Ad")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/announcement.rs b/packages/backend-rs/src/model/entity/announcement.rs index e6f3888fd5..750c8d17a7 100644 --- a/packages/backend-rs/src/model/entity/announcement.rs +++ b/packages/backend-rs/src/model/entity/announcement.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "announcement")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "Announcement", use_nullable = true) -)] +#[macros::export(object, js_name = "Announcement")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/announcement_read.rs b/packages/backend-rs/src/model/entity/announcement_read.rs index 6ab2d2cb52..169b6c9bd1 100644 --- a/packages/backend-rs/src/model/entity/announcement_read.rs +++ b/packages/backend-rs/src/model/entity/announcement_read.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "announcement_read")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "AnnouncementRead", use_nullable = true) -)] +#[macros::export(object, js_name = "AnnouncementRead")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/antenna.rs b/packages/backend-rs/src/model/entity/antenna.rs index 0e851a262b..45d40f10b7 100644 --- a/packages/backend-rs/src/model/entity/antenna.rs +++ b/packages/backend-rs/src/model/entity/antenna.rs @@ -2,14 +2,12 @@ use super::sea_orm_active_enums::AntennaSrc; use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "antenna")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "Antenna", use_nullable = true) -)] +#[macros::export(object, js_name = "Antenna")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/app.rs b/packages/backend-rs/src/model/entity/app.rs index 9d75e4fb4c..06c1d50817 100644 --- a/packages/backend-rs/src/model/entity/app.rs +++ b/packages/backend-rs/src/model/entity/app.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "app")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "App", use_nullable = true) -)] +#[macros::export(object, js_name = "App")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/attestation_challenge.rs b/packages/backend-rs/src/model/entity/attestation_challenge.rs index a64aabe619..ca3804ac04 100644 --- a/packages/backend-rs/src/model/entity/attestation_challenge.rs +++ b/packages/backend-rs/src/model/entity/attestation_challenge.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "attestation_challenge")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "AttestationChallenge", use_nullable = true) -)] +#[macros::export(object, js_name = "AttestationChallenge")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/auth_session.rs b/packages/backend-rs/src/model/entity/auth_session.rs index 30056374a3..1dc6acd414 100644 --- a/packages/backend-rs/src/model/entity/auth_session.rs +++ b/packages/backend-rs/src/model/entity/auth_session.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "auth_session")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "AuthSession", use_nullable = true) -)] +#[macros::export(object, js_name = "AuthSession")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/blocking.rs b/packages/backend-rs/src/model/entity/blocking.rs index 9bd192dc0a..3a9a0753d1 100644 --- a/packages/backend-rs/src/model/entity/blocking.rs +++ b/packages/backend-rs/src/model/entity/blocking.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "blocking")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "Blocking", use_nullable = true) -)] +#[macros::export(object, js_name = "Blocking")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/channel.rs b/packages/backend-rs/src/model/entity/channel.rs index ab4574de57..221a72bc4c 100644 --- a/packages/backend-rs/src/model/entity/channel.rs +++ b/packages/backend-rs/src/model/entity/channel.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "channel")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "Channel", use_nullable = true) -)] +#[macros::export(object, js_name = "Channel")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/channel_following.rs b/packages/backend-rs/src/model/entity/channel_following.rs index 29f0d46814..9be21b8511 100644 --- a/packages/backend-rs/src/model/entity/channel_following.rs +++ b/packages/backend-rs/src/model/entity/channel_following.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "channel_following")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "ChannelFollowing", use_nullable = true) -)] +#[macros::export(object, js_name = "ChannelFollowing")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/channel_note_pining.rs b/packages/backend-rs/src/model/entity/channel_note_pining.rs index 79f067d6cf..d03ee961dd 100644 --- a/packages/backend-rs/src/model/entity/channel_note_pining.rs +++ b/packages/backend-rs/src/model/entity/channel_note_pining.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "channel_note_pining")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "ChannelNotePining", use_nullable = true) -)] +#[macros::export(object, js_name = "ChannelNotePining")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/clip.rs b/packages/backend-rs/src/model/entity/clip.rs index d1e1c18db2..fcd5b11315 100644 --- a/packages/backend-rs/src/model/entity/clip.rs +++ b/packages/backend-rs/src/model/entity/clip.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "clip")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "Clip", use_nullable = true) -)] +#[macros::export(object, js_name = "Clip")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/clip_note.rs b/packages/backend-rs/src/model/entity/clip_note.rs index a76b5d5d8a..9cce432242 100644 --- a/packages/backend-rs/src/model/entity/clip_note.rs +++ b/packages/backend-rs/src/model/entity/clip_note.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "clip_note")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "ClipNote", use_nullable = true) -)] +#[macros::export(object, js_name = "ClipNote")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/drive_file.rs b/packages/backend-rs/src/model/entity/drive_file.rs index 55f771843f..e2139a9a7d 100644 --- a/packages/backend-rs/src/model/entity/drive_file.rs +++ b/packages/backend-rs/src/model/entity/drive_file.rs @@ -2,14 +2,12 @@ use super::sea_orm_active_enums::DriveFileUsageHint; use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "drive_file")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "DriveFile", use_nullable = true) -)] +#[macros::export(object, js_name = "DriveFile")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/drive_folder.rs b/packages/backend-rs/src/model/entity/drive_folder.rs index 972a65e173..428fc70251 100644 --- a/packages/backend-rs/src/model/entity/drive_folder.rs +++ b/packages/backend-rs/src/model/entity/drive_folder.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "drive_folder")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "DriveFolder", use_nullable = true) -)] +#[macros::export(object, js_name = "DriveFolder")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/emoji.rs b/packages/backend-rs/src/model/entity/emoji.rs index f1254dbcfe..44744ede08 100644 --- a/packages/backend-rs/src/model/entity/emoji.rs +++ b/packages/backend-rs/src/model/entity/emoji.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "emoji")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "Emoji", use_nullable = true) -)] +#[macros::export(object, js_name = "Emoji")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/follow_request.rs b/packages/backend-rs/src/model/entity/follow_request.rs index e4629aeaf5..f7b6231242 100644 --- a/packages/backend-rs/src/model/entity/follow_request.rs +++ b/packages/backend-rs/src/model/entity/follow_request.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "follow_request")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "FollowRequest", use_nullable = true) -)] +#[macros::export(object, js_name = "FollowRequest")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/following.rs b/packages/backend-rs/src/model/entity/following.rs index 386a37e12f..3b4459bd9e 100644 --- a/packages/backend-rs/src/model/entity/following.rs +++ b/packages/backend-rs/src/model/entity/following.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "following")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "Following", use_nullable = true) -)] +#[macros::export(object, js_name = "Following")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/gallery_like.rs b/packages/backend-rs/src/model/entity/gallery_like.rs index faff8906aa..062854ef29 100644 --- a/packages/backend-rs/src/model/entity/gallery_like.rs +++ b/packages/backend-rs/src/model/entity/gallery_like.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "gallery_like")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "GalleryLike", use_nullable = true) -)] +#[macros::export(object, js_name = "GalleryLike")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/gallery_post.rs b/packages/backend-rs/src/model/entity/gallery_post.rs index e66a192ce6..3fc93da1ee 100644 --- a/packages/backend-rs/src/model/entity/gallery_post.rs +++ b/packages/backend-rs/src/model/entity/gallery_post.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "gallery_post")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "GalleryPost", use_nullable = true) -)] +#[macros::export(object, js_name = "GalleryPost")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/hashtag.rs b/packages/backend-rs/src/model/entity/hashtag.rs index 56b9314e08..f9bb74b4fe 100644 --- a/packages/backend-rs/src/model/entity/hashtag.rs +++ b/packages/backend-rs/src/model/entity/hashtag.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "hashtag")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "Hashtag", use_nullable = true) -)] +#[macros::export(object, js_name = "Hashtag")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/instance.rs b/packages/backend-rs/src/model/entity/instance.rs index 3bc0a6e7c7..9a49e25ce0 100644 --- a/packages/backend-rs/src/model/entity/instance.rs +++ b/packages/backend-rs/src/model/entity/instance.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "instance")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "Instance", use_nullable = true) -)] +#[macros::export(object, js_name = "Instance")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/messaging_message.rs b/packages/backend-rs/src/model/entity/messaging_message.rs index 7665b58674..202496f012 100644 --- a/packages/backend-rs/src/model/entity/messaging_message.rs +++ b/packages/backend-rs/src/model/entity/messaging_message.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "messaging_message")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "MessagingMessage", use_nullable = true) -)] +#[macros::export(object, js_name = "MessagingMessage")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/meta.rs b/packages/backend-rs/src/model/entity/meta.rs index 3bf205d040..023fcef525 100644 --- a/packages/backend-rs/src/model/entity/meta.rs +++ b/packages/backend-rs/src/model/entity/meta.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "meta")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "Meta", use_nullable = true) -)] +#[macros::export(object, js_name = "Meta")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/migrations.rs b/packages/backend-rs/src/model/entity/migrations.rs index f0d55ba123..b03aaabc3f 100644 --- a/packages/backend-rs/src/model/entity/migrations.rs +++ b/packages/backend-rs/src/model/entity/migrations.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "migrations")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "Migrations", use_nullable = true) -)] +#[macros::export(object, js_name = "Migrations")] pub struct Model { #[sea_orm(primary_key)] pub id: i32, diff --git a/packages/backend-rs/src/model/entity/mod.rs b/packages/backend-rs/src/model/entity/mod.rs index 7f8d16f1ae..ffb21352d2 100644 --- a/packages/backend-rs/src/model/entity/mod.rs +++ b/packages/backend-rs/src/model/entity/mod.rs @@ -53,7 +53,6 @@ pub mod registry_item; pub mod relay; pub mod renote_muting; pub mod reply_muting; -pub mod scheduled_note; pub mod sea_orm_active_enums; pub mod signin; pub mod sw_subscription; diff --git a/packages/backend-rs/src/model/entity/moderation_log.rs b/packages/backend-rs/src/model/entity/moderation_log.rs index 5ede8bdaf4..2ce71a9c78 100644 --- a/packages/backend-rs/src/model/entity/moderation_log.rs +++ b/packages/backend-rs/src/model/entity/moderation_log.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "moderation_log")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "ModerationLog", use_nullable = true) -)] +#[macros::export(object, js_name = "ModerationLog")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/muted_note.rs b/packages/backend-rs/src/model/entity/muted_note.rs index e536030863..5e89c2ed2f 100644 --- a/packages/backend-rs/src/model/entity/muted_note.rs +++ b/packages/backend-rs/src/model/entity/muted_note.rs @@ -2,14 +2,12 @@ use super::sea_orm_active_enums::MutedNoteReason; use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "muted_note")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "MutedNote", use_nullable = true) -)] +#[macros::export(object, js_name = "MutedNote")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/muting.rs b/packages/backend-rs/src/model/entity/muting.rs index d323c5f4df..eff07c6a17 100644 --- a/packages/backend-rs/src/model/entity/muting.rs +++ b/packages/backend-rs/src/model/entity/muting.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "muting")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "Muting", use_nullable = true) -)] +#[macros::export(object, js_name = "Muting")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/note.rs b/packages/backend-rs/src/model/entity/note.rs index 37cbd54862..e3bf3c9490 100644 --- a/packages/backend-rs/src/model/entity/note.rs +++ b/packages/backend-rs/src/model/entity/note.rs @@ -2,14 +2,12 @@ use super::sea_orm_active_enums::NoteVisibility; use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "note")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "Note", use_nullable = true) -)] +#[macros::export(object, js_name = "Note")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, @@ -68,6 +66,8 @@ pub struct Model { #[sea_orm(column_name = "updatedAt")] pub updated_at: Option, pub lang: Option, + #[sea_orm(column_name = "scheduledAt")] + pub scheduled_at: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] @@ -124,8 +124,6 @@ pub enum Relation { PromoNote, #[sea_orm(has_many = "super::promo_read::Entity")] PromoRead, - #[sea_orm(has_many = "super::scheduled_note::Entity")] - ScheduledNote, #[sea_orm( belongs_to = "super::user::Entity", from = "Column::UserId", @@ -228,12 +226,6 @@ impl Related for Entity { } } -impl Related for Entity { - fn to() -> RelationDef { - Relation::ScheduledNote.def() - } -} - impl Related for Entity { fn to() -> RelationDef { Relation::User.def() diff --git a/packages/backend-rs/src/model/entity/note_edit.rs b/packages/backend-rs/src/model/entity/note_edit.rs index 1bdb186cc5..66e9a8e5ca 100644 --- a/packages/backend-rs/src/model/entity/note_edit.rs +++ b/packages/backend-rs/src/model/entity/note_edit.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "note_edit")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "NoteEdit", use_nullable = true) -)] +#[macros::export(object, js_name = "NoteEdit")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/note_favorite.rs b/packages/backend-rs/src/model/entity/note_favorite.rs index 0083803024..8db8612109 100644 --- a/packages/backend-rs/src/model/entity/note_favorite.rs +++ b/packages/backend-rs/src/model/entity/note_favorite.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "note_favorite")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "NoteFavorite", use_nullable = true) -)] +#[macros::export(object, js_name = "NoteFavorite")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/note_file.rs b/packages/backend-rs/src/model/entity/note_file.rs index 9d13c7506d..6742bf67aa 100644 --- a/packages/backend-rs/src/model/entity/note_file.rs +++ b/packages/backend-rs/src/model/entity/note_file.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "note_file")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "NoteFile", use_nullable = true) -)] +#[macros::export(object, js_name = "NoteFile")] pub struct Model { #[sea_orm(column_name = "serialNo", primary_key)] pub serial_no: i64, diff --git a/packages/backend-rs/src/model/entity/note_reaction.rs b/packages/backend-rs/src/model/entity/note_reaction.rs index 4d2e0df11a..e329db8d0f 100644 --- a/packages/backend-rs/src/model/entity/note_reaction.rs +++ b/packages/backend-rs/src/model/entity/note_reaction.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "note_reaction")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "NoteReaction", use_nullable = true) -)] +#[macros::export(object, js_name = "NoteReaction")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/note_thread_muting.rs b/packages/backend-rs/src/model/entity/note_thread_muting.rs index a90e4dba64..29aa782bd5 100644 --- a/packages/backend-rs/src/model/entity/note_thread_muting.rs +++ b/packages/backend-rs/src/model/entity/note_thread_muting.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "note_thread_muting")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "NoteThreadMuting", use_nullable = true) -)] +#[macros::export(object, js_name = "NoteThreadMuting")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/note_unread.rs b/packages/backend-rs/src/model/entity/note_unread.rs index 2b74e3a63c..da312e6e69 100644 --- a/packages/backend-rs/src/model/entity/note_unread.rs +++ b/packages/backend-rs/src/model/entity/note_unread.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "note_unread")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "NoteUnread", use_nullable = true) -)] +#[macros::export(object, js_name = "NoteUnread")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/note_watching.rs b/packages/backend-rs/src/model/entity/note_watching.rs index 8e0c831cdd..5ab675a8e3 100644 --- a/packages/backend-rs/src/model/entity/note_watching.rs +++ b/packages/backend-rs/src/model/entity/note_watching.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "note_watching")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "NoteWatching", use_nullable = true) -)] +#[macros::export(object, js_name = "NoteWatching")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/notification.rs b/packages/backend-rs/src/model/entity/notification.rs index e9375a82c0..e73a9cd1fb 100644 --- a/packages/backend-rs/src/model/entity/notification.rs +++ b/packages/backend-rs/src/model/entity/notification.rs @@ -2,14 +2,12 @@ use super::sea_orm_active_enums::NotificationType; use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "notification")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "Notification", use_nullable = true) -)] +#[macros::export(object, js_name = "Notification")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/page.rs b/packages/backend-rs/src/model/entity/page.rs index eb4ce80c31..f252a43532 100644 --- a/packages/backend-rs/src/model/entity/page.rs +++ b/packages/backend-rs/src/model/entity/page.rs @@ -2,14 +2,12 @@ use super::sea_orm_active_enums::PageVisibility; use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "page")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "Page", use_nullable = true) -)] +#[macros::export(object, js_name = "Page")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/page_like.rs b/packages/backend-rs/src/model/entity/page_like.rs index 452f43fd26..d6aa22c12a 100644 --- a/packages/backend-rs/src/model/entity/page_like.rs +++ b/packages/backend-rs/src/model/entity/page_like.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "page_like")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "PageLike", use_nullable = true) -)] +#[macros::export(object, js_name = "PageLike")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/password_reset_request.rs b/packages/backend-rs/src/model/entity/password_reset_request.rs index 9a65693494..bd4978a44c 100644 --- a/packages/backend-rs/src/model/entity/password_reset_request.rs +++ b/packages/backend-rs/src/model/entity/password_reset_request.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "password_reset_request")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "PasswordResetRequest", use_nullable = true) -)] +#[macros::export(object, js_name = "PasswordResetRequest")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/poll.rs b/packages/backend-rs/src/model/entity/poll.rs index 29c322d711..501243252f 100644 --- a/packages/backend-rs/src/model/entity/poll.rs +++ b/packages/backend-rs/src/model/entity/poll.rs @@ -2,14 +2,12 @@ use super::sea_orm_active_enums::PollNoteVisibility; use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "poll")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "Poll", use_nullable = true) -)] +#[macros::export(object, js_name = "Poll")] pub struct Model { #[sea_orm(column_name = "noteId", primary_key, auto_increment = false, unique)] pub note_id: String, diff --git a/packages/backend-rs/src/model/entity/poll_vote.rs b/packages/backend-rs/src/model/entity/poll_vote.rs index 313618af16..f3a84d2cdf 100644 --- a/packages/backend-rs/src/model/entity/poll_vote.rs +++ b/packages/backend-rs/src/model/entity/poll_vote.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "poll_vote")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "PollVote", use_nullable = true) -)] +#[macros::export(object, js_name = "PollVote")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/prelude.rs b/packages/backend-rs/src/model/entity/prelude.rs index 9da0c02506..57fff023db 100644 --- a/packages/backend-rs/src/model/entity/prelude.rs +++ b/packages/backend-rs/src/model/entity/prelude.rs @@ -51,7 +51,6 @@ pub use super::registry_item::Entity as RegistryItem; pub use super::relay::Entity as Relay; pub use super::renote_muting::Entity as RenoteMuting; pub use super::reply_muting::Entity as ReplyMuting; -pub use super::scheduled_note::Entity as ScheduledNote; pub use super::signin::Entity as Signin; pub use super::sw_subscription::Entity as SwSubscription; pub use super::used_username::Entity as UsedUsername; diff --git a/packages/backend-rs/src/model/entity/promo_note.rs b/packages/backend-rs/src/model/entity/promo_note.rs index 1c6e216991..24b1b98651 100644 --- a/packages/backend-rs/src/model/entity/promo_note.rs +++ b/packages/backend-rs/src/model/entity/promo_note.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "promo_note")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "PromoNote", use_nullable = true) -)] +#[macros::export(object, js_name = "PromoNote")] pub struct Model { #[sea_orm(column_name = "noteId", primary_key, auto_increment = false, unique)] pub note_id: String, diff --git a/packages/backend-rs/src/model/entity/promo_read.rs b/packages/backend-rs/src/model/entity/promo_read.rs index 17c2433cd3..097b6e3cc4 100644 --- a/packages/backend-rs/src/model/entity/promo_read.rs +++ b/packages/backend-rs/src/model/entity/promo_read.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "promo_read")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "PromoRead", use_nullable = true) -)] +#[macros::export(object, js_name = "PromoRead")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/registration_ticket.rs b/packages/backend-rs/src/model/entity/registration_ticket.rs index 9192ac32a6..7a25c016b8 100644 --- a/packages/backend-rs/src/model/entity/registration_ticket.rs +++ b/packages/backend-rs/src/model/entity/registration_ticket.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "registration_ticket")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "RegistrationTicket", use_nullable = true) -)] +#[macros::export(object, js_name = "RegistrationTicket")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/registry_item.rs b/packages/backend-rs/src/model/entity/registry_item.rs index 66f3115b7c..7dbc5e37b9 100644 --- a/packages/backend-rs/src/model/entity/registry_item.rs +++ b/packages/backend-rs/src/model/entity/registry_item.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "registry_item")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "RegistryItem", use_nullable = true) -)] +#[macros::export(object, js_name = "RegistryItem")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/relay.rs b/packages/backend-rs/src/model/entity/relay.rs index 567abb597c..6648179f87 100644 --- a/packages/backend-rs/src/model/entity/relay.rs +++ b/packages/backend-rs/src/model/entity/relay.rs @@ -2,14 +2,12 @@ use super::sea_orm_active_enums::RelayStatus; use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "relay")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "Relay", use_nullable = true) -)] +#[macros::export(object, js_name = "Relay")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/renote_muting.rs b/packages/backend-rs/src/model/entity/renote_muting.rs index 6315a17212..1adc965f16 100644 --- a/packages/backend-rs/src/model/entity/renote_muting.rs +++ b/packages/backend-rs/src/model/entity/renote_muting.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "renote_muting")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "RenoteMuting", use_nullable = true) -)] +#[macros::export(object, js_name = "RenoteMuting")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/reply_muting.rs b/packages/backend-rs/src/model/entity/reply_muting.rs index 5f24dcad52..5f01f223df 100644 --- a/packages/backend-rs/src/model/entity/reply_muting.rs +++ b/packages/backend-rs/src/model/entity/reply_muting.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "reply_muting")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "ReplyMuting", use_nullable = true) -)] +#[macros::export(object, js_name = "ReplyMuting")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/scheduled_note.rs b/packages/backend-rs/src/model/entity/scheduled_note.rs deleted file mode 100644 index f4c5b0b4c4..0000000000 --- a/packages/backend-rs/src/model/entity/scheduled_note.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 - -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -#[sea_orm(table_name = "scheduled_note")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "ScheduledNote", use_nullable = true) -)] -pub struct Model { - #[sea_orm(primary_key, auto_increment = false)] - pub id: String, - #[sea_orm(column_name = "noteId")] - pub note_id: String, - #[sea_orm(column_name = "userId")] - pub user_id: String, - #[sea_orm(column_name = "scheduledAt")] - pub scheduled_at: DateTimeWithTimeZone, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::note::Entity", - from = "Column::NoteId", - to = "super::note::Column::Id", - on_update = "NoAction", - on_delete = "Cascade" - )] - Note, - #[sea_orm( - belongs_to = "super::user::Entity", - from = "Column::UserId", - to = "super::user::Column::Id", - on_update = "NoAction", - on_delete = "Cascade" - )] - User, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Note.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::User.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs b/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs index c25618ef3a..5515dad01e 100644 --- a/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs +++ b/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs @@ -1,13 +1,11 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive( - Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, serde::Serialize, serde::Deserialize, -)] +#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -#[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] +#[macros::derive_clone_and_export(string_enum = "camelCase")] #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "antenna_src")] pub enum AntennaSrc { #[sea_orm(string_value = "all")] @@ -23,12 +21,9 @@ pub enum AntennaSrc { #[sea_orm(string_value = "users")] Users, } -#[derive( - Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, serde::Serialize, serde::Deserialize, -)] +#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -#[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] +#[macros::derive_clone_and_export(string_enum = "camelCase")] #[sea_orm( rs_type = "String", db_type = "Enum", @@ -40,12 +35,9 @@ pub enum DriveFileUsageHint { #[sea_orm(string_value = "userBanner")] UserBanner, } -#[derive( - Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, serde::Serialize, serde::Deserialize, -)] +#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -#[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] +#[macros::derive_clone_and_export(string_enum = "camelCase")] #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "muted_note_reason")] pub enum MutedNoteReason { #[sea_orm(string_value = "manual")] @@ -57,12 +49,9 @@ pub enum MutedNoteReason { #[sea_orm(string_value = "word")] Word, } -#[derive( - Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, serde::Serialize, serde::Deserialize, -)] +#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -#[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] +#[macros::derive_clone_and_export(string_enum = "camelCase")] #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "note_visibility")] pub enum NoteVisibility { #[sea_orm(string_value = "followers")] @@ -76,12 +65,9 @@ pub enum NoteVisibility { #[sea_orm(string_value = "specified")] Specified, } -#[derive( - Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, serde::Serialize, serde::Deserialize, -)] +#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -#[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] +#[macros::derive_clone_and_export(string_enum = "camelCase")] #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "notification_type")] pub enum NotificationType { #[sea_orm(string_value = "app")] @@ -109,12 +95,9 @@ pub enum NotificationType { #[sea_orm(string_value = "reply")] Reply, } -#[derive( - Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, serde::Serialize, serde::Deserialize, -)] +#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -#[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] +#[macros::derive_clone_and_export(string_enum = "camelCase")] #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "page_visibility")] pub enum PageVisibility { #[sea_orm(string_value = "followers")] @@ -124,12 +107,9 @@ pub enum PageVisibility { #[sea_orm(string_value = "specified")] Specified, } -#[derive( - Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, serde::Serialize, serde::Deserialize, -)] +#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -#[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] +#[macros::derive_clone_and_export(string_enum = "camelCase")] #[sea_orm( rs_type = "String", db_type = "Enum", @@ -145,12 +125,9 @@ pub enum PollNoteVisibility { #[sea_orm(string_value = "specified")] Specified, } -#[derive( - Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, serde::Serialize, serde::Deserialize, -)] +#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -#[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] +#[macros::derive_clone_and_export(string_enum = "camelCase")] #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "relay_status")] pub enum RelayStatus { #[sea_orm(string_value = "accepted")] @@ -160,12 +137,9 @@ pub enum RelayStatus { #[sea_orm(string_value = "requesting")] Requesting, } -#[derive( - Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, serde::Serialize, serde::Deserialize, -)] +#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -#[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] +#[macros::derive_clone_and_export(string_enum = "camelCase")] #[sea_orm( rs_type = "String", db_type = "Enum", @@ -181,12 +155,9 @@ pub enum UserEmojiModPerm { #[sea_orm(string_value = "unauthorized")] Unauthorized, } -#[derive( - Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, serde::Serialize, serde::Deserialize, -)] +#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -#[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] +#[macros::derive_clone_and_export(string_enum = "camelCase")] #[sea_orm( rs_type = "String", db_type = "Enum", @@ -200,12 +171,9 @@ pub enum UserProfileFfvisibility { #[sea_orm(string_value = "public")] Public, } -#[derive( - Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, serde::Serialize, serde::Deserialize, -)] +#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -#[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] +#[macros::derive_clone_and_export(string_enum = "camelCase")] #[sea_orm( rs_type = "String", db_type = "Enum", diff --git a/packages/backend-rs/src/model/entity/signin.rs b/packages/backend-rs/src/model/entity/signin.rs index df5590a530..aec9f2dfe6 100644 --- a/packages/backend-rs/src/model/entity/signin.rs +++ b/packages/backend-rs/src/model/entity/signin.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "signin")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "Signin", use_nullable = true) -)] +#[macros::export(object, js_name = "Signin")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/sw_subscription.rs b/packages/backend-rs/src/model/entity/sw_subscription.rs index bedb091701..5273587e0c 100644 --- a/packages/backend-rs/src/model/entity/sw_subscription.rs +++ b/packages/backend-rs/src/model/entity/sw_subscription.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "sw_subscription")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "SwSubscription", use_nullable = true) -)] +#[macros::export(object, js_name = "SwSubscription")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/used_username.rs b/packages/backend-rs/src/model/entity/used_username.rs index c9bec5bd96..84e55baba7 100644 --- a/packages/backend-rs/src/model/entity/used_username.rs +++ b/packages/backend-rs/src/model/entity/used_username.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "used_username")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "UsedUsername", use_nullable = true) -)] +#[macros::export(object, js_name = "UsedUsername")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub username: String, diff --git a/packages/backend-rs/src/model/entity/user.rs b/packages/backend-rs/src/model/entity/user.rs index 974ba2890c..f11e1e43d9 100644 --- a/packages/backend-rs/src/model/entity/user.rs +++ b/packages/backend-rs/src/model/entity/user.rs @@ -2,14 +2,12 @@ use super::sea_orm_active_enums::UserEmojiModPerm; use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "user")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "User", use_nullable = true) -)] +#[macros::export(object, js_name = "User")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, @@ -153,8 +151,6 @@ pub enum Relation { PromoRead, #[sea_orm(has_many = "super::registry_item::Entity")] RegistryItem, - #[sea_orm(has_many = "super::scheduled_note::Entity")] - ScheduledNote, #[sea_orm(has_many = "super::signin::Entity")] Signin, #[sea_orm(has_many = "super::sw_subscription::Entity")] @@ -347,12 +343,6 @@ impl Related for Entity { } } -impl Related for Entity { - fn to() -> RelationDef { - Relation::ScheduledNote.def() - } -} - impl Related for Entity { fn to() -> RelationDef { Relation::Signin.def() diff --git a/packages/backend-rs/src/model/entity/user_group.rs b/packages/backend-rs/src/model/entity/user_group.rs index d57a730a48..f035750e4b 100644 --- a/packages/backend-rs/src/model/entity/user_group.rs +++ b/packages/backend-rs/src/model/entity/user_group.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "user_group")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "UserGroup", use_nullable = true) -)] +#[macros::export(object, js_name = "UserGroup")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/user_group_invitation.rs b/packages/backend-rs/src/model/entity/user_group_invitation.rs index 852638b0e4..41073b38fc 100644 --- a/packages/backend-rs/src/model/entity/user_group_invitation.rs +++ b/packages/backend-rs/src/model/entity/user_group_invitation.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "user_group_invitation")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "UserGroupInvitation", use_nullable = true) -)] +#[macros::export(object, js_name = "UserGroupInvitation")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/user_group_invite.rs b/packages/backend-rs/src/model/entity/user_group_invite.rs index 14f01486f6..28c831155c 100644 --- a/packages/backend-rs/src/model/entity/user_group_invite.rs +++ b/packages/backend-rs/src/model/entity/user_group_invite.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "user_group_invite")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "UserGroupInvite", use_nullable = true) -)] +#[macros::export(object, js_name = "UserGroupInvite")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/user_group_joining.rs b/packages/backend-rs/src/model/entity/user_group_joining.rs index fabd7eccbd..99f38d1856 100644 --- a/packages/backend-rs/src/model/entity/user_group_joining.rs +++ b/packages/backend-rs/src/model/entity/user_group_joining.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "user_group_joining")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "UserGroupJoining", use_nullable = true) -)] +#[macros::export(object, js_name = "UserGroupJoining")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/user_ip.rs b/packages/backend-rs/src/model/entity/user_ip.rs index 77a9af9987..326d82b74a 100644 --- a/packages/backend-rs/src/model/entity/user_ip.rs +++ b/packages/backend-rs/src/model/entity/user_ip.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "user_ip")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "UserIp", use_nullable = true) -)] +#[macros::export(object, js_name = "UserIp")] pub struct Model { #[sea_orm(primary_key)] pub id: i32, diff --git a/packages/backend-rs/src/model/entity/user_keypair.rs b/packages/backend-rs/src/model/entity/user_keypair.rs index dad29ab3b3..cbc203e6ec 100644 --- a/packages/backend-rs/src/model/entity/user_keypair.rs +++ b/packages/backend-rs/src/model/entity/user_keypair.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "user_keypair")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "UserKeypair", use_nullable = true) -)] +#[macros::export(object, js_name = "UserKeypair")] pub struct Model { #[sea_orm(column_name = "userId", primary_key, auto_increment = false, unique)] pub user_id: String, diff --git a/packages/backend-rs/src/model/entity/user_list.rs b/packages/backend-rs/src/model/entity/user_list.rs index ce6f3f0095..839eab1c02 100644 --- a/packages/backend-rs/src/model/entity/user_list.rs +++ b/packages/backend-rs/src/model/entity/user_list.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "user_list")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "UserList", use_nullable = true) -)] +#[macros::export(object, js_name = "UserList")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/user_list_joining.rs b/packages/backend-rs/src/model/entity/user_list_joining.rs index ae1c2ad429..3158cbad21 100644 --- a/packages/backend-rs/src/model/entity/user_list_joining.rs +++ b/packages/backend-rs/src/model/entity/user_list_joining.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "user_list_joining")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "UserListJoining", use_nullable = true) -)] +#[macros::export(object, js_name = "UserListJoining")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/user_note_pining.rs b/packages/backend-rs/src/model/entity/user_note_pining.rs index fcdc3d9d52..2a4c4e83e2 100644 --- a/packages/backend-rs/src/model/entity/user_note_pining.rs +++ b/packages/backend-rs/src/model/entity/user_note_pining.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "user_note_pining")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "UserNotePining", use_nullable = true) -)] +#[macros::export(object, js_name = "UserNotePining")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/user_pending.rs b/packages/backend-rs/src/model/entity/user_pending.rs index 9b13a2f655..6c0265bc2f 100644 --- a/packages/backend-rs/src/model/entity/user_pending.rs +++ b/packages/backend-rs/src/model/entity/user_pending.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "user_pending")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "UserPending", use_nullable = true) -)] +#[macros::export(object, js_name = "UserPending")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/user_profile.rs b/packages/backend-rs/src/model/entity/user_profile.rs index f9ea7e0b68..1c55a1a604 100644 --- a/packages/backend-rs/src/model/entity/user_profile.rs +++ b/packages/backend-rs/src/model/entity/user_profile.rs @@ -3,14 +3,12 @@ use super::sea_orm_active_enums::UserProfileFfvisibility; use super::sea_orm_active_enums::UserProfileMutingNotificationTypes; use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "user_profile")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "UserProfile", use_nullable = true) -)] +#[macros::export(object, js_name = "UserProfile")] pub struct Model { #[sea_orm(column_name = "userId", primary_key, auto_increment = false, unique)] pub user_id: String, diff --git a/packages/backend-rs/src/model/entity/user_publickey.rs b/packages/backend-rs/src/model/entity/user_publickey.rs index f8bc8629eb..a0605e9d2e 100644 --- a/packages/backend-rs/src/model/entity/user_publickey.rs +++ b/packages/backend-rs/src/model/entity/user_publickey.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "user_publickey")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "UserPublickey", use_nullable = true) -)] +#[macros::export(object, js_name = "UserPublickey")] pub struct Model { #[sea_orm(column_name = "userId", primary_key, auto_increment = false, unique)] pub user_id: String, diff --git a/packages/backend-rs/src/model/entity/user_security_key.rs b/packages/backend-rs/src/model/entity/user_security_key.rs index 0aba2916f0..fdc61638a3 100644 --- a/packages/backend-rs/src/model/entity/user_security_key.rs +++ b/packages/backend-rs/src/model/entity/user_security_key.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "user_security_key")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "UserSecurityKey", use_nullable = true) -)] +#[macros::export(object, js_name = "UserSecurityKey")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/model/entity/webhook.rs b/packages/backend-rs/src/model/entity/webhook.rs index 5795550630..b2695bee62 100644 --- a/packages/backend-rs/src/model/entity/webhook.rs +++ b/packages/backend-rs/src/model/entity/webhook.rs @@ -1,14 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[sea_orm(table_name = "webhook")] -#[cfg_attr( - feature = "napi", - napi_derive::napi(object, js_name = "Webhook", use_nullable = true) -)] +#[macros::export(object, js_name = "Webhook")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, diff --git a/packages/backend-rs/src/service/antenna/cache.rs b/packages/backend-rs/src/service/antenna/cache.rs new file mode 100644 index 0000000000..ce83895368 --- /dev/null +++ b/packages/backend-rs/src/service/antenna/cache.rs @@ -0,0 +1,26 @@ +//! In-memory antennas cache handler + +use crate::{database::db_conn, model::entity::antenna}; +use sea_orm::prelude::*; +use std::sync::{Arc, Mutex}; + +static CACHE: Mutex>> = Mutex::new(None); + +fn set(antennas: Arc<[antenna::Model]>) { + let _ = CACHE.lock().map(|mut cache| *cache = Some(antennas)); +} + +pub(super) async fn update() -> Result, DbErr> { + tracing::debug!("updating cache"); + let antennas: Arc<[antenna::Model]> = + antenna::Entity::find().all(db_conn().await?).await?.into(); + set(antennas.clone()); + Ok(antennas) +} + +pub(super) async fn get() -> Result, DbErr> { + if let Some(cache) = CACHE.lock().ok().and_then(|cache| cache.clone()) { + return Ok(cache); + } + update().await +} diff --git a/packages/backend-rs/src/service/antenna/check_hit.rs b/packages/backend-rs/src/service/antenna/check_hit.rs index 9dfbd65e8b..794b2f794e 100644 --- a/packages/backend-rs/src/service/antenna/check_hit.rs +++ b/packages/backend-rs/src/service/antenna/check_hit.rs @@ -1,17 +1,18 @@ -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 crate::{ + config::CONFIG, + database::{cache, db_conn}, + federation::acct::Acct, + model::entity::{antenna, blocking, following, note, sea_orm_active_enums::*}, +}; +use sea_orm::{prelude::*, QuerySelect}; #[derive(thiserror::Error, Debug)] pub enum AntennaCheckError { - #[error("Database error: {0}")] + #[doc = "database error"] + #[error(transparent)] Db(#[from] DbErr), - #[error("Cache error: {0}")] + #[error("Redis cache operation has failed")] Cache(#[from] cache::Error), - #[error("User profile not found: {0}")] - UserProfileNotFound(String), } fn match_all(space_separated_words: &str, text: &str, case_sensitive: bool) -> bool { @@ -27,7 +28,7 @@ fn match_all(space_separated_words: &str, text: &str, case_sensitive: bool) -> b } } -pub async fn check_hit_antenna( +pub(super) async fn check_hit_antenna( antenna: &antenna::Model, note: ¬e::Model, note_all_texts: &[String], @@ -56,14 +57,15 @@ pub async fn check_hit_antenna( return Ok(false); } } else if antenna.src == AntennaSrc::Instances { - let is_from_one_of_specified_servers = antenna.instances.iter().any(|host| { - host.to_ascii_lowercase() - == note_author - .host - .clone() - .unwrap_or(CONFIG.host.clone()) - .to_ascii_lowercase() - }); + let note_author_host = note_author + .host + .clone() + .unwrap_or_else(|| CONFIG.host.clone()) + .to_ascii_lowercase(); + let is_from_one_of_specified_servers = antenna + .instances + .iter() + .any(|host| host.to_ascii_lowercase() == note_author_host); if !is_from_one_of_specified_servers { return Ok(false); @@ -115,7 +117,10 @@ pub async fn check_hit_antenna( return Ok(false); } - if [NoteVisibility::Home, NoteVisibility::Followers].contains(¬e.visibility) { + if matches!( + note.visibility, + NoteVisibility::Home | NoteVisibility::Followers + ) { let following_user_ids: Vec = if let Some(ids) = cache::get_one(cache::Category::Follow, &antenna.user_id).await? { ids @@ -153,7 +158,7 @@ mod unit_test { use pretty_assertions::assert_eq; #[test] - fn test_match_all() { + fn check_match_string() { assert_eq!(match_all("Apple", "apple and banana", false), true); assert_eq!(match_all("Apple", "apple and banana", true), false); assert_eq!(match_all("Apple Banana", "apple and banana", false), true); diff --git a/packages/backend-rs/src/service/antenna/mod.rs b/packages/backend-rs/src/service/antenna/mod.rs index 607d5aa95c..2f255581ed 100644 --- a/packages/backend-rs/src/service/antenna/mod.rs +++ b/packages/backend-rs/src/service/antenna/mod.rs @@ -1,2 +1,4 @@ +mod cache; mod check_hit; pub mod process_new_note; +pub mod update; diff --git a/packages/backend-rs/src/service/antenna/process_new_note.rs b/packages/backend-rs/src/service/antenna/process_new_note.rs index ba9ba10d78..13fc7fdcbb 100644 --- a/packages/backend-rs/src/service/antenna/process_new_note.rs +++ b/packages/backend-rs/src/service/antenna/process_new_note.rs @@ -1,77 +1,57 @@ -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::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 crate::{ + database::{cache, redis_conn, redis_key, RedisConnError}, + federation::acct::Acct, + misc::note::elaborate, + model::entity::note, + service::{ + antenna, + antenna::check_hit::{check_hit_antenna, AntennaCheckError}, + stream, + }, + 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 { - #[error("Database error: {0}")] + #[doc = "database error"] + #[error(transparent)] Db(#[from] DbErr), - #[error("Cache error: {0}")] + #[error("Redis cache operation has failed")] Cache(#[from] cache::Error), - #[error("Redis error: {0}")] + #[error("failed to execute a Redis command")] Redis(#[from] RedisError), - #[error("Redis connection error: {0}")] + #[error("bad Redis connection")] RedisConn(#[from] RedisConnError), - #[error("Invalid ID: {0}")] + #[doc = "provided string is not a valid Firefish ID"] + #[error(transparent)] InvalidId(#[from] InvalidIdError), - #[error("Stream error: {0}")] + #[error("Redis stream operation has failed")] Stream(#[from] stream::Error), - #[error("Failed to check if the note should be added to antenna: {0}")] + #[error("failed to check if the note should be added to antenna")] AntennaCheck(#[from] AntennaCheckError), } // for napi export // https://github.com/napi-rs/napi-rs/issues/2060 -type Antenna = antenna::Model; type Note = note::Model; -// TODO?: it might be better to store this directly in memory -// (like fetch_meta) instead of Redis as it's used so much -async fn antennas() -> Result, Error> { - const CACHE_KEY: &str = "antennas"; - - if let Some(antennas) = cache::get::>(CACHE_KEY).await? { - Ok(antennas) - } else { - let antennas = antenna::Entity::find().all(db_conn().await?).await?; - cache::set(CACHE_KEY, &antennas, 5 * 60).await?; - Ok(antennas) - } -} - -#[crate::export] +#[macros::export] pub async fn update_antennas_on_new_note( - note: Note, + note: &Note, note_author: &Acct, note_muted_users: &[String], ) -> Result<(), Error> { - let note_cloned = note.clone(); - let note_all_texts = all_texts( - NoteLike { - file_ids: note.file_ids, - user_id: note.user_id, - text: note.text, - cw: note.cw, - renote_id: note.renote_id, - reply_id: note.reply_id, - }, - false, - ) - .await?; + let note_all_texts = elaborate!(note, false).await?; // TODO: do this in parallel - for antenna in antennas().await?.iter() { + for antenna in antenna::cache::get().await?.iter() { if note_muted_users.contains(&antenna.user_id) { continue; } - if check_hit_antenna(antenna, ¬e_cloned, ¬e_all_texts, note_author).await? { - add_note_to_antenna(&antenna.id, ¬e_cloned).await?; + if check_hit_antenna(antenna, note, ¬e_all_texts, note_author).await? { + add_note_to_antenna(&antenna.id, note).await?; } } @@ -91,5 +71,7 @@ async fn add_note_to_antenna(antenna_id: &str, note: &Note) -> Result<(), Error> .await?; // for streaming API - Ok(stream::antenna::publish(antenna_id.to_string(), note).await?) + stream::antenna::publish(antenna_id.to_string(), note).await?; + + Ok(()) } diff --git a/packages/backend-rs/src/service/antenna/update.rs b/packages/backend-rs/src/service/antenna/update.rs new file mode 100644 index 0000000000..169e574379 --- /dev/null +++ b/packages/backend-rs/src/service/antenna/update.rs @@ -0,0 +1,7 @@ +//! This module is (currently) used in the TypeScript backend only. + +#[macros::ts_export] +pub async fn update_antenna_cache() -> Result<(), sea_orm::DbErr> { + super::cache::update().await?; + Ok(()) +} diff --git a/packages/backend-rs/src/service/note/watch.rs b/packages/backend-rs/src/service/note/watch.rs index 69e30c0f9b..144f4e2360 100644 --- a/packages/backend-rs/src/service/note/watch.rs +++ b/packages/backend-rs/src/service/note/watch.rs @@ -1,18 +1,18 @@ -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::{database::db_conn, model::entity::note_watching, util::id::gen_id_at}; +use sea_orm::{prelude::*, ActiveValue}; -#[crate::export] +#[macros::export] pub async fn watch_note( watcher_id: &str, note_author_id: &str, 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()), @@ -24,7 +24,7 @@ pub async fn watch_note( Ok(()) } -#[crate::export] +#[macros::export] pub async fn unwatch_note(watcher_id: &str, note_id: &str) -> Result<(), DbErr> { let db = db_conn().await?; diff --git a/packages/backend-rs/src/service/push_notification.rs b/packages/backend-rs/src/service/push_notification.rs index 09edb4fff9..29de12fca5 100644 --- a/packages/backend-rs/src/service/push_notification.rs +++ b/packages/backend-rs/src/service/push_notification.rs @@ -1,26 +1,25 @@ -use crate::database::db_conn; -use crate::misc::get_note_summary::{get_note_summary, NoteLike}; -use crate::misc::meta::fetch_meta; -use crate::model::entity::sw_subscription; -use crate::util::http_client; +use crate::{ + config::local_server_info, database::db_conn, misc::note::summarize, + model::entity::sw_subscription, util::http_client, +}; use once_cell::sync::OnceCell; use sea_orm::prelude::*; -use web_push::{ - ContentEncoding, IsahcWebPushClient, SubscriptionInfo, SubscriptionKeys, VapidSignatureBuilder, - WebPushClient, WebPushError, WebPushMessageBuilder, -}; +use serde::Deserialize; +use web_push::*; #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("Database error: {0}")] + #[doc = "database error"] + #[error(transparent)] Db(#[from] DbErr), - #[error("Web Push error: {0}")] + #[error("web push has failed")] WebPush(#[from] WebPushError), - #[error("Failed to (de)serialize an object: {0}")] + #[error("failed to (de)serialize an object")] Serialize(#[from] serde_json::Error), - #[error("Invalid content: {0}")] + #[doc = "provided content is invalid"] + #[error("invalid content ({0})")] InvalidContent(String), - #[error("HTTP client aquisition error: {0}")] + #[error("failed to acquire an HTTP client")] HttpClient(#[from] http_client::Error), } @@ -32,32 +31,18 @@ fn get_client() -> Result { .cloned()?) } -#[derive(strum::Display, PartialEq)] -#[crate::export(string_enum = "camelCase")] +#[macros::export] pub enum PushNotificationKind { - #[strum(serialize = "notification")] Generic, - #[strum(serialize = "unreadMessagingMessage")] Chat, - #[strum(serialize = "readAllMessagingMessages")] ReadAllChats, - #[strum(serialize = "readAllMessagingMessagesOfARoom")] ReadAllChatsInTheRoom, - #[strum(serialize = "readNotifications")] ReadNotifications, - #[strum(serialize = "readAllNotifications")] ReadAllNotifications, Mastodon, } -fn compact_content( - kind: &PushNotificationKind, - mut content: serde_json::Value, -) -> Result { - if kind != &PushNotificationKind::Generic { - return Ok(content); - } - +fn compact_content(mut content: serde_json::Value) -> Result { if !content.is_object() { return Err(Error::InvalidContent("not a JSON object".to_string())); } @@ -87,8 +72,18 @@ fn compact_content( )); } - let note_like: NoteLike = serde_json::from_value(note.clone())?; - let text = get_note_summary(note_like); + // TODO: get rid of this struct + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct PartialNote { + file_ids: Vec, + text: Option, + cw: Option, + has_poll: bool, + } + + let note_like: PartialNote = serde_json::from_value(note.clone())?; + let text = summarize!(note_like); let note_object = note.as_object_mut().unwrap(); @@ -134,13 +129,13 @@ async fn handle_web_push_failure( Ok(()) } -#[crate::export] +#[macros::export] pub async fn send_push_notification( receiver_user_id: &str, 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() { @@ -159,24 +154,40 @@ pub async fn send_push_notification( .all(db) .await?; + let use_mastodon_api = matches!(kind, PushNotificationKind::Mastodon); + // TODO: refactoring - let payload = if kind == PushNotificationKind::Mastodon { + let payload = if use_mastodon_api { // Leave the `content` as it is serde_json::to_string(content)? } else { // Format the `content` passed from the TypeScript backend // for Firefish push notifications + let label = match kind { + PushNotificationKind::Generic => "notification", + PushNotificationKind::Chat => "unreadMessagingMessage", + PushNotificationKind::ReadAllChats => "readAllMessagingMessages", + PushNotificationKind::ReadAllChatsInTheRoom => "readAllMessagingMessagesOfARoom", + PushNotificationKind::ReadNotifications => "readNotifications", + PushNotificationKind::ReadAllNotifications => "readAllNotifications", + // unreachable + _ => "unknown", + }; format!( "{{\"type\":\"{}\",\"userId\":\"{}\",\"dateTime\":{},\"body\":{}}}", - kind, + label, receiver_user_id, chrono::Utc::now().timestamp_millis(), - serde_json::to_string(&compact_content(&kind, content.clone())?)? + match kind { + PushNotificationKind::Generic => + serde_json::to_string(&compact_content(content.to_owned())?)?, + _ => serde_json::to_string(&content)?, + } ) }; tracing::trace!("payload: {}", payload); - let encoding = if kind == PushNotificationKind::Mastodon { + let encoding = if use_mastodon_api { ContentEncoding::AesGcm } else { ContentEncoding::Aes128Gcm @@ -184,13 +195,13 @@ pub async fn send_push_notification( for subscription in subscriptions.iter() { if !subscription.send_read_message - && [ - PushNotificationKind::ReadAllChats, - PushNotificationKind::ReadAllChatsInTheRoom, - PushNotificationKind::ReadAllNotifications, - PushNotificationKind::ReadNotifications, - ] - .contains(&kind) + && matches!( + kind, + PushNotificationKind::ReadAllChats + | PushNotificationKind::ReadAllChatsInTheRoom + | PushNotificationKind::ReadAllNotifications + | PushNotificationKind::ReadNotifications + ) { continue; } diff --git a/packages/backend-rs/src/service/stream.rs b/packages/backend-rs/src/service/stream.rs index dc73499968..412ed3a3a7 100644 --- a/packages/backend-rs/src/service/stream.rs +++ b/packages/backend-rs/src/service/stream.rs @@ -3,101 +3,119 @@ pub mod channel; pub mod chat; pub mod chat_index; pub mod custom_emoji; +pub mod drive; pub mod group_chat; pub mod moderation; +pub mod notes; -use crate::config::CONFIG; -use crate::database::{redis_conn, RedisConnError}; +use crate::{ + config::CONFIG, + database::{redis_conn, RedisConnError}, +}; use redis::{AsyncCommands, RedisError}; -#[derive(strum::Display)] pub enum Stream { - #[strum(serialize = "internal")] Internal, - #[strum(serialize = "broadcast")] CustomEmoji, - #[strum(to_string = "adminStream:{moderator_id}")] - Moderation { moderator_id: String }, - #[strum(to_string = "user:{user_id}")] - User { user_id: String }, - #[strum(to_string = "channelStream:{channel_id}")] - Channel { channel_id: String }, - #[strum(to_string = "noteStream:{note_id}")] - Note { note_id: String }, - #[strum(serialize = "notesStream")] + Moderation { + moderator_id: String, + }, + User { + user_id: String, + }, + Channel { + channel_id: String, + }, + Note { + note_id: String, + }, Notes, - #[strum(to_string = "userListStream:{list_id}")] - UserList { list_id: String }, - #[strum(to_string = "mainStream:{user_id}")] - Main { user_id: String }, - #[strum(to_string = "driveStream:{user_id}")] - Drive { user_id: String }, - #[strum(to_string = "antennaStream:{antenna_id}")] - Antenna { antenna_id: String }, - #[strum(to_string = "messagingStream:{sender_user_id}-{receiver_user_id}")] + UserList { + list_id: String, + }, + Main { + user_id: String, + }, + Drive { + user_id: String, + }, + Antenna { + antenna_id: String, + }, Chat { sender_user_id: String, receiver_user_id: String, }, - #[strum(to_string = "messagingStream:{group_id}")] - GroupChat { group_id: String }, - #[strum(to_string = "messagingIndexStream:{user_id}")] - ChatIndex { user_id: String }, + GroupChat { + group_id: String, + }, + ChatIndex { + user_id: String, + }, +} + +#[macros::export] +pub enum ChatEvent { + Message, + Read, + Deleted, + Typing, } #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("Redis error: {0}")] + #[error("failed to execute a Redis command")] Redis(#[from] RedisError), - #[error("Redis connection error: {0}")] + #[error("bad Redis connection")] RedisConn(#[from] RedisConnError), - #[error("Json (de)serialization error: {0}")] + #[error("failed to (de)serialize object")] Json(#[from] serde_json::Error), - #[error("Value error: {0}")] - Value(String), + #[error("invalid content")] + InvalidContent, } pub async fn publish_to_stream( stream: &Stream, - kind: Option, + kind: Option<&str>, value: Option, ) -> Result<(), Error> { + let channel = match stream { + Stream::Internal => "internal".to_string(), + Stream::CustomEmoji => "broadcast".to_string(), + Stream::Moderation { moderator_id } => format!("adminStream:{moderator_id}"), + Stream::User { user_id } => format!("user:{user_id}"), + Stream::Channel { channel_id } => format!("channelStream:{channel_id}"), + Stream::Note { note_id } => format!("noteStream:{note_id}"), + Stream::Notes => "notesStream".to_string(), + Stream::UserList { list_id } => format!("userListStream:{list_id}"), + Stream::Main { user_id } => format!("mainStream:{user_id}"), + Stream::Drive { user_id } => format!("driveStream:{user_id}"), + Stream::Antenna { antenna_id } => format!("antennaStream:{antenna_id}"), + Stream::Chat { + sender_user_id, + receiver_user_id, + } => format!("messagingStream:{sender_user_id}-{receiver_user_id}"), + Stream::GroupChat { group_id } => format!("messagingStream:{group_id}"), + Stream::ChatIndex { user_id } => format!("messagingIndexStream:{user_id}"), + }; + let message = if let Some(kind) = kind { format!( "{{\"type\":\"{}\",\"body\":{}}}", kind, - value.unwrap_or("null".to_string()), + value.unwrap_or_else(|| "null".to_string()), ) } else { - value.ok_or(Error::Value("Invalid streaming message".to_string()))? + value.ok_or(Error::InvalidContent)? }; redis_conn() .await? .publish( &CONFIG.host, - format!("{{\"channel\":\"{}\",\"message\":{}}}", stream, message), + format!("{{\"channel\":\"{}\",\"message\":{}}}", channel, message), ) .await?; Ok(()) } - -#[cfg(test)] -mod unit_test { - use super::Stream; - use pretty_assertions::assert_eq; - - #[test] - fn channel_to_string() { - assert_eq!(Stream::Internal.to_string(), "internal"); - assert_eq!(Stream::CustomEmoji.to_string(), "broadcast"); - assert_eq!( - Stream::Moderation { - moderator_id: "9tb42br63g5apjcq".to_string() - } - .to_string(), - "adminStream:9tb42br63g5apjcq" - ); - } -} diff --git a/packages/backend-rs/src/service/stream/antenna.rs b/packages/backend-rs/src/service/stream/antenna.rs index 3058d9f04c..ddd3dd711a 100644 --- a/packages/backend-rs/src/service/stream/antenna.rs +++ b/packages/backend-rs/src/service/stream/antenna.rs @@ -1,10 +1,12 @@ -use crate::model::entity::note; -use crate::service::stream::{publish_to_stream, Error, Stream}; +use crate::{ + model::entity::note, + service::stream::{publish_to_stream, Error, Stream}, +}; pub async fn publish(antenna_id: String, note: ¬e::Model) -> Result<(), Error> { publish_to_stream( &Stream::Antenna { antenna_id }, - Some("note".to_string()), + Some("note"), Some(serde_json::to_string(note)?), ) .await diff --git a/packages/backend-rs/src/service/stream/channel.rs b/packages/backend-rs/src/service/stream/channel.rs index 9f5cf3802a..b1bb865fa1 100644 --- a/packages/backend-rs/src/service/stream/channel.rs +++ b/packages/backend-rs/src/service/stream/channel.rs @@ -1,10 +1,10 @@ use crate::service::stream::{publish_to_stream, Error, Stream}; -#[crate::export(js_name = "publishToChannelStream")] +#[macros::export(js_name = "publishToChannelStream")] pub async fn publish(channel_id: String, user_id: String) -> Result<(), Error> { publish_to_stream( &Stream::Channel { channel_id }, - Some("typing".to_string()), + Some("typing"), Some(format!("\"{}\"", user_id)), ) .await diff --git a/packages/backend-rs/src/service/stream/chat.rs b/packages/backend-rs/src/service/stream/chat.rs index 84280c319c..88532c7e86 100644 --- a/packages/backend-rs/src/service/stream/chat.rs +++ b/packages/backend-rs/src/service/stream/chat.rs @@ -1,34 +1,28 @@ -use crate::service::stream::{publish_to_stream, Error, Stream}; - -#[derive(strum::Display)] -#[crate::export(string_enum = "camelCase")] -pub enum ChatEvent { - #[strum(serialize = "message")] - Message, - #[strum(serialize = "read")] - Read, - #[strum(serialize = "deleted")] - Deleted, - #[strum(serialize = "typing")] - Typing, -} +use crate::service::stream::{publish_to_stream, ChatEvent, Error, Stream}; // We want to merge `kind` and `object` into a single enum // https://github.com/napi-rs/napi-rs/issues/2036 -#[crate::export(js_name = "publishToChatStream")] +#[macros::export(js_name = "publishToChatStream")] pub async fn publish( sender_user_id: String, receiver_user_id: String, kind: ChatEvent, object: &serde_json::Value, ) -> Result<(), Error> { + let kind = match kind { + ChatEvent::Message => "message", + ChatEvent::Read => "read", + ChatEvent::Deleted => "deleted", + ChatEvent::Typing => "typing", + }; + publish_to_stream( &Stream::Chat { sender_user_id, receiver_user_id, }, - Some(kind.to_string()), + Some(kind), Some(serde_json::to_string(object)?), ) .await diff --git a/packages/backend-rs/src/service/stream/chat_index.rs b/packages/backend-rs/src/service/stream/chat_index.rs index 6619c5589c..25b2913f2c 100644 --- a/packages/backend-rs/src/service/stream/chat_index.rs +++ b/packages/backend-rs/src/service/stream/chat_index.rs @@ -1,26 +1,28 @@ use crate::service::stream::{publish_to_stream, Error, Stream}; -#[derive(strum::Display)] -#[crate::export(string_enum = "camelCase")] +#[macros::export] pub enum ChatIndexEvent { - #[strum(serialize = "message")] Message, - #[strum(serialize = "read")] Read, } // We want to merge `kind` and `object` into a single enum // https://github.com/napi-rs/napi-rs/issues/2036 -#[crate::export(js_name = "publishToChatIndexStream")] +#[macros::export(js_name = "publishToChatIndexStream")] pub async fn publish( user_id: String, kind: ChatIndexEvent, object: &serde_json::Value, ) -> Result<(), Error> { + let kind = match kind { + ChatIndexEvent::Message => "message", + ChatIndexEvent::Read => "read", + }; + publish_to_stream( &Stream::ChatIndex { user_id }, - Some(kind.to_string()), + Some(kind), Some(serde_json::to_string(object)?), ) .await diff --git a/packages/backend-rs/src/service/stream/custom_emoji.rs b/packages/backend-rs/src/service/stream/custom_emoji.rs index 2cd67f5169..890d550d6a 100644 --- a/packages/backend-rs/src/service/stream/custom_emoji.rs +++ b/packages/backend-rs/src/service/stream/custom_emoji.rs @@ -1,10 +1,10 @@ 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)] +#[macros::export(object)] pub struct PackedEmoji { pub id: String, pub aliases: Vec, @@ -17,11 +17,11 @@ pub struct PackedEmoji { pub height: Option, } -#[crate::export(js_name = "publishToBroadcastStream")] +#[macros::export(js_name = "publishToBroadcastStream")] pub async fn publish(emoji: &PackedEmoji) -> Result<(), Error> { publish_to_stream( &Stream::CustomEmoji, - Some("emojiAdded".to_string()), + Some("emojiAdded"), Some(format!("{{\"emoji\":{}}}", serde_json::to_string(emoji)?)), ) .await diff --git a/packages/backend-rs/src/service/stream/drive.rs b/packages/backend-rs/src/service/stream/drive.rs new file mode 100644 index 0000000000..21ddf1f4b2 --- /dev/null +++ b/packages/backend-rs/src/service/stream/drive.rs @@ -0,0 +1,58 @@ +use crate::service::stream::{publish_to_stream, Error, Stream}; + +#[macros::export] +pub enum DriveFileEvent { + Create, + Update, + Delete, +} + +#[macros::export] +pub enum DriveFolderEvent { + Create, + Update, + Delete, +} + +// We want to merge `kind` and `object` into a single enum and merge the 2 functions +// https://github.com/napi-rs/napi-rs/issues/2036 + +#[macros::export(js_name = "publishToDriveFileStream")] +pub async fn publish_file( + user_id: String, + kind: DriveFileEvent, + object: &serde_json::Value, // file (create, update) or file id (delete) +) -> Result<(), Error> { + let kind = match kind { + DriveFileEvent::Create => "fileCreated", + DriveFileEvent::Update => "fileUpdated", + DriveFileEvent::Delete => "fileDeleted", + }; + + publish_to_stream( + &Stream::Drive { user_id }, + Some(kind), + Some(serde_json::to_string(object)?), + ) + .await +} + +#[macros::export(js_name = "publishToDriveFolderStream")] +pub async fn publish_folder( + user_id: String, + kind: DriveFolderEvent, + object: &serde_json::Value, // folder (create, update) or folder id (delete) +) -> Result<(), Error> { + let kind = match kind { + DriveFolderEvent::Create => "folderCreated", + DriveFolderEvent::Update => "folderUpdated", + DriveFolderEvent::Delete => "folderDeleted", + }; + + publish_to_stream( + &Stream::Drive { user_id }, + Some(kind), + Some(serde_json::to_string(object)?), + ) + .await +} diff --git a/packages/backend-rs/src/service/stream/group_chat.rs b/packages/backend-rs/src/service/stream/group_chat.rs index 20c04c6fa2..005a523f1c 100644 --- a/packages/backend-rs/src/service/stream/group_chat.rs +++ b/packages/backend-rs/src/service/stream/group_chat.rs @@ -1,17 +1,24 @@ -use crate::service::stream::{chat::ChatEvent, publish_to_stream, Error, Stream}; +use crate::service::stream::{publish_to_stream, ChatEvent, Error, Stream}; // We want to merge `kind` and `object` into a single enum // https://github.com/napi-rs/napi-rs/issues/2036 -#[crate::export(js_name = "publishToGroupChatStream")] +#[macros::export(js_name = "publishToGroupChatStream")] pub async fn publish( group_id: String, kind: ChatEvent, object: &serde_json::Value, ) -> Result<(), Error> { + let kind = match kind { + ChatEvent::Message => "message", + ChatEvent::Read => "read", + ChatEvent::Deleted => "deleted", + ChatEvent::Typing => "typing", + }; + publish_to_stream( &Stream::GroupChat { group_id }, - Some(kind.to_string()), + Some(kind), Some(serde_json::to_string(object)?), ) .await diff --git a/packages/backend-rs/src/service/stream/moderation.rs b/packages/backend-rs/src/service/stream/moderation.rs index ef604ed6bf..3ac261ff50 100644 --- a/packages/backend-rs/src/service/stream/moderation.rs +++ b/packages/backend-rs/src/service/stream/moderation.rs @@ -1,9 +1,9 @@ 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)] +#[macros::export(object)] pub struct AbuseUserReportLike { pub id: String, pub target_user_id: String, @@ -11,11 +11,11 @@ pub struct AbuseUserReportLike { pub comment: String, } -#[crate::export(js_name = "publishToModerationStream")] +#[macros::export(js_name = "publishToModerationStream")] pub async fn publish(moderator_id: String, report: &AbuseUserReportLike) -> Result<(), Error> { publish_to_stream( &Stream::Moderation { moderator_id }, - Some("newAbuseUserReport".to_string()), + Some("newAbuseUserReport"), Some(serde_json::to_string(report)?), ) .await diff --git a/packages/backend-rs/src/service/stream/notes.rs b/packages/backend-rs/src/service/stream/notes.rs new file mode 100644 index 0000000000..8832bc03e3 --- /dev/null +++ b/packages/backend-rs/src/service/stream/notes.rs @@ -0,0 +1,13 @@ +use crate::{ + model::entity::note, + service::stream::{publish_to_stream, Error, Stream}, +}; + +// for napi export +// https://github.com/napi-rs/napi-rs/issues/2060 +type Note = note::Model; + +#[macros::export(js_name = "publishToNotesStream")] +pub async fn publish(note: &Note) -> Result<(), Error> { + publish_to_stream(&Stream::Notes, None, Some(serde_json::to_string(note)?)).await +} diff --git a/packages/backend-rs/src/util/error_chain.rs b/packages/backend-rs/src/util/error_chain.rs new file mode 100644 index 0000000000..4d1341dc05 --- /dev/null +++ b/packages/backend-rs/src/util/error_chain.rs @@ -0,0 +1,82 @@ +//! Error formatter used until backtracing methods are standarized + +use std::error::Error; + +/// Prettifies [`Error`] message (mainly for napi export) +pub fn format_error(mut error: &dyn Error) -> String { + let mut to_return = String::new(); + + to_return.push_str(&format!( + concat!(" raw: {0:?}", "\n", " message: {0}"), + error, + )); + + while let Some(source) = error.source() { + to_return.push('\n'); + to_return.push_str(&format!("caused by: {}", source)); + error = source; + } + + to_return +} + +#[cfg(test)] +mod unit_test { + use pretty_assertions::assert_eq; + + #[test] + fn format_error() { + #[derive(thiserror::Error, Debug)] + #[error("inner error 1")] + struct InnerError1; + + #[derive(thiserror::Error, Debug)] + #[error("unexpected string '{0}'")] + struct InnerError2(String); + + #[derive(thiserror::Error, Debug)] + enum ErrorVariants { + #[error("error 1 occured")] + Error1(#[from] InnerError1), + #[error("error 2 occured")] + Error2(#[from] InnerError2), + } + + fn causes_inner_error_1() -> Result<(), InnerError1> { + Err(InnerError1) + } + fn causes_inner_error_2() -> Result<(), InnerError2> { + Err(InnerError2("foo".to_string())) + } + + fn causes_error_1() -> Result<(), ErrorVariants> { + causes_inner_error_1()?; + Ok(()) + } + fn causes_error_2() -> Result<(), ErrorVariants> { + causes_inner_error_2()?; + Ok(()) + } + + let error_1 = causes_error_1().unwrap_err(); + let error_2 = causes_error_2().unwrap_err(); + + let error_message_1 = super::format_error(&error_1); + let error_message_2 = super::format_error(&error_2); + + // We can't assume consistency in `Debug` output, so the output texts + // may need to be updated in the future, and we shouldn't make this kind + // of assumption in the actual program. + let expected_message_1 = " + raw: Error1(InnerError1) + message: error 1 occured +caused by: inner error 1"; + let expected_message_2 = r#" + raw: Error2(InnerError2("foo")) + message: error 2 occured +caused by: unexpected string 'foo'"#; + + assert_eq!(error_message_1, expected_message_1[1..]); + assert_eq!(error_message_2, expected_message_2[1..]); + } +} diff --git a/packages/backend-rs/src/util/http_client.rs b/packages/backend-rs/src/util/http_client.rs index 3e861966d2..3b56d57c6e 100644 --- a/packages/backend-rs/src/util/http_client.rs +++ b/packages/backend-rs/src/util/http_client.rs @@ -7,9 +7,9 @@ use std::time::Duration; #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("Isahc error: {0}")] + #[error("HTTP request failed")] Isahc(#[from] isahc::Error), - #[error("Url parse error: {0}")] + #[error("invalid URL")] UrlParse(#[from] isahc::http::uri::InvalidUri), } @@ -20,13 +20,13 @@ static CLIENT: OnceCell = OnceCell::new(); /// # Example /// ```no_run /// # use backend_rs::util::http_client::client; -/// use isahc::ReadResponseExt; +/// use isahc::AsyncReadResponseExt; /// -/// # fn f() -> Result<(), Box> { -/// let mut response = client()?.get("https://example.com/")?; +/// # async fn f() -> Result<(), Box> { +/// let mut response = client()?.get_async("https://example.com/").await?; /// /// if response.status().is_success() { -/// println!("{}", response.text()?); +/// println!("{}", response.text().await?); /// } /// # Ok(()) /// # } diff --git a/packages/backend-rs/src/util/id.rs b/packages/backend-rs/src/util/id.rs index 39e4c34045..c7f5f294dd 100644 --- a/packages/backend-rs/src/util/id.rs +++ b/packages/backend-rs/src/util/id.rs @@ -27,11 +27,11 @@ fn init_id_generator(length: u8, fingerprint: &str) { /// It automatically calls [init_id_generator], if the generator has not been initialized. fn create_id(datetime: &NaiveDateTime) -> String { if GENERATOR.get().is_none() { - let length = match &CONFIG.cuid { - Some(cuid) => cmp::min(cmp::max(cuid.length.unwrap_or(16), 16), 24), + let length = match CONFIG.cuid.as_ref() { + Some(cuid) => cuid.length.unwrap_or(16).clamp(16, 24), None => 16, }; - let fingerprint = match &CONFIG.cuid { + let fingerprint = match CONFIG.cuid.as_ref() { Some(cuid) => cuid.fingerprint.as_deref().unwrap_or_default(), None => "", }; @@ -46,12 +46,13 @@ fn create_id(datetime: &NaiveDateTime) -> String { } #[derive(thiserror::Error, Debug)] -#[error("Invalid ID: {id}")] +#[doc = "Error type to indicate invalid Firefish ID"] +#[error("'{id}' is not a valid Firefish ID")] pub struct InvalidIdError { id: String, } -#[crate::export] +#[macros::export] pub fn get_timestamp(id: &str) -> Result { let n: Option = BASE36.decode_var_len(&id[0..8]); if let Some(n) = n { @@ -67,13 +68,13 @@ pub fn get_timestamp(id: &str) -> Result { /// in the same millisecond to reach 50% chance of collision. /// /// Ref: -#[crate::export] +#[macros::export] pub fn gen_id() -> String { create_id(&Utc::now().naive_utc()) } /// Generate an ID using a specific datetime -#[crate::export] +#[macros::export] pub fn gen_id_at(date: DateTime) -> String { create_id(&date.naive_utc()) } @@ -86,7 +87,7 @@ mod unit_test { use std::thread; #[test] - fn can_create_and_decode_id() { + fn create_and_decode_id() { let now = Utc::now(); assert_eq!(gen_id().len(), 16); assert_ne!(gen_id_at(now), gen_id_at(now)); @@ -108,7 +109,7 @@ mod unit_test { } #[test] - fn fixture_id_timestamp_check() { + fn get_timestamp_from_id() { assert_eq!(get_timestamp("9e112pilk1").unwrap(), 1682499501741); assert_eq!(get_timestamp("9e183znmxa").unwrap(), 1682511318850); assert_eq!(get_timestamp("9e9srqr79p").unwrap(), 1683029748787); diff --git a/packages/backend-rs/src/util/mod.rs b/packages/backend-rs/src/util/mod.rs index 761527c4ae..3df33831e8 100644 --- a/packages/backend-rs/src/util/mod.rs +++ b/packages/backend-rs/src/util/mod.rs @@ -1,5 +1,6 @@ //! Basic utilities such as ID generator and HTTP client +pub mod error_chain; pub mod http_client; pub mod id; pub mod random; diff --git a/packages/backend-rs/src/util/random.rs b/packages/backend-rs/src/util/random.rs index c757330f80..0c35657a47 100644 --- a/packages/backend-rs/src/util/random.rs +++ b/packages/backend-rs/src/util/random.rs @@ -3,7 +3,7 @@ use rand::{distributions::Alphanumeric, thread_rng, Rng}; /// Generates a random string based on [thread_rng] and [Alphanumeric]. -#[crate::export] +#[macros::export] pub fn generate_secure_random_string(length: u16) -> String { thread_rng() .sample_iter(Alphanumeric) @@ -12,20 +12,19 @@ pub fn generate_secure_random_string(length: u16) -> String { .collect() } -#[crate::export] +#[macros::export] pub fn generate_user_token() -> String { generate_secure_random_string(16) } #[cfg(test)] mod unit_test { + use super::generate_secure_random_string; use pretty_assertions::{assert_eq, assert_ne}; use std::thread; - use super::generate_secure_random_string; - #[test] - fn can_generate_unique_strings() { + fn generate_unique_strings() { assert_eq!(generate_secure_random_string(16).len(), 16); assert_ne!( generate_secure_random_string(16), diff --git a/packages/backend/.swcrc b/packages/backend/.swcrc deleted file mode 100644 index 08aca7562a..0000000000 --- a/packages/backend/.swcrc +++ /dev/null @@ -1,29 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/swcrc", - "jsc": { - "parser": { - "syntax": "typescript", - "dynamicImport": true, - "decorators": true - }, - "transform": { - "legacyDecorator": true, - "decoratorMetadata": true - }, - "experimental": { - "keepImportAssertions": true, - "emitAssertForImportAttributes": true - }, - "baseUrl": "src", - "paths": { - "@/*": ["*"] - }, - "target": "es2022" - }, - "minify": false, - "module": { - "type": "es6", - "strict": true, - "resolveFully": true - } -} diff --git a/packages/backend/assets/redoc.html b/packages/backend/assets/redoc.html index 403af4bbfa..834f645607 100644 --- a/packages/backend/assets/redoc.html +++ b/packages/backend/assets/redoc.html @@ -1,23 +1,19 @@ + + Firefish API - - - - - - + + + - - + diff --git a/packages/backend/package.json b/packages/backend/package.json index 665df61aa4..a46afb52f4 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -10,38 +10,35 @@ "migration:revert": "typeorm migration:revert --dataSource ./built/ormconfig.js", "migration:new": "pnpm node ./scripts/create-migration.mjs", "check:connect": "node ./check_connect.js", - "build": "pnpm swc src --out-dir built --source-maps false --copy-files --strip-leading-paths", - "build:debug": "pnpm swc src --out-dir built --source-maps true --copy-files --strip-leading-paths", - "watch": "pnpm swc src --out-dir built --source-maps true --copy-files --strip-leading-paths --watch", - "lint": "pnpm biome check --apply **/*.ts ; pnpm run format", + "build": "pnpm tsc --project tsconfig.json ; pnpm tsc-alias --project tsconfig.json", + "build:debug": "pnpm tsc --sourceMap --project tsconfig.json ; pnpm tsc-alias --project tsconfig.json", + "watch": "pnpm tsc --project tsconfig.json --watch ; pnpm tsc-alias --project tsconfig.json --watch", + "lint": "pnpm biome check --write **/*.ts", "mocha": "cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha", "test": "pnpm run mocha", "format": "pnpm biome format * --write" }, - "optionalDependencies": { - "@swc/core-android-arm64": "1.3.11" - }, "dependencies": { - "@bull-board/api": "5.19.2", - "@bull-board/koa": "5.19.2", - "@bull-board/ui": "5.19.2", + "@bull-board/api": "5.20.5", + "@bull-board/koa": "5.20.5", + "@bull-board/ui": "5.20.5", "@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.14.0", + "@redocly/openapi-core": "1.17.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.1630.0", + "aws-sdk": "2.1653.0", "axios": "1.7.2", "backend-rs": "workspace:*", "blurhash": "2.0.5", - "bull": "4.12.9", - "cacheable-lookup": "TheEssem/cacheable-lookup", + "bull": "4.15.0", + "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,17 +54,17 @@ "firefish-js": "workspace:*", "fluent-ffmpeg": "2.1.3", "form-data": "4.0.0", - "got": "14.3.0", + "got": "14.4.1", "gunzip-maybe": "1.4.2", "hpagent": "1.2.0", "ioredis": "5.4.1", - "ip-cidr": "4.0.0", + "ip-cidr": "4.0.1", "is-svg": "5.0.1", "jsdom": "24.1.0", "json5": "2.2.3", "jsonld": "8.3.2", "jsrsasign": "11.1.0", - "katex": "0.16.10", + "katex": "0.16.11", "koa": "2.15.3", "koa-body": "6.0.1", "koa-bodyparser": "4.4.1", @@ -77,7 +74,6 @@ "koa-mount": "4.0.0", "koa-remove-trailing-slashes": "2.0.3", "koa-send": "5.0.1", - "koa-slow": "2.1.0", "megalodon": "workspace:*", "mfm-js": "0.24.0", "mime-types": "2.1.35", @@ -85,18 +81,18 @@ "multer": "1.4.5-lts.1", "nested-property": "4.0.0", "node-fetch": "3.3.2", - "nodemailer": "6.9.13", + "nodemailer": "6.9.14", "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", "punycode": "2.3.1", "pureimage": "0.4.13", "qrcode": "1.5.3", - "qs": "6.12.1", + "qs": "6.12.2", "random-seed": "0.3.0", "ratelimiter": "3.4.1", "redis-semaphore": "5.6.0", @@ -116,20 +112,18 @@ "tmp": "0.2.3", "typeorm": "0.3.20", "ulid": "2.3.0", - "uuid": "9.0.1", + "uuid": "10.0.0", "websocket": "1.0.35", "xev": "3.0.2" }, "devDependencies": { - "@swc/cli": "0.3.12", - "@swc/core": "1.5.22", "@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", @@ -142,8 +136,8 @@ "@types/koa__cors": "5.0.0", "@types/koa__multer": "2.0.7", "@types/koa__router": "12.0.4", - "@types/mocha": "10.0.6", - "@types/node": "20.12.13", + "@types/mocha": "10.0.7", + "@types/node": "20.14.9", "@types/node-fetch": "2.6.11", "@types/nodemailer": "6.4.15", "@types/oauth": "0.9.5", @@ -163,22 +157,20 @@ "@types/syslog-pro": "1.0.3", "@types/tinycolor2": "1.4.6", "@types/tmp": "0.2.6", - "@types/uuid": "9.0.8", - "@types/web-push": "3.6.3", + "@types/uuid": "10.0.0", "@types/websocket": "1.0.10", "@types/ws": "8.5.10", "cross-env": "7.0.3", - "eslint": "9.3.0", - "mocha": "10.4.0", + "mocha": "10.6.0", "pug": "3.0.3", "strict-event-emitter-types": "2.0.0", - "swc-loader": "0.2.6", "ts-loader": "9.5.1", "ts-node": "10.9.2", + "tsc-alias": "1.8.10", "tsconfig-paths": "4.2.0", - "type-fest": "4.18.3", - "typescript": "5.4.5", - "webpack": "5.91.0", - "ws": "8.17.0" + "type-fest": "4.21.0", + "typescript": "5.5.3", + "webpack": "5.92.1", + "ws": "8.17.1" } } diff --git a/packages/backend/src/@types/backend-rs.d.ts b/packages/backend/src/@types/backend-rs.d.ts deleted file mode 100644 index c1a6cc71be..0000000000 --- a/packages/backend/src/@types/backend-rs.d.ts +++ /dev/null @@ -1 +0,0 @@ -type DateTimeWithTimeZone = Date; diff --git a/packages/backend/src/@types/koa-slow.d.ts b/packages/backend/src/@types/koa-slow.d.ts deleted file mode 100644 index e24be51e2a..0000000000 --- a/packages/backend/src/@types/koa-slow.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -declare module "koa-slow" { - import type { Middleware } from "koa"; - - interface ISlowOptions { - url?: RegExp; - delay?: number; - } - - function slow(options?: ISlowOptions): Middleware; - - namespace slow {} // Hack - - export = slow; -} diff --git a/packages/backend/src/boot/index.ts b/packages/backend/src/boot/index.ts index 3e542a6e36..1ebe0f4642 100644 --- a/packages/backend/src/boot/index.ts +++ b/packages/backend/src/boot/index.ts @@ -3,7 +3,6 @@ import chalk from "chalk"; import Xev from "xev"; import Logger from "@/services/logger.js"; -import { envOption } from "@/config.js"; import { inspect } from "node:util"; // for typeorm @@ -12,6 +11,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 +21,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})` @@ -27,14 +30,14 @@ export default async function () { const type = cluster.isPrimary ? "(master)" : "(worker)"; process.title = `Firefish ${mode} ${type}`; - if (cluster.isPrimary || envOption.disableClustering) { + if (cluster.isPrimary) { await masterMain(); if (cluster.isPrimary) { ev.mount(); } } - if (cluster.isWorker || envOption.disableClustering) { + if (cluster.isWorker) { await workerMain(); } diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index e065492eae..dbf51c08c5 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -4,14 +4,14 @@ import semver from "semver"; import Logger from "@/services/logger.js"; import { - fetchMeta, greet, - initializeRustLogger, removeOldAttestationChallenges, showServerInfo, + updateMetaCache, + updateNodeinfoCache, type Config, } from "backend-rs"; -import { config, envOption } from "@/config.js"; +import { config } from "@/config.js"; import { db, initDb } from "@/db/postgre.js"; import { inspect } from "node:util"; @@ -24,7 +24,6 @@ const bootLogger = logger.createSubLogger("boot", "magenta", false); export async function masterMain() { // initialize app try { - initializeRustLogger(); greet(); showEnvironment(); showServerInfo(); @@ -41,9 +40,7 @@ export async function masterMain() { bootLogger.info("Firefish initialized"); - if (!envOption.disableClustering) { - await spawnWorkers(config.clusterLimits); - } + await spawnWorkers(config.clusterLimits); bootLogger.info( `Now listening on port ${config.port} on ${config.url}`, @@ -51,14 +48,14 @@ export async function masterMain() { true, ); - if (!envOption.noDaemons) { - 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); - // Remove old attestation challenges - setInterval(() => removeOldAttestationChallenges(), 1000 * 60 * 30); - } + 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(() => updateMetaCache(), 1000 * 60 * 5); + // Update nodeinfo cache every hour + setInterval(() => updateNodeinfoCache(), 1000 * 60 * 60); + // Remove old attestation challenges + setInterval(() => removeOldAttestationChallenges(), 1000 * 60 * 30); } function showEnvironment(): void { diff --git a/packages/backend/src/boot/worker.ts b/packages/backend/src/boot/worker.ts index 0acdcd97c6..3b641f84a0 100644 --- a/packages/backend/src/boot/worker.ts +++ b/packages/backend/src/boot/worker.ts @@ -25,6 +25,6 @@ export async function workerMain() { if (cluster.isWorker) { // Send a 'ready' message to parent process - process.send!("ready"); + process.send?.("ready"); } } diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index c91294b611..5adab590a2 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -1,4 +1,3 @@ -import { loadConfig, loadEnv } from "backend-rs"; +import { loadConfig } from "backend-rs"; export const config = loadConfig(); -export const envOption = loadEnv(); diff --git a/packages/backend/src/daemons/server-stats.ts b/packages/backend/src/daemons/server-stats.ts index 2db09e8ae6..04a1542b11 100644 --- a/packages/backend/src/daemons/server-stats.ts +++ b/packages/backend/src/daemons/server-stats.ts @@ -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() { diff --git a/packages/backend/src/db/postgre.ts b/packages/backend/src/db/postgre.ts index 62bd2f9a1c..d1030f5125 100644 --- a/packages/backend/src/db/postgre.ts +++ b/packages/backend/src/db/postgre.ts @@ -74,7 +74,6 @@ import { Webhook } from "@/models/entities/webhook.js"; import { UserIp } from "@/models/entities/user-ip.js"; import { NoteEdit } from "@/models/entities/note-edit.js"; import { NoteFile } from "@/models/entities/note-file.js"; -import { ScheduledNote } from "@/models/entities/scheduled-note.js"; import { entities as charts } from "@/services/chart/entities.js"; import { dbLogger } from "./logger.js"; @@ -183,7 +182,6 @@ export const entities = [ UserPending, Webhook, UserIp, - ScheduledNote, ...charts, ]; @@ -219,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"], }); diff --git a/packages/backend/src/mfm/to-html.ts b/packages/backend/src/mfm/to-html.ts index 83a99ae386..fe853ce1ef 100644 --- a/packages/backend/src/mfm/to-html.ts +++ b/packages/backend/src/mfm/to-html.ts @@ -61,7 +61,7 @@ export function toHtml( }, fn(node) { - const el = doc.createElement("i"); + const el = doc.createElement("span"); appendChildren(node.children, el); return el; }, diff --git a/packages/backend/src/migration/1716804636187-refactor-scheduled-posts.ts b/packages/backend/src/migration/1716804636187-refactor-scheduled-posts.ts new file mode 100644 index 0000000000..7beaadace2 --- /dev/null +++ b/packages/backend/src/migration/1716804636187-refactor-scheduled-posts.ts @@ -0,0 +1,81 @@ +import type { MigrationInterface, QueryRunner } from "typeorm"; + +export class RefactorScheduledPosts1716804636187 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "note" ADD COLUMN "scheduledAt" timestamp with time zone`, + ); + await queryRunner.query( + `CREATE TEMP TABLE "tmp_scheduled_note" (LIKE "note")`, + ); + await queryRunner.query( + `INSERT INTO "tmp_scheduled_note" (SELECT * FROM "note" WHERE "note"."id" IN (SELECT "noteId" FROM "scheduled_note"))`, + ); + await queryRunner.query( + `UPDATE "tmp_scheduled_note" SET "scheduledAt" = "scheduled_note"."scheduledAt" FROM "scheduled_note" WHERE "tmp_scheduled_note"."id" = "scheduled_note"."noteId"`, + ); + await queryRunner.query( + `DELETE FROM "note" WHERE "note"."id" IN (SELECT "noteId" FROM "scheduled_note")`, + ); + await queryRunner.query( + `INSERT INTO "note" SELECT * FROM "tmp_scheduled_note"`, + ); + await queryRunner.query(`DROP TABLE "scheduled_note"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "scheduled_note" ( + "id" character varying(32) NOT NULL PRIMARY KEY, + "noteId" character varying(32) NOT NULL, + "userId" character varying(32) NOT NULL, + "scheduledAt" TIMESTAMP WITH TIME ZONE NOT NULL + )`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "scheduled_note"."noteId" IS 'The ID of the temporarily created note that corresponds to the schedule.'`, + ); + // temp function to populate "scheduled_note"."id" with random values (as it's unused) + await queryRunner.query("CREATE EXTENSION pgcrypto"); + await queryRunner.query(` + CREATE FUNCTION generate_scheduled_note_id(size int) RETURNS text AS $$ DECLARE + characters text := 'abcdefghijklmnopqrstuvwxyz0123456789'; + bytes bytea := gen_random_bytes(size); + l int := length(characters); + i int := 0; + output text := ''; + BEGIN + WHILE i < size LOOP + output := output || substr(characters, get_byte(bytes, i) % l + 1, 1); + i := i + 1; + END LOOP; + RETURN output; + END; + $$ LANGUAGE plpgsql VOLATILE; + `); + await queryRunner.query( + `INSERT INTO "scheduled_note" ("id", "noteId", "userId", "scheduledAt") (SELECT generate_scheduled_note_id(16), "id", "userId", "scheduledAt" FROM "note" WHERE "note"."scheduledAt" IS NOT NULL)`, + ); + await queryRunner.query("DROP EXTENSION pgcrypto"); + await queryRunner.query(`DROP FUNCTION "generate_scheduled_note_id"`); + await queryRunner.query( + `CREATE INDEX "IDX_noteId_ScheduledNote" ON "scheduled_note" ("noteId")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_userId_ScheduledNote" ON "scheduled_note" ("userId")`, + ); + await queryRunner.query(` + ALTER TABLE "scheduled_note" + ADD FOREIGN KEY ("noteId") REFERENCES "note"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + `); + await queryRunner.query(` + ALTER TABLE "scheduled_note" + ADD FOREIGN KEY ("userId") REFERENCES "user"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + `); + await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "scheduledAt"`); + } +} diff --git a/packages/backend/src/misc/fetch-proxy-account.ts b/packages/backend/src/misc/fetch-proxy-account.ts index 8d015da25d..26fb733781 100644 --- a/packages/backend/src/misc/fetch-proxy-account.ts +++ b/packages/backend/src/misc/fetch-proxy-account.ts @@ -3,7 +3,7 @@ import type { ILocalUser } from "@/models/entities/user.js"; import { Users } from "@/models/index.js"; export async function fetchProxyAccount(): Promise { - const meta = await fetchMeta(true); + const meta = await fetchMeta(); if (meta.proxyAccountId == null) return null; return (await Users.findOneByOrFail({ id: meta.proxyAccountId, diff --git a/packages/backend/src/misc/translate.ts b/packages/backend/src/misc/translate.ts index 3395ce93be..d1bcd5e72e 100644 --- a/packages/backend/src/misc/translate.ts +++ b/packages/backend/src/misc/translate.ts @@ -2,7 +2,7 @@ import fetch from "node-fetch"; import { Converter } from "opencc-js"; import { getAgentByUrl } from "@/misc/fetch.js"; import { fetchMeta } from "backend-rs"; -import type { PostLanguage } from "@/misc/langmap"; +import type { PostLanguage } from "firefish-js"; import * as deepl from "deepl-node"; // DeepL translate and LibreTranslate don't provide @@ -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."); diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts index b5fc632be1..7ff23d5f09 100644 --- a/packages/backend/src/models/entities/drive-file.ts +++ b/packages/backend/src/models/entities/drive-file.ts @@ -224,13 +224,13 @@ export class DriveFile { nullable: true, }) @JoinColumn() - public user: User | null; + public user: Relation; @ManyToOne(() => DriveFolder, { onDelete: "SET NULL", nullable: true, }) @JoinColumn() - public folder: DriveFolder | null; + public folder: Relation; //#endregion Relations } diff --git a/packages/backend/src/models/entities/note.ts b/packages/backend/src/models/entities/note.ts index 1c9d3570a3..97db90a880 100644 --- a/packages/backend/src/models/entities/note.ts +++ b/packages/backend/src/models/entities/note.ts @@ -31,6 +31,11 @@ export class Note { }) public createdAt: Date; + @Column("timestamp with time zone", { + nullable: true, + }) + public scheduledAt: Date | null; + @Index() @Column({ ...id(), diff --git a/packages/backend/src/models/entities/scheduled-note.ts b/packages/backend/src/models/entities/scheduled-note.ts deleted file mode 100644 index 6c0b1296d8..0000000000 --- a/packages/backend/src/models/entities/scheduled-note.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { - Entity, - JoinColumn, - Column, - ManyToOne, - OneToOne, - PrimaryColumn, - Index, - type Relation, -} from "typeorm"; -import { Note } from "./note.js"; -import { id } from "../id.js"; -import { User } from "./user.js"; - -@Entity() -export class ScheduledNote { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column({ - ...id(), - comment: - "The ID of the temporarily created note that corresponds to the schedule.", - }) - public noteId: Note["id"]; - - @Index() - @Column(id()) - public userId: User["id"]; - - @Column("timestamp with time zone") - public scheduledAt: Date; - - //#region Relations - @OneToOne(() => Note, { - onDelete: "CASCADE", - }) - @JoinColumn() - public note: Relation; - - @ManyToOne(() => User, { - onDelete: "CASCADE", - }) - @JoinColumn() - public user: Relation; - //#endregion -} diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts index d6b81ad70e..c578d9d409 100644 --- a/packages/backend/src/models/index.ts +++ b/packages/backend/src/models/index.ts @@ -67,7 +67,6 @@ import { Webhook } from "./entities/webhook.js"; import { UserIp } from "./entities/user-ip.js"; import { NoteFileRepository } from "./repositories/note-file.js"; import { NoteEditRepository } from "./repositories/note-edit.js"; -import { ScheduledNote } from "./entities/scheduled-note.js"; export const Announcements = db.getRepository(Announcement); export const AnnouncementReads = db.getRepository(AnnouncementRead); @@ -136,4 +135,3 @@ export const RegistryItems = db.getRepository(RegistryItem); export const Webhooks = db.getRepository(Webhook); export const Ads = db.getRepository(Ad); export const PasswordResetRequests = db.getRepository(PasswordResetRequest); -export const ScheduledNotes = db.getRepository(ScheduledNote); diff --git a/packages/backend/src/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts index 7e362faf88..062973a741 100644 --- a/packages/backend/src/models/repositories/note.ts +++ b/packages/backend/src/models/repositories/note.ts @@ -11,7 +11,6 @@ import { Polls, Channels, Notes, - ScheduledNotes, } from "../index.js"; import type { Packed } from "@/misc/schema.js"; import { countReactions, decodeReaction, nyaify } from "backend-rs"; @@ -199,19 +198,17 @@ export const NoteRepository = db.getRepository(Note).extend({ host, ); - let scheduledAt: string | undefined; - if (note.visibility === "specified" && note.visibleUserIds.length === 0) { - scheduledAt = ( - await ScheduledNotes.findOneBy({ - noteId: note.id, - }) - )?.scheduledAt?.toISOString(); - } - const reactionEmoji = await populateEmojis(reactionEmojiNames, host); const packed: Packed<"Note"> = await awaitAll({ id: note.id, createdAt: note.createdAt.toISOString(), + // FIXME: note.scheduledAt should be a `Date` + scheduledAt: + note.scheduledAt == null + ? undefined + : typeof note.scheduledAt === "string" + ? note.scheduledAt + : note.scheduledAt?.toISOString(), userId: note.userId, user: Users.pack(note.user ?? note.userId, me, { detail: false, @@ -241,7 +238,6 @@ export const NoteRepository = db.getRepository(Note).extend({ }, }) : undefined, - scheduledAt, reactions: countReactions(note.reactions), reactionEmojis: reactionEmoji, emojis: noteEmoji, diff --git a/packages/backend/src/queue/index.ts b/packages/backend/src/queue/index.ts index a604a1c9fa..f82f8cda3b 100644 --- a/packages/backend/src/queue/index.ts +++ b/packages/backend/src/queue/index.ts @@ -1,7 +1,7 @@ import type httpSignature from "@peertube/http-signature"; import { v4 as uuid } from "uuid"; -import { config, envOption } from "@/config.js"; +import { config } from "@/config.js"; import type { DriveFile } from "@/models/entities/drive-file.js"; import type { IActivity } from "@/remote/activitypub/type.js"; import type { Webhook, webhookEventTypes } from "@/models/entities/webhook.js"; @@ -518,8 +518,6 @@ export function webhookDeliver( } export default function () { - if (envOption.onlyServer) return; - deliverQueue.process(config.deliverJobConcurrency || 128, processDeliver); inboxQueue.process(config.inboxJobConcurrency || 16, processInbox); endedPollNotificationQueue.process(endedPollNotification); diff --git a/packages/backend/src/queue/processors/db/import-muting.ts b/packages/backend/src/queue/processors/db/import-muting.ts index af65837e29..f96aa7d618 100644 --- a/packages/backend/src/queue/processors/db/import-muting.ts +++ b/packages/backend/src/queue/processors/db/import-muting.ts @@ -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, }); diff --git a/packages/backend/src/queue/processors/db/import-user-lists.ts b/packages/backend/src/queue/processors/db/import-user-lists.ts index f4e8523ba8..11f5f66675 100644 --- a/packages/backend/src/queue/processors/db/import-user-lists.ts +++ b/packages/backend/src/queue/processors/db/import-user-lists.ts @@ -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])); diff --git a/packages/backend/src/queue/processors/db/scheduled-note.ts b/packages/backend/src/queue/processors/db/scheduled-note.ts index def37f1306..3d3a711dfd 100644 --- a/packages/backend/src/queue/processors/db/scheduled-note.ts +++ b/packages/backend/src/queue/processors/db/scheduled-note.ts @@ -1,4 +1,4 @@ -import { Users, Notes, ScheduledNotes, DriveFiles } from "@/models/index.js"; +import { Users, Notes, DriveFiles } from "@/models/index.js"; import type { DbUserScheduledNoteData } from "@/queue/types.js"; import { queueLogger } from "../../logger.js"; import type Bull from "bull"; @@ -14,52 +14,82 @@ export async function scheduledNote( ): Promise { logger.info(`Creating: ${job.data.noteId}`); - const user = await Users.findOneBy({ id: job.data.user.id }); + const [user, draftNote] = await Promise.all([ + Users.findOneBy({ id: job.data.user.id }), + Notes.findOneBy({ id: job.data.noteId }), + ]); + if (user == null) { + logger.warn(`User ${job.data.user.id} does not exist, aborting`); done(); return; } - const note = await Notes.findOneBy({ id: job.data.noteId }); - if (note == null) { + if (draftNote == null) { + logger.warn(`Note ${job.data.noteId} does not exist, aborting`); done(); return; } - const files = await DriveFiles.findBy({ id: In(note.fileIds) }); if (user.isSuspended) { - deleteNote(user, note); + logger.info( + `Cancelled due to user ${job.data.user.id} being suspended, aborting`, + ); + await deleteNote(user, draftNote); done(); return; } - await ScheduledNotes.delete({ - noteId: note.id, - userId: user.id, - }); + const [visibleUsers, reply, renote, files] = await Promise.all([ + job.data.option.visibleUserIds + ? Users.findBy({ + id: In(job.data.option.visibleUserIds), + }) + : [], + job.data.option.replyId != null + ? Notes.findOneBy({ id: job.data.option.replyId }) + : undefined, + job.data.option.renoteId != null + ? Notes.findOneBy({ id: job.data.option.renoteId }) + : undefined, + DriveFiles.findBy({ id: In(draftNote.fileIds) }), + ]); - const visibleUsers = job.data.option.visibleUserIds - ? await Users.findBy({ - id: In(job.data.option.visibleUserIds), - }) - : []; + if (job.data.option.replyId != null && reply == null) { + logger.warn( + `Note ${job.data.option.replyId} (reply) does not exist, aborting`, + ); + done(); + return; + } + if (job.data.option.renoteId != null && renote == null) { + logger.warn( + `Note ${job.data.option.renoteId} (renote) does not exist, aborting`, + ); + done(); + return; + } + + // Create scheduled (actual) note await createNote(user, { createdAt: new Date(), + scheduledAt: null, files, poll: job.data.option.poll, - text: note.text || undefined, - lang: note.lang, - reply: note.reply, - renote: note.renote, - cw: note.cw, - localOnly: note.localOnly, + text: draftNote.text || undefined, + lang: draftNote.lang, + reply, + renote, + cw: draftNote.cw, + localOnly: draftNote.localOnly, visibility: job.data.option.visibility, visibleUsers, - channel: note.channel, + channel: draftNote.channel, }); - await deleteNote(user, note); + // Delete temporal (draft) note + await deleteNote(user, draftNote); logger.info("Success"); diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index c0d719a312..2db59a031d 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -62,6 +62,8 @@ export type DbUserScheduledNoteData = { option: { visibility: string; visibleUserIds?: string[] | null; + replyId?: string; + renoteId?: string; poll?: IPoll; }; noteId: Note["id"]; diff --git a/packages/backend/src/remote/activitypub/check-fetch.ts b/packages/backend/src/remote/activitypub/check-fetch.ts index bff7548983..3ab10f1939 100644 --- a/packages/backend/src/remote/activitypub/check-fetch.ts +++ b/packages/backend/src/remote/activitypub/check-fetch.ts @@ -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 { - 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 { } export async function checkFetch(req: IncomingMessage): Promise { - const meta = await fetchMeta(true); + const meta = await fetchMeta(); if (meta.secureMode || meta.privateMode) { if (req.headers.host !== config.host) return 400; diff --git a/packages/backend/src/remote/activitypub/kernel/flag/index.ts b/packages/backend/src/remote/activitypub/kernel/flag/index.ts index c556605865..755d2b7d5e 100644 --- a/packages/backend/src/remote/activitypub/kernel/flag/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/flag/index.ts @@ -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, diff --git a/packages/backend/src/remote/activitypub/models/image.ts b/packages/backend/src/remote/activitypub/models/image.ts index 5fb261488f..e8ba7629a8 100644 --- a/packages/backend/src/remote/activitypub/models/image.ts +++ b/packages/backend/src/remote/activitypub/models/image.ts @@ -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, diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index dc3b29dc84..e861553e97 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -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, diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts index 2ada433eb5..0d04ded58b 100644 --- a/packages/backend/src/server/activitypub.ts +++ b/packages/backend/src/server/activitypub.ts @@ -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 { diff --git a/packages/backend/src/server/activitypub/featured.ts b/packages/backend/src/server/activitypub/featured.ts index 671c7ac67e..4dc93e734a 100644 --- a/packages/backend/src/server/activitypub/featured.ts +++ b/packages/backend/src/server/activitypub/featured.ts @@ -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 { diff --git a/packages/backend/src/server/activitypub/followers.ts b/packages/backend/src/server/activitypub/followers.ts index 603e93ebe8..92d6e800ea 100644 --- a/packages/backend/src/server/activitypub/followers.ts +++ b/packages/backend/src/server/activitypub/followers.ts @@ -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 { diff --git a/packages/backend/src/server/activitypub/following.ts b/packages/backend/src/server/activitypub/following.ts index be5a4e9643..8ded68b8c4 100644 --- a/packages/backend/src/server/activitypub/following.ts +++ b/packages/backend/src/server/activitypub/following.ts @@ -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 { diff --git a/packages/backend/src/server/activitypub/outbox.ts b/packages/backend/src/server/activitypub/outbox.ts index 06319565e5..f04dcb6bf6 100644 --- a/packages/backend/src/server/activitypub/outbox.ts +++ b/packages/backend/src/server/activitypub/outbox.ts @@ -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 { diff --git a/packages/backend/src/server/api/api-handler.ts b/packages/backend/src/server/api/api-handler.ts index 5e65636427..e46d9558f9 100644 --- a/packages/backend/src/server/api/api-handler.ts +++ b/packages/backend/src/server/api/api-handler.ts @@ -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); diff --git a/packages/backend/src/server/api/call.ts b/packages/backend/src/server/api/call.ts index 3107156a9b..2309e3665c 100644 --- a/packages/backend/src/server/api/call.ts +++ b/packages/backend/src/server/api/call.ts @@ -117,7 +117,7 @@ export default async ( } // private mode - const meta = await fetchMeta(true); + const meta = await fetchMeta(); if ( meta.privateMode && ep.meta.requireCredentialPrivateMode && diff --git a/packages/backend/src/server/api/common/read-messaging-message.ts b/packages/backend/src/server/api/common/read-messaging-message.ts index e8706fe86a..d4d04c6b91 100644 --- a/packages/backend/src/server/api/common/read-messaging-message.ts +++ b/packages/backend/src/server/api/common/read-messaging-message.ts @@ -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) { diff --git a/packages/backend/src/server/api/common/signin.ts b/packages/backend/src/server/api/common/signin.ts index e5ca09df95..bafec3dfad 100644 --- a/packages/backend/src/server/api/common/signin.ts +++ b/packages/backend/src/server/api/common/signin.ts @@ -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, diff --git a/packages/backend/src/server/api/common/signup.ts b/packages/backend/src/server/api/common/signup.ts index 67f06ac4e1..05e92bcbea 100644 --- a/packages/backend/src/server/api/common/signup.ts +++ b/packages/backend/src/server/api/common/signup.ts @@ -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(), }), ); diff --git a/packages/backend/src/server/api/endpoints/admin/ad/create.ts b/packages/backend/src/server/api/endpoints/admin/ad/create.ts index c7a56d3a98..56eedbe602 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/create.ts @@ -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, diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts index 1ae25e62d0..1bd83f3b95 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts @@ -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, diff --git a/packages/backend/src/server/api/endpoints/admin/invite.ts b/packages/backend/src/server/api/endpoints/admin/invite.ts index d220ae5e4a..912bb986df 100644 --- a/packages/backend/src/server/api/endpoints/admin/invite.ts +++ b/packages/backend/src/server/api/endpoints/admin/invite.ts @@ -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, }); diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 9e41e58c0a..052f89452e 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -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, diff --git a/packages/backend/src/server/api/endpoints/admin/promo/create.ts b/packages/backend/src/server/api/endpoints/admin/promo/create.ts index 55038fc89b..ffbab9367b 100644 --- a/packages/backend/src/server/api/endpoints/admin/promo/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/promo/create.ts @@ -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); } diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index e5234ea720..210b5d25f5 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -2,6 +2,7 @@ import { Meta } from "@/models/entities/meta.js"; import { insertModerationLog } from "@/services/insert-moderation-log.js"; import { db } from "@/db/postgre.js"; import define from "@/server/api/define.js"; +import { updateMetaCache } from "backend-rs"; export const meta = { tags: ["admin"], @@ -583,5 +584,5 @@ export default define(meta, paramDef, async (ps, me) => { } }); - insertModerationLog(me, "updateMeta"); + await Promise.all([insertModerationLog(me, "updateMeta"), updateMetaCache()]); }); diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index 76842df529..69dd19b0cb 100644 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ b/packages/backend/src/server/api/endpoints/antennas/create.ts @@ -1,5 +1,5 @@ import define from "@/server/api/define.js"; -import { fetchMeta, genId } from "backend-rs"; +import { fetchMeta, genIdAt, updateAntennaCache } 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, @@ -171,6 +173,7 @@ export default define(meta, paramDef, async (ps, user) => { }).then((x) => Antennas.findOneByOrFail(x.identifiers[0])); publishInternalEvent("antennaCreated", antenna); + await updateAntennaCache(); return await Antennas.pack(antenna); }); diff --git a/packages/backend/src/server/api/endpoints/antennas/delete.ts b/packages/backend/src/server/api/endpoints/antennas/delete.ts index e5a372f193..64ba1c2e1f 100644 --- a/packages/backend/src/server/api/endpoints/antennas/delete.ts +++ b/packages/backend/src/server/api/endpoints/antennas/delete.ts @@ -2,6 +2,7 @@ import define from "@/server/api/define.js"; import { ApiError } from "@/server/api/error.js"; import { Antennas } from "@/models/index.js"; import { publishInternalEvent } from "@/services/stream.js"; +import { updateAntennaCache } from "backend-rs"; export const meta = { tags: ["antennas"], @@ -40,4 +41,5 @@ export default define(meta, paramDef, async (ps, user) => { await Antennas.delete(antenna.id); publishInternalEvent("antennaDeleted", antenna); + await updateAntennaCache(); }); diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts index 5e74f4d6b2..24d659f47a 100644 --- a/packages/backend/src/server/api/endpoints/antennas/update.ts +++ b/packages/backend/src/server/api/endpoints/antennas/update.ts @@ -2,6 +2,7 @@ import define from "@/server/api/define.js"; import { ApiError } from "@/server/api/error.js"; import { Antennas, UserLists, UserGroupJoinings } from "@/models/index.js"; import { publishInternalEvent } from "@/services/stream.js"; +import { updateAntennaCache } from "backend-rs"; export const meta = { tags: ["antennas"], @@ -166,6 +167,7 @@ export default define(meta, paramDef, async (ps, user) => { "antennaUpdated", await Antennas.findOneByOrFail({ id: antenna.id }), ); + await updateAntennaCache(); return await Antennas.pack(antenna.id); }); diff --git a/packages/backend/src/server/api/endpoints/app/create.ts b/packages/backend/src/server/api/endpoints/app/create.ts index 0d580d1535..05f511f399 100644 --- a/packages/backend/src/server/api/endpoints/app/create.ts +++ b/packages/backend/src/server/api/endpoints/app/create.ts @@ -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, diff --git a/packages/backend/src/server/api/endpoints/auth/accept.ts b/packages/backend/src/server/api/endpoints/auth/accept.ts index 3bdc9e18b5..ab8b0cd148 100644 --- a/packages/backend/src/server/api/endpoints/auth/accept.ts +++ b/packages/backend/src/server/api/endpoints/auth/accept.ts @@ -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, diff --git a/packages/backend/src/server/api/endpoints/auth/session/generate.ts b/packages/backend/src/server/api/endpoints/auth/session/generate.ts index f20219aa82..2f44c778d5 100644 --- a/packages/backend/src/server/api/endpoints/auth/session/generate.ts +++ b/packages/backend/src/server/api/endpoints/auth/session/generate.ts @@ -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])); diff --git a/packages/backend/src/server/api/endpoints/blocking/create.ts b/packages/backend/src/server/api/endpoints/blocking/create.ts index 85ac239f35..a2c58aea50 100644 --- a/packages/backend/src/server/api/endpoints/blocking/create.ts +++ b/packages/backend/src/server/api/endpoints/blocking/create.ts @@ -69,14 +69,12 @@ export default define(meta, paramDef, async (ps, user) => { }); // Check if already blocking - const exist = await Blockings.exist({ - where: { - blockerId: blocker.id, - blockeeId: blockee.id, - }, + const exists = await Blockings.existsBy({ + blockerId: blocker.id, + blockeeId: blockee.id, }); - if (exist) { + if (exists) { throw new ApiError(meta.errors.alreadyBlocking); } diff --git a/packages/backend/src/server/api/endpoints/blocking/delete.ts b/packages/backend/src/server/api/endpoints/blocking/delete.ts index ce1a717d13..bdd930f0df 100644 --- a/packages/backend/src/server/api/endpoints/blocking/delete.ts +++ b/packages/backend/src/server/api/endpoints/blocking/delete.ts @@ -69,14 +69,12 @@ export default define(meta, paramDef, async (ps, user) => { }); // Check not blocking - const exist = await Blockings.exist({ - where: { - blockerId: blocker.id, - blockeeId: blockee.id, - }, + const exists = await Blockings.existsBy({ + blockerId: blocker.id, + blockeeId: blockee.id, }); - if (!exist) { + if (!exists) { throw new ApiError(meta.errors.notBlocking); } diff --git a/packages/backend/src/server/api/endpoints/channels/create.ts b/packages/backend/src/server/api/endpoints/channels/create.ts index 29c6ca029a..f5ec33a14c 100644 --- a/packages/backend/src/server/api/endpoints/channels/create.ts +++ b/packages/backend/src/server/api/endpoints/channels/create.ts @@ -2,7 +2,7 @@ import define from "@/server/api/define.js"; import { ApiError } from "@/server/api/error.js"; import { Channels, DriveFiles } from "@/models/index.js"; import type { Channel } from "@/models/entities/channel.js"; -import { genId } from "backend-rs"; +import { genIdAt } from "backend-rs"; export const meta = { tags: ["channels"], @@ -55,9 +55,11 @@ export default define(meta, paramDef, async (ps, user) => { } } + const now = new Date(); + const channel = await Channels.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, userId: user.id, name: ps.name, description: ps.description || null, diff --git a/packages/backend/src/server/api/endpoints/channels/follow.ts b/packages/backend/src/server/api/endpoints/channels/follow.ts index fea6ca797b..afc55ac1cf 100644 --- a/packages/backend/src/server/api/endpoints/channels/follow.ts +++ b/packages/backend/src/server/api/endpoints/channels/follow.ts @@ -1,7 +1,7 @@ import define from "@/server/api/define.js"; import { ApiError } from "@/server/api/error.js"; import { Channels, ChannelFollowings } from "@/models/index.js"; -import { genId } from "backend-rs"; +import { genIdAt } from "backend-rs"; import { publishUserEvent } from "@/services/stream.js"; export const meta = { @@ -37,9 +37,11 @@ export default define(meta, paramDef, async (ps, user) => { throw new ApiError(meta.errors.noSuchChannel); } + const now = new Date(); + await ChannelFollowings.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, followerId: user.id, followeeId: channel.id, }); diff --git a/packages/backend/src/server/api/endpoints/clips/add-note.ts b/packages/backend/src/server/api/endpoints/clips/add-note.ts index 2c34db5603..305e977681 100644 --- a/packages/backend/src/server/api/endpoints/clips/add-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/add-note.ts @@ -57,14 +57,12 @@ export default define(meta, paramDef, async (ps, user) => { throw err; }); - const exist = await ClipNotes.exist({ - where: { - noteId: note.id, - clipId: clip.id, - }, + const exists = await ClipNotes.existsBy({ + noteId: note.id, + clipId: clip.id, }); - if (exist) { + if (exists) { throw new ApiError(meta.errors.alreadyClipped); } diff --git a/packages/backend/src/server/api/endpoints/clips/create.ts b/packages/backend/src/server/api/endpoints/clips/create.ts index ae652d75c2..9e5e979509 100644 --- a/packages/backend/src/server/api/endpoints/clips/create.ts +++ b/packages/backend/src/server/api/endpoints/clips/create.ts @@ -1,5 +1,5 @@ import define from "@/server/api/define.js"; -import { genId } from "backend-rs"; +import { genIdAt } from "backend-rs"; import { Clips } from "@/models/index.js"; export const meta = { @@ -33,9 +33,10 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, user) => { + const now = new Date(); const clip = await Clips.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, userId: user.id, name: ps.name, isPublic: ps.isPublic, diff --git a/packages/backend/src/server/api/endpoints/custom-motd.ts b/packages/backend/src/server/api/endpoints/custom-motd.ts index ac1012258d..ea3571a362 100644 --- a/packages/backend/src/server/api/endpoints/custom-motd.ts +++ b/packages/backend/src/server/api/endpoints/custom-motd.ts @@ -27,7 +27,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async () => { - const meta = await fetchMeta(true); + const meta = await fetchMeta(); const motd = await Promise.all(meta.customMotd.map((x) => x)); return motd; }); diff --git a/packages/backend/src/server/api/endpoints/custom-splash-icons.ts b/packages/backend/src/server/api/endpoints/custom-splash-icons.ts index 4eb35aa3e5..2493ea0bde 100644 --- a/packages/backend/src/server/api/endpoints/custom-splash-icons.ts +++ b/packages/backend/src/server/api/endpoints/custom-splash-icons.ts @@ -27,7 +27,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async () => { - const meta = await fetchMeta(true); + const meta = await fetchMeta(); const icons = await Promise.all(meta.customSplashIcons.map((x) => x)); return icons; }); diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts index c04f219a9b..f19ccf3b45 100644 --- a/packages/backend/src/server/api/endpoints/drive.ts +++ b/packages/backend/src/server/api/endpoints/drive.ts @@ -35,7 +35,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, user) => { - const instance = await fetchMeta(false); + const instance = await fetchMeta(); // Calculate drive usage const usage = await DriveFiles.calcDriveUsageOf(user.id); diff --git a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts index 2ce9fa4700..cc98865fb4 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts @@ -26,12 +26,8 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, user) => { - const exist = await DriveFiles.exist({ - where: { - md5: ps.md5, - userId: user.id, - }, + return await DriveFiles.existsBy({ + md5: ps.md5, + userId: user.id, }); - - return exist; }); diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts index 5b2a70bb94..e7d73513dd 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -95,7 +95,7 @@ export default define( name = null; } - const instanceMeta = await fetchMeta(true); + const instanceMeta = await fetchMeta(); try { // Create file diff --git a/packages/backend/src/server/api/endpoints/drive/files/delete.ts b/packages/backend/src/server/api/endpoints/drive/files/delete.ts index 062b69b9c5..2b5b3bc7bd 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/delete.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/delete.ts @@ -1,5 +1,5 @@ import { deleteFile } from "@/services/drive/delete-file.js"; -import { publishDriveStream } from "@/services/stream.js"; +import { publishToDriveFileStream, DriveFileEvent } from "backend-rs"; import define from "@/server/api/define.js"; import { ApiError } from "@/server/api/error.js"; import { DriveFiles } from "@/models/index.js"; @@ -51,5 +51,5 @@ export default define(meta, paramDef, async (ps, user) => { await deleteFile(file); // Publish fileDeleted event - publishDriveStream(user.id, "fileDeleted", file.id); + publishToDriveFileStream(user.id, DriveFileEvent.Delete, file.id); }); diff --git a/packages/backend/src/server/api/endpoints/drive/files/update.ts b/packages/backend/src/server/api/endpoints/drive/files/update.ts index 4d9567a838..c82cffb4a6 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts @@ -1,8 +1,9 @@ -import { publishDriveStream } from "@/services/stream.js"; +import { publishToDriveFileStream, DriveFileEvent } from "backend-rs"; import { DriveFiles, DriveFolders } from "@/models/index.js"; import { config } from "@/config.js"; import define from "@/server/api/define.js"; import { ApiError } from "@/server/api/error.js"; +import { toRustObject } from "@/prelude/undefined-to-null.js"; export const meta = { tags: ["drive"], @@ -110,7 +111,7 @@ export default define(meta, paramDef, async (ps, user) => { const fileObj = await DriveFiles.pack(file, { self: true }); // Publish fileUpdated event - publishDriveStream(user.id, "fileUpdated", fileObj); + publishToDriveFileStream(user.id, DriveFileEvent.Update, toRustObject(file)); return fileObj; }); diff --git a/packages/backend/src/server/api/endpoints/drive/folders/create.ts b/packages/backend/src/server/api/endpoints/drive/folders/create.ts index 9ea570a6b7..8d8c8b37b4 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/create.ts @@ -1,8 +1,9 @@ -import { publishDriveStream } from "@/services/stream.js"; +import { publishToDriveFolderStream, DriveFolderEvent } from "backend-rs"; import define from "@/server/api/define.js"; import { ApiError } from "@/server/api/error.js"; import { DriveFolders } from "@/models/index.js"; -import { genId } from "backend-rs"; +import { genIdAt } from "backend-rs"; +import { toRustObject } from "@/prelude/undefined-to-null.js"; export const meta = { tags: ["drive"], @@ -52,9 +53,10 @@ export default define(meta, paramDef, async (ps, user) => { } // Create folder + const now = new Date(); const folder = await DriveFolders.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, name: ps.name, parentId: parent != null ? parent.id : null, userId: user.id, @@ -63,7 +65,11 @@ export default define(meta, paramDef, async (ps, user) => { const folderObj = await DriveFolders.pack(folder); // Publish folderCreated event - publishDriveStream(user.id, "folderCreated", folderObj); + publishToDriveFolderStream( + user.id, + DriveFolderEvent.Create, + toRustObject(folder), + ); return folderObj; }); diff --git a/packages/backend/src/server/api/endpoints/drive/folders/delete.ts b/packages/backend/src/server/api/endpoints/drive/folders/delete.ts index 1954b43361..a5e80dee44 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/delete.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/delete.ts @@ -1,5 +1,5 @@ import define from "@/server/api/define.js"; -import { publishDriveStream } from "@/services/stream.js"; +import { publishToDriveFolderStream, DriveFolderEvent } from "backend-rs"; import { ApiError } from "@/server/api/error.js"; import { DriveFolders, DriveFiles } from "@/models/index.js"; @@ -56,5 +56,5 @@ export default define(meta, paramDef, async (ps, user) => { await DriveFolders.delete(folder.id); // Publish folderCreated event - publishDriveStream(user.id, "folderDeleted", folder.id); + publishToDriveFolderStream(user.id, DriveFolderEvent.Delete, folder.id); }); diff --git a/packages/backend/src/server/api/endpoints/drive/folders/update.ts b/packages/backend/src/server/api/endpoints/drive/folders/update.ts index 7b266548e5..0deb1ef8a8 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/update.ts @@ -1,7 +1,8 @@ -import { publishDriveStream } from "@/services/stream.js"; +import { publishToDriveFolderStream, DriveFolderEvent } from "backend-rs"; import define from "@/server/api/define.js"; import { ApiError } from "@/server/api/error.js"; import { DriveFolders } from "@/models/index.js"; +import { toRustObject } from "@/prelude/undefined-to-null.js"; export const meta = { tags: ["drive"], @@ -112,7 +113,11 @@ export default define(meta, paramDef, async (ps, user) => { const folderObj = await DriveFolders.pack(folder); // Publish folderUpdated event - publishDriveStream(user.id, "folderUpdated", folderObj); + publishToDriveFolderStream( + user.id, + DriveFolderEvent.Update, + toRustObject(folder), + ); return folderObj; }); diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index 362ab098fb..ab636ac2f7 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -100,7 +100,7 @@ export default define(meta, paramDef, async (ps, me) => { } if (typeof ps.blocked === "boolean") { - const meta = await fetchMeta(false); + const meta = await fetchMeta(); if (ps.blocked) { if (meta.blockedHosts.length === 0) { return []; @@ -116,7 +116,7 @@ export default define(meta, paramDef, async (ps, me) => { } if (typeof ps.silenced === "boolean") { - const meta = await fetchMeta(false); + const meta = await fetchMeta(); if (ps.silenced) { if (meta.silencedHosts.length === 0) { return []; diff --git a/packages/backend/src/server/api/endpoints/following/create.ts b/packages/backend/src/server/api/endpoints/following/create.ts index 956dc14f6f..29d5d6c84c 100644 --- a/packages/backend/src/server/api/endpoints/following/create.ts +++ b/packages/backend/src/server/api/endpoints/following/create.ts @@ -82,14 +82,12 @@ export default define(meta, paramDef, async (ps, user) => { }); // Check if already following - const exist = await Followings.exist({ - where: { - followerId: follower.id, - followeeId: followee.id, - }, + const exists = await Followings.existsBy({ + followerId: follower.id, + followeeId: followee.id, }); - if (exist) { + if (exists) { throw new ApiError(meta.errors.alreadyFollowing); } diff --git a/packages/backend/src/server/api/endpoints/following/delete.ts b/packages/backend/src/server/api/endpoints/following/delete.ts index 740ef5fd93..95fd254f48 100644 --- a/packages/backend/src/server/api/endpoints/following/delete.ts +++ b/packages/backend/src/server/api/endpoints/following/delete.ts @@ -69,14 +69,12 @@ export default define(meta, paramDef, async (ps, user) => { }); // Check not following - const exist = await Followings.exist({ - where: { - followerId: follower.id, - followeeId: followee.id, - }, + const exists = await Followings.existsBy({ + followerId: follower.id, + followeeId: followee.id, }); - if (!exist) { + if (!exists) { throw new ApiError(meta.errors.notFollowing); } diff --git a/packages/backend/src/server/api/endpoints/following/invalidate.ts b/packages/backend/src/server/api/endpoints/following/invalidate.ts index 682e7d963b..1baa8df64f 100644 --- a/packages/backend/src/server/api/endpoints/following/invalidate.ts +++ b/packages/backend/src/server/api/endpoints/following/invalidate.ts @@ -69,14 +69,12 @@ export default define(meta, paramDef, async (ps, user) => { }); // Check not following - const exist = await Followings.exist({ - where: { - followerId: follower.id, - followeeId: followee.id, - }, + const exists = await Followings.existsBy({ + followerId: follower.id, + followeeId: followee.id, }); - if (!exist) { + if (!exists) { throw new ApiError(meta.errors.notFollowing); } diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts index 01c772bc3c..eda2d81e5c 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts @@ -1,6 +1,6 @@ import define from "@/server/api/define.js"; import { DriveFiles, GalleryPosts } from "@/models/index.js"; -import { HOUR, genId } from "backend-rs"; +import { HOUR, genIdAt } from "backend-rs"; import { GalleryPost } from "@/models/entities/gallery-post.js"; import type { DriveFile } from "@/models/entities/drive-file.js"; @@ -62,11 +62,13 @@ export default define(meta, paramDef, async (ps, user) => { throw new Error(); } + const now = new Date(); + const post = await GalleryPosts.insert( new GalleryPost({ - id: genId(), - createdAt: new Date(), - updatedAt: new Date(), + id: genIdAt(now), + createdAt: now, + updatedAt: now, title: ps.title, description: ps.description, userId: user.id, diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts index 13f7d82bb2..bd31da4900 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts @@ -1,7 +1,7 @@ import define from "@/server/api/define.js"; import { ApiError } from "@/server/api/error.js"; import { GalleryPosts, GalleryLikes } from "@/models/index.js"; -import { genId } from "backend-rs"; +import { genIdAt } from "backend-rs"; export const meta = { tags: ["gallery"], @@ -40,21 +40,21 @@ export default define(meta, paramDef, async (ps, user) => { } // if already liked - const exist = await GalleryLikes.exist({ - where: { - postId: post.id, - userId: user.id, - }, + const exists = await GalleryLikes.existsBy({ + postId: post.id, + userId: user.id, }); - if (exist) { + if (exists) { throw new ApiError(meta.errors.alreadyLiked); } + const now = new Date(); + // Create like await GalleryLikes.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, postId: post.id, userId: user.id, }); diff --git a/packages/backend/src/server/api/endpoints/hashtags/trend.ts b/packages/backend/src/server/api/endpoints/hashtags/trend.ts index 531a494248..2f606110fa 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/trend.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/trend.ts @@ -66,7 +66,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async () => { - const instance = await fetchMeta(false); + const instance = await fetchMeta(); const hiddenTags = instance.hiddenTags.map((t) => normalizeForSearch(t)); const now = new Date(); // 5分単位で丸めた現在日時 diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts index 0951369dd8..01413d6f37 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts @@ -75,7 +75,7 @@ export default define(meta, paramDef, async (ps, user) => { const credentialIdLength = authData.readUInt16BE(53); const credentialId = authData.slice(55, 55 + credentialIdLength); const publicKeyData = authData.slice(55 + credentialIdLength); - const publicKey: Map = new Map( + const publicKey: Map = new Map( Object.entries(decode(publicKeyData)).map(([key, value]) => [ Number(key), value, diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts index 4991e8fc90..acf12b7168 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts @@ -2,7 +2,7 @@ import define from "@/server/api/define.js"; import { UserProfiles, AttestationChallenges } from "@/models/index.js"; import { promisify } from "node:util"; import * as crypto from "node:crypto"; -import { genId, verifyPassword } from "backend-rs"; +import { genIdAt, verifyPassword } from "backend-rs"; import { hash } from "@/server/api/2fa.js"; const randomBytes = promisify(crypto.randomBytes); @@ -39,13 +39,14 @@ export default define(meta, paramDef, async (ps, user) => { .replace(/\+/g, "-") .replace(/\//g, "_"); - const challengeId = genId(); + const now = new Date(); + const challengeId = genIdAt(now); await AttestationChallenges.insert({ userId: user.id, id: challengeId, challenge: hash(Buffer.from(challenge, "utf-8")).toString("hex"), - createdAt: new Date(), + createdAt: now, registrationChallenge: true, }); diff --git a/packages/backend/src/server/api/endpoints/i/import-posts.ts b/packages/backend/src/server/api/endpoints/i/import-posts.ts index f7a4d41465..01f61138f5 100644 --- a/packages/backend/src/server/api/endpoints/i/import-posts.ts +++ b/packages/backend/src/server/api/endpoints/i/import-posts.ts @@ -44,7 +44,7 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const file = await DriveFiles.findOneBy({ id: ps.fileId }); - const instanceMeta = await fetchMeta(true); + const instanceMeta = await fetchMeta(); if (instanceMeta.experimentalFeatures?.postImports === false) throw new ApiError(meta.errors.importsDisabled); diff --git a/packages/backend/src/server/api/endpoints/i/read-announcement.ts b/packages/backend/src/server/api/endpoints/i/read-announcement.ts index 140eecfe67..7981292031 100644 --- a/packages/backend/src/server/api/endpoints/i/read-announcement.ts +++ b/packages/backend/src/server/api/endpoints/i/read-announcement.ts @@ -1,6 +1,6 @@ import define from "@/server/api/define.js"; import { ApiError } from "@/server/api/error.js"; -import { genId } from "backend-rs"; +import { genIdAt } from "backend-rs"; import { AnnouncementReads, Announcements, Users } from "@/models/index.js"; import { publishMainStream } from "@/services/stream.js"; @@ -30,30 +30,30 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { // Check if announcement exists - const exist = await Announcements.exist({ - where: { id: ps.announcementId }, + const exists = await Announcements.existsBy({ + id: ps.announcementId, }); - if (!exist) { + if (!exists) { throw new ApiError(meta.errors.noSuchAnnouncement); } // Check if already read - const read = await AnnouncementReads.exist({ - where: { - announcementId: ps.announcementId, - userId: user.id, - }, + const read = await AnnouncementReads.existsBy({ + announcementId: ps.announcementId, + userId: user.id, }); if (read) { return; } + const now = new Date(); + // Create read await AnnouncementReads.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, announcementId: ps.announcementId, userId: user.id, }); diff --git a/packages/backend/src/server/api/endpoints/i/registry/set.ts b/packages/backend/src/server/api/endpoints/i/registry/set.ts index 7090f0914b..e778bd98d8 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/set.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/set.ts @@ -1,7 +1,7 @@ import { publishMainStream } from "@/services/stream.js"; import define from "@/server/api/define.js"; import { RegistryItems } from "@/models/index.js"; -import { genId } from "backend-rs"; +import { genIdAt } from "backend-rs"; export const meta = { requireCredential: true, @@ -41,10 +41,11 @@ export default define(meta, paramDef, async (ps, user) => { value: ps.value, }); } else { + const now = new Date(); await RegistryItems.insert({ - id: genId(), - createdAt: new Date(), - updatedAt: new Date(), + id: genIdAt(now), + createdAt: now, + updatedAt: now, userId: user.id, domain: null, scope: ps.scope, diff --git a/packages/backend/src/server/api/endpoints/i/revoke-token.ts b/packages/backend/src/server/api/endpoints/i/revoke-token.ts index f3e8116944..e38b330742 100644 --- a/packages/backend/src/server/api/endpoints/i/revoke-token.ts +++ b/packages/backend/src/server/api/endpoints/i/revoke-token.ts @@ -17,9 +17,9 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, user) => { - const exist = await AccessTokens.exist({ where: { id: ps.tokenId } }); + const exists = await AccessTokens.existsBy({ id: ps.tokenId }); - if (exist) { + if (exists) { await AccessTokens.delete({ id: ps.tokenId, userId: user.id, diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts index 4faae058c3..031f0e4eab 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts @@ -1,5 +1,5 @@ import define from "@/server/api/define.js"; -import { genId } from "backend-rs"; +import { genIdAt } from "backend-rs"; import { Webhooks } from "@/models/index.js"; import { publishInternalEvent } from "@/services/stream.js"; import { webhookEventTypes } from "@/models/entities/webhook.js"; @@ -30,9 +30,10 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, user) => { + const now = new Date(); const webhook = await Webhooks.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, userId: user.id, name: ps.name, url: ps.url, diff --git a/packages/backend/src/server/api/endpoints/latest-version.ts b/packages/backend/src/server/api/endpoints/latest-version.ts index e2146303b7..526b89bc62 100644 --- a/packages/backend/src/server/api/endpoints/latest-version.ts +++ b/packages/backend/src/server/api/endpoints/latest-version.ts @@ -6,6 +6,7 @@ export const meta = { requireCredential: false, requireCredentialPrivateMode: true, + allowGet: true, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index d4a4172426..a9622c2ed3 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -402,7 +402,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, me) => { - const instance = await fetchMeta(false); + const instance = await fetchMeta(); const emojis = await Emojis.find({ where: { diff --git a/packages/backend/src/server/api/endpoints/mute/create.ts b/packages/backend/src/server/api/endpoints/mute/create.ts index a8d37153a4..c8d6744b1b 100644 --- a/packages/backend/src/server/api/endpoints/mute/create.ts +++ b/packages/backend/src/server/api/endpoints/mute/create.ts @@ -1,7 +1,7 @@ import define from "@/server/api/define.js"; import { ApiError } from "@/server/api/error.js"; import { getUser } from "@/server/api/common/getters.js"; -import { genId } from "backend-rs"; +import { genIdAt } from "backend-rs"; import { Mutings, NoteWatchings } from "@/models/index.js"; import type { Muting } from "@/models/entities/muting.js"; import { publishUserEvent } from "@/services/stream.js"; @@ -64,14 +64,12 @@ export default define(meta, paramDef, async (ps, user) => { }); // Check if already muting - const exist = await Mutings.exist({ - where: { - muterId: muter.id, - muteeId: mutee.id, - }, + const exists = await Mutings.existsBy({ + muterId: muter.id, + muteeId: mutee.id, }); - if (exist) { + if (exists) { throw new ApiError(meta.errors.alreadyMuting); } @@ -79,10 +77,12 @@ export default define(meta, paramDef, async (ps, user) => { return; } + const now = new Date(); + // Create mute await Mutings.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null, muterId: muter.id, muteeId: mutee.id, diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 50179db5e2..17f9a7ece9 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -7,7 +7,6 @@ import { Notes, Channels, Blockings, - ScheduledNotes, } from "@/models/index.js"; import type { DriveFile } from "@/models/entities/drive-file.js"; import type { Note } from "@/models/entities/note.js"; @@ -16,7 +15,7 @@ import { config } from "@/config.js"; import { noteVisibilities } from "@/types.js"; import { ApiError } from "@/server/api/error.js"; import define from "@/server/api/define.js"; -import { HOUR, genId } from "backend-rs"; +import { HOUR } from "backend-rs"; import { getNote } from "@/server/api/common/getters.js"; import { langmap } from "firefish-js"; import { createScheduledNoteJob } from "@/queue/index.js"; @@ -95,6 +94,12 @@ export const meta = { code: "ACCOUNT_LOCKED", id: "d390d7e1-8a5e-46ed-b625-06271cafd3d3", }, + + scheduledTimeIsPast: { + message: "The scheduled time is past.", + code: "SCHEDULED_TIME_IS_PAST", + id: "277f91df-8d8e-4647-b4e3-5885fda8978a", + }, }, } as const; @@ -233,11 +238,9 @@ export default define(meta, paramDef, async (ps, user) => { // Check blocking if (renote.userId !== user.id) { - const isBlocked = await Blockings.exists({ - where: { - blockerId: renote.userId, - blockeeId: user.id, - }, + const isBlocked = await Blockings.existsBy({ + blockerId: renote.userId, + blockeeId: user.id, }); if (isBlocked) { throw new ApiError(meta.errors.youHaveBeenBlocked); @@ -260,11 +263,9 @@ export default define(meta, paramDef, async (ps, user) => { // Check blocking if (reply.userId !== user.id) { - const isBlocked = await Blockings.exists({ - where: { - blockerId: reply.userId, - blockeeId: user.id, - }, + const isBlocked = await Blockings.existsBy({ + blockerId: reply.userId, + blockeeId: user.id, }); if (isBlocked) { throw new ApiError(meta.errors.youHaveBeenBlocked); @@ -303,10 +304,10 @@ export default define(meta, paramDef, async (ps, user) => { } let delay: number | null = null; - if (ps.scheduledAt) { + if (ps.scheduledAt != null) { delay = ps.scheduledAt - Date.now(); if (delay < 0) { - delay = null; + throw new ApiError(meta.errors.scheduledTimeIsPast); } } @@ -315,12 +316,14 @@ export default define(meta, paramDef, async (ps, user) => { user, { createdAt: new Date(), + scheduledAt: delay != null ? new Date(ps.scheduledAt!) : null, files: files, poll: ps.poll ? { choices: ps.poll.choices, multiple: ps.poll.multiple, - expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, + expiresAt: + ps.poll.expiresAt != null ? new Date(ps.poll.expiresAt) : null, } : undefined, text: ps.text || undefined, @@ -346,13 +349,6 @@ export default define(meta, paramDef, async (ps, user) => { false, delay ? async (note) => { - await ScheduledNotes.insert({ - id: genId(), - noteId: note.id, - userId: user.id, - scheduledAt: new Date(ps.scheduledAt as number), - }); - createScheduledNoteJob( { user: { id: user.id }, @@ -369,6 +365,8 @@ export default define(meta, paramDef, async (ps, user) => { : undefined, visibility: ps.visibility, visibleUserIds: ps.visibleUserIds, + replyId: ps.replyId ?? undefined, + renoteId: ps.renoteId ?? undefined, }, }, delay, diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts index e2c7ca951f..37df0f4023 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts @@ -1,5 +1,5 @@ import { NoteFavorites } from "@/models/index.js"; -import { genId } from "backend-rs"; +import { genIdAt } from "backend-rs"; import define from "@/server/api/define.js"; import { ApiError } from "@/server/api/error.js"; import { getNote } from "@/server/api/common/getters.js"; @@ -43,21 +43,21 @@ export default define(meta, paramDef, async (ps, user) => { }); // if already favorited - const exist = await NoteFavorites.exist({ - where: { - noteId: note.id, - userId: user.id, - }, + const exists = await NoteFavorites.existsBy({ + noteId: note.id, + userId: user.id, }); - if (exist) { + if (exists) { throw new ApiError(meta.errors.alreadyFavorited); } + const now = new Date(); + // Create favorite await NoteFavorites.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, noteId: note.id, userId: user.id, }); diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index 476375dc0b..a56f4ae9d6 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -64,7 +64,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, user) => { - const m = await fetchMeta(true); + const m = await fetchMeta(); if (m.disableGlobalTimeline) { if (user == null || !(user.isAdmin || user.isModerator)) { throw new ApiError(meta.errors.gtlDisabled); diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index e6ab910040..3b5486ec8c 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -71,7 +71,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, user) => { - const m = await fetchMeta(true); + const m = await fetchMeta(); if (m.disableLocalTimeline && !user.isAdmin && !user.isModerator) { throw new ApiError(meta.errors.stlDisabled); } diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index 2a99c1236c..1d7453e67f 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -74,7 +74,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, user) => { - const m = await fetchMeta(true); + const m = await fetchMeta(); if (m.disableLocalTimeline) { if (user == null || !(user.isAdmin || user.isModerator)) { throw new ApiError(meta.errors.ltlDisabled); diff --git a/packages/backend/src/server/api/endpoints/notes/recommended-timeline.ts b/packages/backend/src/server/api/endpoints/notes/recommended-timeline.ts index 073a8f8569..a2aba37aa2 100644 --- a/packages/backend/src/server/api/endpoints/notes/recommended-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/recommended-timeline.ts @@ -74,7 +74,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, user) => { - const m = await fetchMeta(true); + const m = await fetchMeta(); if (m.disableRecommendedTimeline) { if (user == null || !(user.isAdmin || user.isModerator)) { throw new ApiError(meta.errors.rtlDisabled); diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts index 16304dd269..1f9a55588d 100644 --- a/packages/backend/src/server/api/endpoints/notes/renotes.ts +++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts @@ -46,7 +46,7 @@ export const paramDef = { type: "string", enum: ["all", "renote", "quote"], nullable: true, - default: null, + default: "all", }, }, required: ["noteId"], diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts index b666e05835..72b5ebbf48 100644 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts @@ -1,5 +1,5 @@ import { Notes, NoteThreadMutings } from "@/models/index.js"; -import { genId } from "backend-rs"; +import { genIdAt } from "backend-rs"; import readNote from "@/services/note/read.js"; import define from "@/server/api/define.js"; import { getNote } from "@/server/api/common/getters.js"; @@ -49,9 +49,11 @@ export default define(meta, paramDef, async (ps, user) => { await readNote(user.id, mutedNotes); + const now = new Date(); + await NoteThreadMutings.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, threadId: note.threadId || note.id, userId: user.id, }); diff --git a/packages/backend/src/server/api/endpoints/pages/create.ts b/packages/backend/src/server/api/endpoints/pages/create.ts index 4980c49806..4677352258 100644 --- a/packages/backend/src/server/api/endpoints/pages/create.ts +++ b/packages/backend/src/server/api/endpoints/pages/create.ts @@ -1,5 +1,5 @@ import { Pages, DriveFiles } from "@/models/index.js"; -import { genId, HOUR } from "backend-rs"; +import { genIdAt, HOUR } from "backend-rs"; import { Page } from "@/models/entities/page.js"; import define from "@/server/api/define.js"; import { ApiError } from "@/server/api/error.js"; @@ -97,11 +97,13 @@ export default define(meta, paramDef, async (ps, user) => { } }); + const now = new Date(); + const page = await Pages.insert( new Page({ - id: genId(), - createdAt: new Date(), - updatedAt: new Date(), + id: genIdAt(now), + createdAt: now, + updatedAt: now, title: ps.title, name: ps.name, summary: ps.summary, diff --git a/packages/backend/src/server/api/endpoints/pages/like.ts b/packages/backend/src/server/api/endpoints/pages/like.ts index 1480b47e0b..e8db1788db 100644 --- a/packages/backend/src/server/api/endpoints/pages/like.ts +++ b/packages/backend/src/server/api/endpoints/pages/like.ts @@ -1,5 +1,5 @@ import { Pages, PageLikes } from "@/models/index.js"; -import { genId } from "backend-rs"; +import { genIdAt } from "backend-rs"; import define from "@/server/api/define.js"; import { ApiError } from "@/server/api/error.js"; @@ -40,21 +40,21 @@ export default define(meta, paramDef, async (ps, user) => { } // if already liked - const exist = await PageLikes.exist({ - where: { - pageId: page.id, - userId: user.id, - }, + const exists = await PageLikes.existsBy({ + pageId: page.id, + userId: user.id, }); - if (exist) { + if (exists) { throw new ApiError(meta.errors.alreadyLiked); } + const now = new Date(); + // Create like await PageLikes.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, pageId: page.id, userId: user.id, }); diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts index 03b0b8871d..7762159f11 100644 --- a/packages/backend/src/server/api/endpoints/pinned-users.ts +++ b/packages/backend/src/server/api/endpoints/pinned-users.ts @@ -30,7 +30,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, me) => { - const meta = await fetchMeta(true); + const meta = await fetchMeta(); const users = await Promise.all( meta.pinnedUsers diff --git a/packages/backend/src/server/api/endpoints/promo/read.ts b/packages/backend/src/server/api/endpoints/promo/read.ts index 0509548b05..0190c5a9a0 100644 --- a/packages/backend/src/server/api/endpoints/promo/read.ts +++ b/packages/backend/src/server/api/endpoints/promo/read.ts @@ -1,5 +1,5 @@ import { PromoReads } from "@/models/index.js"; -import { genId } from "backend-rs"; +import { genIdAt } from "backend-rs"; import define from "@/server/api/define.js"; import { ApiError } from "@/server/api/error.js"; import { getNote } from "@/server/api/common/getters.js"; @@ -33,20 +33,20 @@ export default define(meta, paramDef, async (ps, user) => { throw err; }); - const exist = await PromoReads.exist({ - where: { - noteId: note.id, - userId: user.id, - }, + const exists = await PromoReads.existsBy({ + noteId: note.id, + userId: user.id, }); - if (exist) { + if (exists) { return; } + const now = new Date(); + await PromoReads.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, noteId: note.id, userId: user.id, }); diff --git a/packages/backend/src/server/api/endpoints/recommended-instances.ts b/packages/backend/src/server/api/endpoints/recommended-instances.ts index 5c5e267b2e..abdcb420e8 100644 --- a/packages/backend/src/server/api/endpoints/recommended-instances.ts +++ b/packages/backend/src/server/api/endpoints/recommended-instances.ts @@ -27,7 +27,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async () => { - const meta = await fetchMeta(true); + const meta = await fetchMeta(); const instances = await Promise.all(meta.recommendedInstances.map((x) => x)); return instances; }); diff --git a/packages/backend/src/server/api/endpoints/renote-mute/create.ts b/packages/backend/src/server/api/endpoints/renote-mute/create.ts index 46535cb39d..3b2c22a98a 100644 --- a/packages/backend/src/server/api/endpoints/renote-mute/create.ts +++ b/packages/backend/src/server/api/endpoints/renote-mute/create.ts @@ -1,6 +1,6 @@ -import { genId } from "backend-rs"; +import { genIdAt } from "backend-rs"; import { RenoteMutings } from "@/models/index.js"; -import { RenoteMuting } from "@/models/entities/renote-muting.js"; +import type { RenoteMuting } from "@/models/entities/renote-muting.js"; import define from "@/server/api/define.js"; import { ApiError } from "@/server/api/error.js"; import { getUser } from "@/server/api/common/getters.js"; @@ -47,21 +47,21 @@ export default define(meta, paramDef, async (ps, user) => { }); // Check if already muting - const exist = await RenoteMutings.exist({ - where: { - muterId: muter.id, - muteeId: mutee.id, - }, + const exists = await RenoteMutings.existsBy({ + muterId: muter.id, + muteeId: mutee.id, }); - if (exist) { + if (exists) { throw new ApiError(meta.errors.alreadyMuting); } + const now = new Date(); + // Create mute await RenoteMutings.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, muterId: muter.id, muteeId: mutee.id, } as RenoteMuting); diff --git a/packages/backend/src/server/api/endpoints/reply-mute/create.ts b/packages/backend/src/server/api/endpoints/reply-mute/create.ts index fc8644a7e1..2d9f260b28 100644 --- a/packages/backend/src/server/api/endpoints/reply-mute/create.ts +++ b/packages/backend/src/server/api/endpoints/reply-mute/create.ts @@ -1,6 +1,6 @@ -import { genId } from "backend-rs"; +import { genIdAt } from "backend-rs"; import { ReplyMutings } from "@/models/index.js"; -import { ReplyMuting } from "@/models/entities/reply-muting.js"; +import type { ReplyMuting } from "@/models/entities/reply-muting.js"; import define from "@/server/api/define.js"; import { ApiError } from "@/server/api/error.js"; import { getUser } from "@/server/api/common/getters.js"; @@ -47,19 +47,21 @@ export default define(meta, paramDef, async (ps, user) => { }); // Check if already muting - const exist = await ReplyMutings.findOneBy({ + const exists = await ReplyMutings.existsBy({ muterId: muter.id, muteeId: mutee.id, }); - if (exist != null) { + if (exists) { throw new ApiError(meta.errors.alreadyMuting); } + const now = new Date(); + // Create mute await ReplyMutings.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, muterId: muter.id, muteeId: mutee.id, } as ReplyMuting); diff --git a/packages/backend/src/server/api/endpoints/reply-mute/delete.ts b/packages/backend/src/server/api/endpoints/reply-mute/delete.ts index f659a17bfc..152d3fca60 100644 --- a/packages/backend/src/server/api/endpoints/reply-mute/delete.ts +++ b/packages/backend/src/server/api/endpoints/reply-mute/delete.ts @@ -45,18 +45,18 @@ export default define(meta, paramDef, async (ps, user) => { }); // Check not muting - const exist = await ReplyMutings.findOneBy({ + const record = await ReplyMutings.findOneBy({ muterId: muter.id, muteeId: mutee.id, }); - if (exist == null) { + if (record == null) { throw new ApiError(meta.errors.notMuting); } // Delete mute await ReplyMutings.delete({ - id: exist.id, + id: record.id, }); // publishUserEvent(user.id, "unmute", mutee); diff --git a/packages/backend/src/server/api/endpoints/request-reset-password.ts b/packages/backend/src/server/api/endpoints/request-reset-password.ts index 58beb94a88..ed269de15f 100644 --- a/packages/backend/src/server/api/endpoints/request-reset-password.ts +++ b/packages/backend/src/server/api/endpoints/request-reset-password.ts @@ -3,7 +3,7 @@ import { IsNull } from "typeorm"; import { config } from "@/config.js"; import { Users, UserProfiles, PasswordResetRequests } from "@/models/index.js"; import { sendEmail } from "@/services/send-email.js"; -import { HOUR, genId } from "backend-rs"; +import { HOUR, genIdAt } from "backend-rs"; import define from "@/server/api/define.js"; export const meta = { @@ -54,10 +54,11 @@ export default define(meta, paramDef, async (ps) => { } const token = rndstr("a-z0-9", 64); + const now = new Date(); await PasswordResetRequests.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, userId: profile.userId, token, }); diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts index 8f35daa7f8..03faabe5a6 100644 --- a/packages/backend/src/server/api/endpoints/server-info.ts +++ b/packages/backend/src/server/api/endpoints/server-info.ts @@ -17,7 +17,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async () => { - const instanceMeta = await fetchMeta(true); + const instanceMeta = await fetchMeta(); if (!instanceMeta.enableServerMachineStats) { return { diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts index dd8c7500b3..2d696b1598 100644 --- a/packages/backend/src/server/api/endpoints/sw/register.ts +++ b/packages/backend/src/server/api/endpoints/sw/register.ts @@ -1,4 +1,4 @@ -import { fetchMeta, genId } from "backend-rs"; +import { fetchMeta, genIdAt } from "backend-rs"; import { SwSubscriptions } from "@/models/index.js"; import define from "@/server/api/define.js"; @@ -63,7 +63,7 @@ export default define(meta, paramDef, async (ps, me) => { publickey: ps.publickey, }); - const instance = await fetchMeta(false); + const instance = await fetchMeta(); // if already subscribed if (subscription != null) { @@ -76,9 +76,11 @@ export default define(meta, paramDef, async (ps, me) => { }; } + const now = new Date(); + await SwSubscriptions.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, userId: me.id, endpoint: ps.endpoint, auth: ps.auth, diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index ab1cac2442..26d80adcd5 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -103,11 +103,9 @@ export default define(meta, paramDef, async (ps, me) => { if (me == null) { throw new ApiError(meta.errors.forbidden); } else if (me.id !== user.id) { - const isFollowed = await Followings.exist({ - where: { - followeeId: user.id, - followerId: me.id, - }, + const isFollowed = await Followings.existsBy({ + followeeId: user.id, + followerId: me.id, }); if (!isFollowed) { throw new ApiError(meta.errors.nullFollowers); diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index 05ab3602b0..7d5d4d65a4 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -102,11 +102,9 @@ export default define(meta, paramDef, async (ps, me) => { if (me == null) { throw new ApiError(meta.errors.forbidden); } else if (me.id !== user.id) { - const isFollowing = await Followings.exist({ - where: { - followeeId: user.id, - followerId: me.id, - }, + const isFollowing = await Followings.existsBy({ + followeeId: user.id, + followerId: me.id, }); if (!isFollowing) { throw new ApiError(meta.errors.cannot_find); diff --git a/packages/backend/src/server/api/endpoints/users/groups/create.ts b/packages/backend/src/server/api/endpoints/users/groups/create.ts index e6ff909343..d1417ac36c 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/create.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/create.ts @@ -1,5 +1,5 @@ import { UserGroups, UserGroupJoinings } from "@/models/index.js"; -import { genId } from "backend-rs"; +import { genIdAt } from "backend-rs"; import type { UserGroup } from "@/models/entities/user-group.js"; import type { UserGroupJoining } from "@/models/entities/user-group-joining.js"; import define from "@/server/api/define.js"; @@ -30,17 +30,19 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, user) => { + const now = new Date(); + const userGroup = await UserGroups.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, userId: user.id, name: ps.name, } as UserGroup).then((x) => UserGroups.findOneByOrFail(x.identifiers[0])); // Push the owner await UserGroupJoinings.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, userId: user.id, userGroupId: userGroup.id, } as UserGroupJoining); diff --git a/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts b/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts index f86f415d7a..c86b59dd49 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts @@ -1,5 +1,5 @@ import { UserGroupJoinings, UserGroupInvitations } from "@/models/index.js"; -import { genId } from "backend-rs"; +import { genIdAt } from "backend-rs"; import type { UserGroupJoining } from "@/models/entities/user-group-joining.js"; import { ApiError } from "@/server/api/error.js"; import define from "@/server/api/define.js"; @@ -44,10 +44,12 @@ export default define(meta, paramDef, async (ps, user) => { throw new ApiError(meta.errors.noSuchInvitation); } + const now = new Date(); + // Push the user await UserGroupJoinings.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, userId: user.id, userGroupId: invitation.userGroupId, } as UserGroupJoining); diff --git a/packages/backend/src/server/api/endpoints/users/groups/invite.ts b/packages/backend/src/server/api/endpoints/users/groups/invite.ts index 62e13a4458..5aa90859e6 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/invite.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/invite.ts @@ -3,7 +3,7 @@ import { UserGroupJoinings, UserGroupInvitations, } from "@/models/index.js"; -import { genId } from "backend-rs"; +import { genIdAt } from "backend-rs"; import type { UserGroupInvitation } from "@/models/entities/user-group-invitation.js"; import { createNotification } from "@/services/create-notification.js"; import { getUser } from "@/server/api/common/getters.js"; @@ -91,9 +91,11 @@ export default define(meta, paramDef, async (ps, me) => { throw new ApiError(meta.errors.alreadyInvited); } + const now = new Date(); + const invitation = await UserGroupInvitations.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, userId: user.id, userGroupId: userGroup.id, } as UserGroupInvitation).then((x) => diff --git a/packages/backend/src/server/api/endpoints/users/lists/create.ts b/packages/backend/src/server/api/endpoints/users/lists/create.ts index fa10bf24ce..2849ea0770 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/create.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/create.ts @@ -1,5 +1,5 @@ import { UserLists } from "@/models/index.js"; -import { genId } from "backend-rs"; +import { genIdAt } from "backend-rs"; import type { UserList } from "@/models/entities/user-list.js"; import define from "@/server/api/define.js"; @@ -29,9 +29,10 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, user) => { + const now = new Date(); const userList = await UserLists.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, userId: user.id, name: ps.name, } as UserList).then((x) => UserLists.findOneByOrFail(x.identifiers[0])); diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts index 8dd4442d00..9efb3b5982 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/push.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts @@ -70,25 +70,21 @@ export default define(meta, paramDef, async (ps, me) => { // Check blocking if (user.id !== me.id) { - const isBlocked = await Blockings.exist({ - where: { - blockerId: user.id, - blockeeId: me.id, - }, + const isBlocked = await Blockings.existsBy({ + blockerId: user.id, + blockeeId: me.id, }); if (isBlocked) { throw new ApiError(meta.errors.youHaveBeenBlocked); } } - const exist = await UserListJoinings.exist({ - where: { - userListId: ps.listId, - userId: user.id, - }, + const exists = await UserListJoinings.existsBy({ + userListId: ps.listId, + userId: user.id, }); - if (exist) { + if (exists) { throw new ApiError(meta.errors.alreadyAdded); } diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts index 8a134cf9e7..27a185bc08 100644 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -1,7 +1,7 @@ import * as mfm from "mfm-js"; import sanitizeHtml from "sanitize-html"; import { AbuseUserReports, UserProfiles, Users } from "@/models/index.js"; -import { genId, publishToModerationStream } from "backend-rs"; +import { genIdAt, publishToModerationStream } from "backend-rs"; import { sendEmail } from "@/services/send-email.js"; import { getUser } from "@/server/api/common/getters.js"; import { ApiError } from "@/server/api/error.js"; @@ -61,9 +61,11 @@ export default define(meta, paramDef, async (ps, me) => { throw new ApiError(meta.errors.cannotReportAdmin); } + const now = new Date(); + const report = await AbuseUserReports.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, targetUserId: user.id, targetUserHost: user.host, reporterId: me.id, diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index 61812a0189..199a9b7556 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -87,14 +87,20 @@ export const paramDef = { export default define(meta, paramDef, async (ps, me) => { let user; - const isAdminOrModerator = me && (me.isAdmin || me.isModerator); + const isAdminOrModerator = me != null && (me.isAdmin || me.isModerator); if (ps.userIds) { if (ps.userIds.length === 0) { return []; } - const isUrl = ps.userIds[0].startsWith("http"); + let isUrl = false; + + try { + const url = new URL(ps.userIds[0]); + isUrl = ["http", "https"].includes(url.protocol); + } catch (_) {} + let users: User[]; if (isUrl) { users = await Users.findBy( diff --git a/packages/backend/src/server/api/mastodon/endpoints/meta.ts b/packages/backend/src/server/api/mastodon/endpoints/meta.ts index 69a1822892..0c643202d6 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/meta.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/meta.ts @@ -9,7 +9,7 @@ export async function getInstance( contact: Entity.Account, ) { const [meta, totalUsers, totalStatuses] = await Promise.all([ - fetchMeta(true), + fetchMeta(), Users.count({ where: { host: IsNull() } }), Notes.count({ where: { userHost: IsNull() } }), ]); diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts index 6fa70717e7..e459616827 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/status.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts @@ -211,7 +211,7 @@ export function apiStatusMastodon(router: Router): void { router.post<{ Params: { id: string } }>( "/v1/statuses/:id/favourite", async (ctx) => { - const meta = await fetchMeta(true); + const meta = await fetchMeta(); const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); @@ -233,7 +233,7 @@ export function apiStatusMastodon(router: Router): void { router.post<{ Params: { id: string } }>( "/v1/statuses/:id/unfavourite", async (ctx) => { - const meta = await fetchMeta(true); + const meta = await fetchMeta(); const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts index 79dc48a8bb..84991156e2 100644 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ b/packages/backend/src/server/api/openapi/gen-spec.ts @@ -101,7 +101,7 @@ export function genOpenapiSpec() { } const info = { - operationId: endpoint.name, + operationId: `POST-${endpoint.name}`, summary: endpoint.name, description: desc, externalDocs: { @@ -208,11 +208,11 @@ export function genOpenapiSpec() { }, }; - const path = { + const path: Record = { post: info, }; if (endpoint.meta.allowGet) { - path.get = { ...info }; + path.get = { ...info, operationId: `GET-${endpoint.name}` }; // API Key authentication is not permitted for GET requests path.get.security = path.get.security.filter( (elem) => !Object.prototype.hasOwnProperty.call(elem, "ApiKeyAuth"), diff --git a/packages/backend/src/server/api/private/signin.ts b/packages/backend/src/server/api/private/signin.ts index 1ed11bbbc2..e124977cf6 100644 --- a/packages/backend/src/server/api/private/signin.ts +++ b/packages/backend/src/server/api/private/signin.ts @@ -11,7 +11,7 @@ import { } from "@/models/index.js"; import type { ILocalUser } from "@/models/entities/user.js"; import { - genId, + genIdAt, hashPassword, isOldPasswordAlgorithm, verifyPassword, @@ -27,9 +27,9 @@ export default async (ctx: Koa.Context) => { ctx.set("Access-Control-Allow-Credentials", "true"); const body = ctx.request.body as any; - const username = body["username"]; - const password = body["password"]; - const token = body["token"]; + const username = body.username; + const password = body.password; + const token = body.token; function error(status: number, error: { id: string }) { ctx.status = status; @@ -101,10 +101,12 @@ export default async (ctx: Koa.Context) => { } async function fail(status?: number, failure?: { id: string }) { + const now = new Date(); + // Append signin history await Signins.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, userId: user.id, ip: ctx.ip, headers: ctx.headers, @@ -251,13 +253,14 @@ export default async (ctx: Koa.Context) => { .replace(/\+/g, "-") .replace(/\//g, "_"); - const challengeId = genId(); + const now = new Date(); + const challengeId = genIdAt(now); await AttestationChallenges.insert({ userId: user.id, id: challengeId, challenge: hash(Buffer.from(challenge, "utf-8")).toString("hex"), - createdAt: new Date(), + createdAt: now, registrationChallenge: false, }); diff --git a/packages/backend/src/server/api/private/signup.ts b/packages/backend/src/server/api/private/signup.ts index 4dd4ffb231..b8a4c32138 100644 --- a/packages/backend/src/server/api/private/signup.ts +++ b/packages/backend/src/server/api/private/signup.ts @@ -5,13 +5,13 @@ import { Users, RegistrationTickets, UserPendings } from "@/models/index.js"; import { signup } from "@/server/api/common/signup.js"; import { config } from "@/config.js"; import { sendEmail } from "@/services/send-email.js"; -import { fetchMeta, genId, hashPassword } from "backend-rs"; +import { fetchMeta, genIdAt, hashPassword } from "backend-rs"; import { validateEmailForAccount } from "@/services/validate-email-for-account.js"; export default async (ctx: Koa.Context) => { const body = ctx.request.body; - const instance = await fetchMeta(false); + const instance = await fetchMeta(); // Verify *Captcha // ただしテスト時はこの機構は障害となるため無効にする @@ -84,9 +84,11 @@ export default async (ctx: Koa.Context) => { // Generate hash of password const hash = hashPassword(password); + const now = new Date(); + await UserPendings.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, code, email: emailAddress, username: username, diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index 1760d5abf7..be6de739b3 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -16,7 +16,7 @@ export default class extends Channel { } public async init(params: any) { - const meta = await fetchMeta(true); + const meta = await fetchMeta(); if (meta.disableGlobalTimeline) { if (this.user == null || !(this.user.isAdmin || this.user.isModerator)) return; diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index 5100a48efd..c0224e5661 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -16,7 +16,7 @@ export default class extends Channel { } public async init(params: any) { - const meta = await fetchMeta(true); + const meta = await fetchMeta(); if ( meta.disableLocalTimeline && !this.user!.isAdmin && diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index 2c9a38d677..6d750d67d7 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -15,7 +15,7 @@ export default class extends Channel { } public async init(params: any) { - const meta = await fetchMeta(true); + const meta = await fetchMeta(); if (meta.disableLocalTimeline) { if (this.user == null || !(this.user.isAdmin || this.user.isModerator)) return; diff --git a/packages/backend/src/server/api/stream/channels/recommended-timeline.ts b/packages/backend/src/server/api/stream/channels/recommended-timeline.ts index 5d0d6fc602..b42f936a01 100644 --- a/packages/backend/src/server/api/stream/channels/recommended-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/recommended-timeline.ts @@ -16,7 +16,7 @@ export default class extends Channel { } public async init(params: any) { - const meta = await fetchMeta(true); + const meta = await fetchMeta(); if ( meta.disableRecommendedTimeline && !this.user!.isAdmin && @@ -36,7 +36,7 @@ export default class extends Channel { // チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または // チャンネルの投稿ではなく、全体公開のローカルの投稿 または // フォローしているチャンネルの投稿 の場合だけ - const meta = await fetchMeta(true); + const meta = await fetchMeta(); if ( !( note.user.host != null && diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts index 2722a6610a..d8d2fbb378 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -22,13 +22,11 @@ export default class extends Channel { this.listId = params.listId as string; // Check existence and owner - const exist = await UserLists.exists({ - where: { - id: this.listId, - userId: this.user!.id, - }, + const exists = await UserLists.existsBy({ + id: this.listId, + userId: this.user!.id, }); - if (!exist) return; + if (!exists) return; // Subscribe stream this.subscriber.on(`userListStream:${this.listId}`, this.send); diff --git a/packages/backend/src/server/index.ts b/packages/backend/src/server/index.ts index b4c015bb5b..ba67c5a486 100644 --- a/packages/backend/src/server/index.ts +++ b/packages/backend/src/server/index.ts @@ -10,10 +10,9 @@ import Router from "@koa/router"; import cors from "@koa/cors"; import mount from "koa-mount"; import koaLogger from "koa-logger"; -import * as slow from "koa-slow"; import { IsNull } from "typeorm"; -import { config, envOption } from "@/config.js"; +import { config } from "@/config.js"; import Logger from "@/services/logger.js"; import { Users } from "@/models/index.js"; import { fetchMeta, stringToAcct } from "backend-rs"; @@ -54,15 +53,6 @@ if (!["production", "test"].includes(process.env.NODE_ENV || "")) { serverLogger.debug(str); }), ); - - // Delay - if (envOption.slow) { - app.use( - slow({ - delay: 3000, - }), - ); - } } // HSTS @@ -124,7 +114,7 @@ router.get("/avatar/@:acct", async (ctx) => { }); router.get("/identicon/:x", async (ctx) => { - const meta = await fetchMeta(true); + const meta = await fetchMeta(); if (meta.enableIdenticonGeneration) { const [temp, cleanup] = await createTemp(); await genIdenticon(ctx.params.x, fs.createWriteStream(temp)); diff --git a/packages/backend/src/server/web/feed.ts b/packages/backend/src/server/web/feed.ts index 3beffc82f0..efdda8c1cd 100644 --- a/packages/backend/src/server/web/feed.ts +++ b/packages/backend/src/server/web/feed.ts @@ -5,6 +5,7 @@ import type { User } from "@/models/entities/user.js"; import type { Note } from "@/models/entities/note.js"; import { Notes, DriveFiles, UserProfiles, Users } from "@/models/index.js"; import getNoteHtml from "@/remote/activitypub/misc/get-note-html.js"; +import { isQuote, getNoteSummary } from "backend-rs"; /** * If there is this part in the note, it will cause CDATA to be terminated early. @@ -17,7 +18,7 @@ export default async function ( user: User, threadDepth = 5, history = 20, - noteintitle = false, + noteintitle = true, renotes = true, replies = true, ) { @@ -81,20 +82,26 @@ export default async function ( depth -= 1; } - let title = `${author.name} `; - if (note.renoteId) { - title += "renotes"; - } else if (note.replyId) { - title += "replies"; - } else { - title += "says"; - } + let title = `Post by ${author.name}`; + if (noteintitle) { - const content = note.cw ?? note.text; + if (note.renoteId) { + title = `Boost by ${author.name}`; + } else if (note.replyId) { + title = `Reply by ${author.name}`; + } else { + title = `Post by ${author.name}`; + } + const effectiveNote = + !isQuote(note) && note.renote != null ? note.renote : note; + const content = getNoteSummary( + effectiveNote.fileIds, + effectiveNote.text, + effectiveNote.cw, + effectiveNote.hasPoll, + ); if (content) { title += `: ${content}`; - } else { - title += "something"; } } diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts index 51f94842ea..af6e237be6 100644 --- a/packages/backend/src/server/web/index.ts +++ b/packages/backend/src/server/web/index.ts @@ -334,7 +334,7 @@ const getFeed = async ( noRenotes: string, noReplies: string, ) => { - const meta = await fetchMeta(true); + const meta = await fetchMeta(); if (meta.privateMode) { return; } @@ -482,7 +482,7 @@ const userPage: Router.Middleware = async (ctx, next) => { } const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - const meta = await fetchMeta(true); + const meta = await fetchMeta(); const me = profile.fields ? profile.fields .filter((filed) => filed.value?.match(/^https?:/)) @@ -531,7 +531,7 @@ router.get("/notes/:note", async (ctx, next) => { const profile = await UserProfiles.findOneByOrFail({ userId: note.userId, }); - const meta = await fetchMeta(true); + const meta = await fetchMeta(); await ctx.render("note", { ...metaToPugArgs(meta), note: packedNote, @@ -540,13 +540,13 @@ router.get("/notes/:note", async (ctx, next) => { await Users.findOneByOrFail({ id: note.userId }), ), // TODO: Let locale changeable by instance setting - summary: getNoteSummary(note), + summary: getNoteSummary(note.fileIds, note.text, note.cw, note.hasPoll), }); ctx.set("Cache-Control", "public, max-age=15"); ctx.set( "Content-Security-Policy", - "default-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src *; font-src 'self' data:; img-src *; media-src *; worker-src 'self'; frame-ancestors *", + "default-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src *; font-src 'self' data:; img-src * data:; media-src *; worker-src 'self'; frame-ancestors *", ); return; @@ -562,19 +562,19 @@ router.get("/posts/:note", async (ctx, next) => { visibility: In(["public", "home"]), }); - if (note) { - const _note = await Notes.pack(note); + if (note != null) { + const packedNote = await Notes.pack(note); const profile = await UserProfiles.findOneByOrFail({ userId: note.userId }); - const meta = await fetchMeta(true); + const meta = await fetchMeta(); await ctx.render("note", { ...metaToPugArgs(meta), - note: _note, + note: packedNote, profile, avatarUrl: await Users.getAvatarUrl( await Users.findOneByOrFail({ id: note.userId }), ), // TODO: Let locale changeable by instance setting - summary: getNoteSummary(_note), + summary: getNoteSummary(note.fileIds, note.text, note.cw, note.hasPoll), }); ctx.set("Cache-Control", "public, max-age=15"); @@ -603,7 +603,7 @@ router.get("/@:user/pages/:page", async (ctx, next) => { if (page) { const _page = await Pages.pack(page); const profile = await UserProfiles.findOneByOrFail({ userId: page.userId }); - const meta = await fetchMeta(true); + const meta = await fetchMeta(); await ctx.render("page", { ...metaToPugArgs(meta), page: _page, @@ -635,7 +635,7 @@ router.get("/clips/:clip", async (ctx, next) => { if (clip) { const _clip = await Clips.pack(clip); const profile = await UserProfiles.findOneByOrFail({ userId: clip.userId }); - const meta = await fetchMeta(true); + const meta = await fetchMeta(); await ctx.render("clip", { ...metaToPugArgs(meta), clip: _clip, @@ -660,7 +660,7 @@ router.get("/gallery/:post", async (ctx, next) => { if (post) { const _post = await GalleryPosts.pack(post); const profile = await UserProfiles.findOneByOrFail({ userId: post.userId }); - const meta = await fetchMeta(true); + const meta = await fetchMeta(); await ctx.render("gallery-post", { ...metaToPugArgs(meta), post: _post, @@ -686,7 +686,7 @@ router.get("/channels/:channel", async (ctx, next) => { if (channel) { const _channel = await Channels.pack(channel); - const meta = await fetchMeta(true); + const meta = await fetchMeta(); await ctx.render("channel", { ...metaToPugArgs(meta), channel: _channel, @@ -739,7 +739,7 @@ router.get("/api/v1/streaming", async (ctx) => { // Render base html for all requests router.get("(.*)", async (ctx) => { - const meta = await fetchMeta(true); + const meta = await fetchMeta(); await ctx.render("base", { ...metaToPugArgs(meta), diff --git a/packages/backend/src/server/web/manifest.ts b/packages/backend/src/server/web/manifest.ts index 801e5f047e..cad253bee4 100644 --- a/packages/backend/src/server/web/manifest.ts +++ b/packages/backend/src/server/web/manifest.ts @@ -77,7 +77,7 @@ const manifest = { }; export const manifestHandler = async (ctx: Koa.Context) => { - const instance = await fetchMeta(true); + const instance = await fetchMeta(); manifest.short_name = instance.name || "Firefish"; manifest.name = instance.name || "Firefish"; diff --git a/packages/backend/src/server/web/url-preview.ts b/packages/backend/src/server/web/url-preview.ts index f4ad758dc5..e164f41518 100644 --- a/packages/backend/src/server/web/url-preview.ts +++ b/packages/backend/src/server/web/url-preview.ts @@ -22,7 +22,7 @@ export const urlPreviewHandler = async (ctx: Koa.Context) => { return; } - const meta = await fetchMeta(true); + const meta = await fetchMeta(); logger.info( meta.summalyProxy diff --git a/packages/backend/src/services/blocking/create.ts b/packages/backend/src/services/blocking/create.ts index 2e5766c45b..b5e23c8277 100644 --- a/packages/backend/src/services/blocking/create.ts +++ b/packages/backend/src/services/blocking/create.ts @@ -15,7 +15,7 @@ import { UserListJoinings, UserLists, } from "@/models/index.js"; -import { genId } from "backend-rs"; +import { genIdAt } from "backend-rs"; import { getActiveWebhooks } from "@/misc/webhook-cache.js"; import { webhookDeliver } from "@/queue/index.js"; @@ -28,9 +28,11 @@ export default async function (blocker: User, blockee: User) { removeFromList(blockee, blocker), ]); + const now = new Date(); + const blocking = { - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, blocker, blockerId: blocker.id, blockee, diff --git a/packages/backend/src/services/create-notification.ts b/packages/backend/src/services/create-notification.ts index 335ba561e7..4a633dff2e 100644 --- a/packages/backend/src/services/create-notification.ts +++ b/packages/backend/src/services/create-notification.ts @@ -8,7 +8,7 @@ import { Followings, } from "@/models/index.js"; import { - genId, + genIdAt, isSilencedServer, sendPushNotification, PushNotificationKind, @@ -39,8 +39,9 @@ export async function createNotification( (notifier.isSilenced || (Users.isRemoteUser(notifier) && (await isSilencedServer(notifier.host)))) && - !(await Followings.exists({ - where: { followerId: notifieeId, followeeId: data.notifierId }, + !(await Followings.existsBy({ + followerId: notifieeId, + followeeId: data.notifierId, })) ) return null; @@ -61,10 +62,12 @@ export async function createNotification( } } + const now = new Date(); + // Create notification const notification = await Notifications.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, notifieeId: notifieeId, type: type, // 相手がこの通知をミュートしているようなら、既読を予めつけておく diff --git a/packages/backend/src/services/create-system-user.ts b/packages/backend/src/services/create-system-user.ts index d13d5a7bda..0aabf7cc83 100644 --- a/packages/backend/src/services/create-system-user.ts +++ b/packages/backend/src/services/create-system-user.ts @@ -2,8 +2,9 @@ import { v4 as uuid } from "uuid"; import { genRsaKeyPair } from "@/misc/gen-key-pair.js"; import { User } from "@/models/entities/user.js"; import { UserProfile } from "@/models/entities/user-profile.js"; +import { Users } from "@/models/index.js"; import { IsNull } from "typeorm"; -import { generateUserToken, genId, hashPassword } from "backend-rs"; +import { generateUserToken, genIdAt, hashPassword } 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"; @@ -21,19 +22,23 @@ export async function createSystemUser(username: string) { let account!: User; + const exists = await Users.existsBy({ + usernameLower: username.toLowerCase(), + host: IsNull(), + }); + + if (exists) { + throw new Error("the user already exists"); + } + + const now = new Date(); + // Start transaction await db.transaction(async (transactionalEntityManager) => { - const exist = await transactionalEntityManager.findOneBy(User, { - usernameLower: username.toLowerCase(), - host: IsNull(), - }); - - if (exist) throw new Error("the user is already exists"); - account = await transactionalEntityManager .insert(User, { - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, username: username, usernameLower: username.toLowerCase(), host: null, @@ -60,7 +65,7 @@ export async function createSystemUser(username: string) { }); await transactionalEntityManager.insert(UsedUsername, { - createdAt: new Date(), + createdAt: now, username: username.toLowerCase(), }); }); diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts index 390fba1a37..cf84ebd8f4 100644 --- a/packages/backend/src/services/drive/add-file.ts +++ b/packages/backend/src/services/drive/add-file.ts @@ -5,8 +5,14 @@ import { v4 as uuid } from "uuid"; import type S3 from "aws-sdk/clients/s3.js"; // TODO: migrate to SDK v3 import sharp from "sharp"; import { IsNull } from "typeorm"; -import { publishMainStream, publishDriveStream } from "@/services/stream.js"; -import { FILE_TYPE_BROWSERSAFE, fetchMeta, genId } from "backend-rs"; +import { publishMainStream } from "@/services/stream.js"; +import { + DriveFileEvent, + FILE_TYPE_BROWSERSAFE, + fetchMeta, + genId, + publishToDriveFileStream, +} from "backend-rs"; import { contentDisposition } from "@/misc/content-disposition.js"; import { getFileInfo } from "@/misc/get-file-info.js"; import { @@ -28,6 +34,7 @@ import { driveLogger } from "./logger.js"; import { GenerateVideoThumbnail } from "./generate-video-thumbnail.js"; import { deleteFile } from "./delete-file.js"; import { inspect } from "node:util"; +import { toRustObject } from "@/prelude/undefined-to-null.js"; const logger = driveLogger.createSubLogger("register", "yellow"); @@ -78,7 +85,7 @@ async function save( // thunbnail, webpublic を必要なら生成 const alts = await generateAlts(path, type, !file.uri); - const meta = await fetchMeta(true); + const meta = await fetchMeta(); if (meta.useObjectStorage) { //#region ObjectStorage params @@ -363,7 +370,7 @@ async function upload( if (type === "image/apng") type = "image/png"; if (!FILE_TYPE_BROWSERSAFE.includes(type)) type = "application/octet-stream"; - const meta = await fetchMeta(true); + const meta = await fetchMeta(); const params = { Bucket: meta.objectStorageBucket, @@ -505,7 +512,7 @@ export async function addFile({ const usage = await DriveFiles.calcDriveUsageOf(user); const u = await Users.findOneBy({ id: user.id }); - const instance = await fetchMeta(true); + const instance = await fetchMeta(); let driveCapacity = 1024 * 1024 * @@ -577,7 +584,7 @@ export async function addFile({ : null; const folder = await fetchFolder(); - const instance = await fetchMeta(true); + const instance = await fetchMeta(); let file = new DriveFile(); file.id = genId(); @@ -656,11 +663,15 @@ export async function addFile({ logger.info(`drive file has been created ${file.id}`); - if (user) { + if (user != null) { DriveFiles.pack(file, { self: true }).then((packedFile) => { // Publish driveFileCreated event publishMainStream(user.id, "driveFileCreated", packedFile); - publishDriveStream(user.id, "fileCreated", packedFile); + publishToDriveFileStream( + user.id, + DriveFileEvent.Create, + toRustObject(file), + ); }); } diff --git a/packages/backend/src/services/drive/delete-file.ts b/packages/backend/src/services/drive/delete-file.ts index b4b5580a1c..94371f1b0f 100644 --- a/packages/backend/src/services/drive/delete-file.ts +++ b/packages/backend/src/services/drive/delete-file.ts @@ -82,7 +82,7 @@ async function postProcess(file: DriveFile, isExpired = false) { } export async function deleteObjectStorageFile(key: string) { - const meta = await fetchMeta(true); + const meta = await fetchMeta(); const s3 = getS3(meta); diff --git a/packages/backend/src/services/following/create.ts b/packages/backend/src/services/following/create.ts index 49852ee9ef..df9ab8ab2f 100644 --- a/packages/backend/src/services/following/create.ts +++ b/packages/backend/src/services/following/create.ts @@ -17,7 +17,7 @@ import { Instances, UserProfiles, } from "@/models/index.js"; -import { genId, isSilencedServer } from "backend-rs"; +import { genIdAt, isSilencedServer } from "backend-rs"; import { createNotification } from "@/services/create-notification.js"; import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js"; import type { Packed } from "@/misc/schema.js"; @@ -46,9 +46,11 @@ export async function insertFollowingDoc( let alreadyFollowed = false; + const now = new Date(); + await Followings.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, followerId: follower.id, followeeId: followee.id, diff --git a/packages/backend/src/services/following/requests/create.ts b/packages/backend/src/services/following/requests/create.ts index 146e20efd9..e4f093dfc1 100644 --- a/packages/backend/src/services/following/requests/create.ts +++ b/packages/backend/src/services/following/requests/create.ts @@ -4,7 +4,7 @@ import renderFollow from "@/remote/activitypub/renderer/follow.js"; import { deliver } from "@/queue/index.js"; import type { User } from "@/models/entities/user.js"; import { Blockings, FollowRequests, Users } from "@/models/index.js"; -import { genId } from "backend-rs"; +import { genIdAt } from "backend-rs"; import { createNotification } from "@/services/create-notification.js"; import { config } from "@/config.js"; @@ -42,9 +42,11 @@ export default async function ( if (blocking) throw new Error("blocking"); if (blocked) throw new Error("blocked"); + const now = new Date(); + const followRequest = await FollowRequests.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, followerId: follower.id, followeeId: followee.id, requestId, diff --git a/packages/backend/src/services/i/pin.ts b/packages/backend/src/services/i/pin.ts index b44ba5f9fa..42fc09b406 100644 --- a/packages/backend/src/services/i/pin.ts +++ b/packages/backend/src/services/i/pin.ts @@ -7,7 +7,7 @@ import type { User } from "@/models/entities/user.js"; import type { Note } from "@/models/entities/note.js"; import { Notes, UserNotePinings, Users } from "@/models/index.js"; import type { UserNotePining } from "@/models/entities/user-note-pining.js"; -import { genId } from "backend-rs"; +import { genIdAt } from "backend-rs"; import { deliverToFollowers } from "@/remote/activitypub/deliver-manager.js"; import { deliverToRelays } from "@/services/relay.js"; @@ -49,9 +49,11 @@ export async function addPinned( ); } + const now = new Date(); + await UserNotePinings.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, userId: user.id, noteId: note.id, } as UserNotePining); diff --git a/packages/backend/src/services/insert-moderation-log.ts b/packages/backend/src/services/insert-moderation-log.ts index 9c76cf45ac..6fcaeda146 100644 --- a/packages/backend/src/services/insert-moderation-log.ts +++ b/packages/backend/src/services/insert-moderation-log.ts @@ -1,5 +1,5 @@ import { ModerationLogs } from "@/models/index.js"; -import { genId } from "backend-rs"; +import { genIdAt } from "backend-rs"; import type { User } from "@/models/entities/user.js"; export async function insertModerationLog( @@ -7,9 +7,10 @@ export async function insertModerationLog( type: string, info?: Record, ) { + const now = new Date(); await ModerationLogs.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, userId: moderator.id, type: type, info: info || {}, diff --git a/packages/backend/src/services/logger.ts b/packages/backend/src/services/logger.ts index 2f1f552d03..f7843eac18 100644 --- a/packages/backend/src/services/logger.ts +++ b/packages/backend/src/services/logger.ts @@ -2,7 +2,7 @@ import cluster from "node:cluster"; import chalk from "chalk"; import { default as convertColor } from "color-convert"; import { format as dateFormat } from "date-fns"; -import { config, envOption } from "@/config.js"; +import { config } from "@/config.js"; import * as SyslogPro from "syslog-pro"; @@ -101,16 +101,16 @@ export default class Logger { const l = level === "error" ? important - ? chalk.bgRed.white("ERR ") - : chalk.red("ERR ") + ? chalk.bgRed.white("ERROR") + : chalk.red("ERROR") : level === "warning" - ? chalk.yellow("WARN") + ? chalk.yellow(" WARN") : level === "info" - ? chalk.cyan("INFO") + ? chalk.green(" INFO") : level === "debug" - ? chalk.green("DEBUG") + ? chalk.blue("DEBUG") : level === "trace" - ? chalk.gray("TRACE") + ? chalk.magenta("TRACE") : null; const domains = [this.domain] .concat(subDomains) @@ -133,7 +133,6 @@ export default class Logger { : null; let log = `${l} ${worker}\t[${domains.join(" ")}]\t${m}`; - if (envOption.withLogTime) log = `${chalk.gray(time)} ${log}`; console.log(important ? chalk.bold(log) : log); @@ -212,8 +211,7 @@ export default class Logger { // Fixed if statement is ignored when logLevel includes debug if ( config.logLevel?.includes("debug") || - process.env.NODE_ENV !== "production" || - envOption.verbose + process.env.NODE_ENV !== "production" ) { this.log("debug", message, data, important); } diff --git a/packages/backend/src/services/messages/create.ts b/packages/backend/src/services/messages/create.ts index 709e9bc235..f1f26065bd 100644 --- a/packages/backend/src/services/messages/create.ts +++ b/packages/backend/src/services/messages/create.ts @@ -8,7 +8,7 @@ import { Users, } from "@/models/index.js"; import { - genId, + genIdAt, sendPushNotification, publishToChatStream, publishToGroupChatStream, @@ -36,9 +36,10 @@ export async function createMessage( file: DriveFile | null, uri?: string, ) { + const now = new Date(); const message = { - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, fileId: file ? file.id : null, recipientId: recipientUser ? recipientUser.id : null, groupId: recipientGroup ? recipientGroup.id : null, diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index 8fe44a60ba..03ca4f7578 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -1,9 +1,5 @@ import * as mfm from "mfm-js"; -import { - publishMainStream, - publishNotesStream, - publishNoteStream, -} from "@/services/stream.js"; +import { publishMainStream, publishNoteStream } from "@/services/stream.js"; import DeliverManager from "@/remote/activitypub/deliver-manager.js"; import renderNote from "@/remote/activitypub/renderer/note.js"; import renderCreate from "@/remote/activitypub/renderer/create.js"; @@ -49,6 +45,7 @@ import { genIdAt, isQuote, isSilencedServer, + publishToNotesStream, } from "backend-rs"; import { countSameRenotes } from "@/misc/count-same-renotes.js"; import { deliverToRelays, getCachedRelays } from "../relay.js"; @@ -133,15 +130,10 @@ class NotificationManager { } } -type MinimumUser = { - id: User["id"]; - host: User["host"]; - username: User["username"]; - uri: User["uri"]; -}; - -type Option = { +type UserLike = Pick; +type NoteLike = { createdAt?: Date | null; + scheduledAt?: Date | null; name?: string | null; text?: string | null; lang?: string | null; @@ -152,9 +144,9 @@ type Option = { localOnly?: boolean | null; cw?: string | null; visibility?: string; - visibleUsers?: MinimumUser[] | null; + visibleUsers?: UserLike[] | null; channel?: Channel | null; - apMentions?: MinimumUser[] | null; + apMentions?: UserLike[] | null; apHashtags?: string[] | null; apEmojis?: string[] | null; uri?: string | null; @@ -163,16 +155,11 @@ type Option = { }; export default async ( - user: { - id: User["id"]; - username: User["username"]; - host: User["host"]; - isSilenced: User["isSilenced"]; - createdAt: User["createdAt"]; - isBot: User["isBot"]; - inbox?: User["inbox"]; - }, - data: Option, + user: Pick< + User, + "id" | "username" | "host" | "isSilenced" | "createdAt" | "isBot" + > & { inbox?: User["inbox"] }, + data: NoteLike, silent = false, waitToPublish?: (note: Note) => Promise, ) => @@ -181,6 +168,9 @@ export default async ( const dontFederateInitially = data.visibility?.startsWith("hidden") === true; + // Whether this is a scheduled "draft" post (yet to be published) + const isDraft = data.scheduledAt != null; + // If you reply outside the channel, match the scope of the target. // TODO (I think it's a process that could be done on the client side, but it's server side for now.) if ( @@ -208,6 +198,7 @@ export default async ( data.createdAt > now ) data.createdAt = now; + if (data.visibility == null) data.visibility = "public"; if (data.localOnly == null) data.localOnly = false; if (data.channel != null) data.visibility = "public"; @@ -277,13 +268,9 @@ export default async ( data.localOnly = true; } - if (data.text) { - data.text = data.text.trim(); - } else { - data.text = null; - } + data.text = data.text?.trim() ?? null; - if (data.lang) { + if (data.lang != null) { if (!Object.keys(langmap).includes(data.lang.toLowerCase())) throw new Error("invalid param"); data.lang = data.lang.toLowerCase(); @@ -297,10 +284,10 @@ export default async ( // Parse MFM if needed if (!(tags && emojis && mentionedUsers)) { - const tokens = data.text ? mfm.parse(data.text)! : []; - const cwTokens = data.cw ? mfm.parse(data.cw)! : []; + const tokens = data.text ? mfm.parse(data.text) : []; + const cwTokens = data.cw ? mfm.parse(data.cw) : []; const choiceTokens = data.poll?.choices - ? concat(data.poll.choices.map((choice) => mfm.parse(choice)!)) + ? concat(data.poll.choices.map((choice) => mfm.parse(choice))) : []; const combinedTokens = tokens.concat(cwTokens).concat(choiceTokens); @@ -318,16 +305,16 @@ export default async ( .splice(0, 32); if ( - data.reply && + data.reply != null && user.id !== data.reply.userId && - !mentionedUsers.some((u) => u.id === data.reply!.userId) + !mentionedUsers.some((u) => u.id === data.reply?.userId) ) { mentionedUsers.push( - await Users.findOneByOrFail({ id: data.reply!.userId }), + await Users.findOneByOrFail({ id: data.reply.userId }), ); } - if (data.visibility === "specified") { + if (!isDraft && data.visibility === "specified") { if (data.visibleUsers == null) throw new Error("invalid param"); for (const u of data.visibleUsers) { @@ -338,10 +325,10 @@ export default async ( if ( data.reply && - !data.visibleUsers.some((x) => x.id === data.reply!.userId) + !data.visibleUsers.some((x) => x.id === data.reply?.userId) ) { data.visibleUsers.push( - await Users.findOneByOrFail({ id: data.reply!.userId }), + await Users.findOneByOrFail({ id: data.reply?.userId }), ); } } @@ -365,314 +352,321 @@ export default async ( }); } - // ハッシュタグ更新 - if (data.visibility === "public" || data.visibility === "home") { - updateHashtags(user, tags); - } + if (!isDraft) { + // ハッシュタグ更新 + if (data.visibility === "public" || data.visibility === "home") { + updateHashtags(user, tags); + } - // Increment notes count (user) - incNotesCountOfUser(user); + // Increment notes count (user) + incNotesCountOfUser(user); - // Word mutes & antenna - const thisNoteIsMutedBy: string[] = []; + // Word mutes & antenna + const thisNoteIsMutedBy: string[] = []; - await hardMutesCache - .fetch(null, () => - UserProfiles.find({ - where: { - enableWordMute: true, - }, - select: ["userId", "mutedWords", "mutedPatterns"], - }), - ) - .then(async (us) => { - for (const u of us) { - if (u.userId === user.id) return; - await checkWordMute(note, u.mutedWords, u.mutedPatterns).then( - (shouldMute: boolean) => { - if (shouldMute) { - thisNoteIsMutedBy.push(u.userId); - MutedNotes.insert({ - id: genId(), - userId: u.userId, - noteId: note.id, - reason: "word", - }); - } + await hardMutesCache + .fetch(null, () => + UserProfiles.find({ + where: { + enableWordMute: true, }, - ); - } - }); + select: ["userId", "mutedWords", "mutedPatterns"], + }), + ) + .then(async (us) => { + for (const u of us) { + if (u.userId === user.id) return; + await checkWordMute(note, u.mutedWords, u.mutedPatterns).then( + (shouldMute: boolean) => { + if (shouldMute) { + thisNoteIsMutedBy.push(u.userId); + MutedNotes.insert({ + id: genId(), + userId: u.userId, + noteId: note.id, + reason: "word", + }); + } + }, + ); + } + }); - // type errors will be resolved by https://github.com/napi-rs/napi-rs/pull/2054 - const _note = toRustObject(note); - if (note.renoteId == null || isQuote(_note)) { - await updateAntennasOnNewNote(_note, user, thisNoteIsMutedBy); - } + // type errors will be resolved by https://github.com/napi-rs/napi-rs/pull/2054 + const _note = toRustObject(note); + if (note.renoteId == null || isQuote(_note)) { + await updateAntennasOnNewNote(_note, user, thisNoteIsMutedBy); + } - // Channel - if (note.channelId) { - ChannelFollowings.findBy({ followeeId: note.channelId }).then( - (followings) => { - for (const following of followings) { - insertNoteUnread(following.followerId, note, { - isSpecified: false, + // Channel + if (note.channelId != null) { + ChannelFollowings.findBy({ followeeId: note.channelId }).then( + (followings) => { + for (const following of followings) { + insertNoteUnread(following.followerId, note, { + isSpecified: false, + isMentioned: false, + }); + } + }, + ); + } + + if (data.reply) { + saveReply(data.reply, note); + } + + // この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき + if ( + data.renote && + !user.isBot && + (await countSameRenotes(user.id, data.renote.id, note.id)) === 0 + ) { + incRenoteCount(data.renote); + } + + if (data.poll?.expiresAt) { + const delay = data.poll.expiresAt.getTime() - Date.now(); + endedPollNotificationQueue.add( + { + noteId: note.id, + }, + { + delay, + removeOnComplete: true, + }, + ); + } + + if (!silent) { + if (Users.isLocalUser(user)) activeUsersChart.write(user); + + // 未読通知を作成 + if (data.visibility === "specified") { + if (data.visibleUsers == null) throw new Error("invalid param"); + + for (const u of data.visibleUsers) { + // ローカルユーザーのみ + if (!Users.isLocalUser(u)) continue; + + insertNoteUnread(u.id, note, { + isSpecified: true, isMentioned: false, }); } - }, - ); - } - - if (data.reply) { - saveReply(data.reply, note); - } - - // この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき - if ( - data.renote && - !user.isBot && - (await countSameRenotes(user.id, data.renote.id, note.id)) === 0 - ) { - incRenoteCount(data.renote); - } - - if (data.poll?.expiresAt) { - const delay = data.poll.expiresAt.getTime() - Date.now(); - endedPollNotificationQueue.add( - { - noteId: note.id, - }, - { - delay, - removeOnComplete: true, - }, - ); - } - - if (!silent) { - if (Users.isLocalUser(user)) activeUsersChart.write(user); - - // 未読通知を作成 - if (data.visibility === "specified") { - if (data.visibleUsers == null) throw new Error("invalid param"); - - for (const u of data.visibleUsers) { - // ローカルユーザーのみ - if (!Users.isLocalUser(u)) continue; - - insertNoteUnread(u.id, note, { - isSpecified: true, - isMentioned: false, - }); - } - } else { - for (const u of mentionedUsers) { - // ローカルユーザーのみ - if (!Users.isLocalUser(u)) continue; - - insertNoteUnread(u.id, note, { - isSpecified: false, - isMentioned: true, - }); - } - } - - if (!dontFederateInitially) { - let publishKey: string; - let noteToPublish: Note; - const relays = await getCachedRelays(); - - // Some relays (e.g., aode-relay) deliver posts by boosting them as - // Announce activities. In that case, user is the relay's actor. - const boostedByRelay = - !!user.inbox && - relays.map((relay) => relay.inbox).includes(user.inbox); - - if (boostedByRelay && data.renote && data.renote.userHost) { - publishKey = `publishedNote:${data.renote.id}`; - noteToPublish = data.renote; } else { - publishKey = `publishedNote:${note.id}`; - noteToPublish = note; - } + for (const u of mentionedUsers) { + // ローカルユーザーのみ + if (!Users.isLocalUser(u)) continue; - const lock = new Mutex(redisClient, "publishedNote"); - await lock.acquire(); - try { - const published = (await redisClient.get(publishKey)) != null; - if (!published) { - await redisClient.set(publishKey, "done", "EX", 30); - if (noteToPublish.renoteId) { - // Prevents other threads from publishing the boosting post - await redisClient.set( - `publishedNote:${noteToPublish.renoteId}`, - "done", - "EX", - 30, - ); - } - publishNotesStream(noteToPublish); - } - } finally { - await lock.release(); - } - } - if (note.replyId != null) { - // Only provide the reply note id here as the recipient may not be authorized to see the note. - publishNoteStream(note.replyId, "replied", { - id: note.id, - }); - } - - const webhooks = await getActiveWebhooks().then((webhooks) => - webhooks.filter((x) => x.userId === user.id && x.on.includes("note")), - ); - - for (const webhook of webhooks) { - webhookDeliver(webhook, "note", { - note: await Notes.pack(note, user), - }); - } - - const nm = new NotificationManager(user, note); - const nmRelatedPromises = []; - - await createMentionedEvents(mentionedUsers, note, nm); - - // If has in reply to note - if (data.reply) { - // Fetch watchers - nmRelatedPromises.push(notifyToWatchersOfReplyee(data.reply, user, nm)); - - // 通知 - if (data.reply.userHost === null) { - const threadMuted = await NoteThreadMutings.findOneBy({ - userId: data.reply.userId, - threadId: data.reply.threadId || data.reply.id, - }); - - if (!threadMuted) { - nm.push(data.reply.userId, "reply"); - - const packedReply = await Notes.pack(note, { - id: data.reply.userId, + insertNoteUnread(u.id, note, { + isSpecified: false, + isMentioned: true, }); - publishMainStream(data.reply.userId, "reply", packedReply); + } + } + if (note.replyId != null) { + // Only provide the reply note id here as the recipient may not be authorized to see the note. + publishNoteStream(note.replyId, "replied", { + id: note.id, + }); + } + + const webhooks = await getActiveWebhooks().then((webhooks) => + webhooks.filter((x) => x.userId === user.id && x.on.includes("note")), + ); + + for (const webhook of webhooks) { + webhookDeliver(webhook, "note", { + note: await Notes.pack(note, user), + }); + } + + const nm = new NotificationManager(user, note); + const nmRelatedPromises = []; + + await createMentionedEvents(mentionedUsers, note, nm); + + // If has in reply to note + if (data.reply != null) { + // Fetch watchers + nmRelatedPromises.push( + notifyToWatchersOfReplyee(data.reply, user, nm), + ); + + // 通知 + if (data.reply.userHost === null) { + const threadMuted = await NoteThreadMutings.findOneBy({ + userId: data.reply.userId, + threadId: data.reply.threadId || data.reply.id, + }); + + if (!threadMuted) { + nm.push(data.reply.userId, "reply"); + + const packedReply = await Notes.pack(note, { + id: data.reply.userId, + }); + publishMainStream(data.reply.userId, "reply", packedReply); + + const webhooks = (await getActiveWebhooks()).filter( + (x) => + x.userId === data.reply?.userId && x.on.includes("reply"), + ); + for (const webhook of webhooks) { + webhookDeliver(webhook, "reply", { + note: packedReply, + }); + } + } + } + } + + // If it is renote + if (data.renote != null) { + const type = data.text ? "quote" : "renote"; + + // Notify + if (data.renote.userHost === null) { + const threadMuted = await NoteThreadMutings.findOneBy({ + userId: data.renote.userId, + threadId: data.renote.threadId || data.renote.id, + }); + + if (!threadMuted) { + nm.push(data.renote.userId, type); + } + } + // Fetch watchers + nmRelatedPromises.push( + notifyToWatchersOfRenotee(data.renote, user, nm, type), + ); + + // Publish event + if (user.id !== data.renote.userId && data.renote.userHost === null) { + const packedRenote = await Notes.pack(note, { + id: data.renote.userId, + }); + publishMainStream(data.renote.userId, "renote", packedRenote); + + const renote = data.renote; const webhooks = (await getActiveWebhooks()).filter( - (x) => x.userId === data.reply!.userId && x.on.includes("reply"), + (x) => x.userId === renote.userId && x.on.includes("renote"), ); for (const webhook of webhooks) { - webhookDeliver(webhook, "reply", { - note: packedReply, + webhookDeliver(webhook, "renote", { + note: packedRenote, }); } } } - } - // If it is renote - if (data.renote) { - const type = data.text ? "quote" : "renote"; + Promise.all(nmRelatedPromises).then(() => { + nm.deliver(); + }); - // Notify - if (data.renote.userHost === null) { - const threadMuted = await NoteThreadMutings.findOneBy({ - userId: data.renote.userId, - threadId: data.renote.threadId || data.renote.id, - }); + //#region AP deliver + if (Users.isLocalUser(user) && !dontFederateInitially) { + (async () => { + const noteActivity = await renderNoteOrRenoteActivity(data, note); + const dm = new DeliverManager(user, noteActivity); - if (!threadMuted) { - nm.push(data.renote.userId, type); - } - } - // Fetch watchers - nmRelatedPromises.push( - notifyToWatchersOfRenotee(data.renote, user, nm, type), - ); - - // Publish event - if (user.id !== data.renote.userId && data.renote.userHost === null) { - const packedRenote = await Notes.pack(note, { - id: data.renote.userId, - }); - publishMainStream(data.renote.userId, "renote", packedRenote); - - const renote = data.renote; - const webhooks = (await getActiveWebhooks()).filter( - (x) => x.userId === renote.userId && x.on.includes("renote"), - ); - for (const webhook of webhooks) { - webhookDeliver(webhook, "renote", { - note: packedRenote, - }); - } + // メンションされたリモートユーザーに配送 + for (const u of mentionedUsers.filter((u) => + Users.isRemoteUser(u), + )) { + dm.addDirectRecipe(u as IRemoteUser); + } + + // 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送 + if (data.reply?.userHost != null) { + const u = await Users.findOneBy({ id: data.reply.userId }); + if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u); + } + + // 投稿がRenoteかつ投稿者がローカルユーザーかつRenote元の投稿の投稿者がリモートユーザーなら配送 + if (data.renote?.userHost != null) { + const u = await Users.findOneBy({ id: data.renote.userId }); + if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u); + } + + // フォロワーに配送 + if (["public", "home", "followers"].includes(note.visibility)) { + dm.addFollowersRecipe(); + } + + if (["public"].includes(note.visibility)) { + deliverToRelays(user, noteActivity); + } + + dm.execute(); + })(); } + //#endregion } - Promise.all(nmRelatedPromises).then(() => { - nm.deliver(); - }); + if (data.channel) { + Channels.increment({ id: data.channel.id }, "notesCount", 1); + Channels.update(data.channel.id, { + lastNotedAt: new Date(), + }); - //#region AP deliver - if (Users.isLocalUser(user) && !dontFederateInitially) { - (async () => { - const noteActivity = await renderNoteOrRenoteActivity(data, note); - const dm = new DeliverManager(user, noteActivity); - - // メンションされたリモートユーザーに配送 - for (const u of mentionedUsers.filter((u) => Users.isRemoteUser(u))) { - dm.addDirectRecipe(u as IRemoteUser); + await Notes.countBy({ + userId: user.id, + channelId: data.channel.id, + }).then((count) => { + // この処理が行われるのはノート作成後なので、ノートが一つしかなかったら最初の投稿だと判断できる + // TODO: とはいえノートを削除して何回も投稿すればその分だけインクリメントされる雑さもあるのでどうにかしたい + if (count === 1 && data.channel != null) { + Channels.increment({ id: data.channel.id }, "usersCount", 1); } - - // 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送 - if (data.reply?.userHost != null) { - const u = await Users.findOneBy({ id: data.reply.userId }); - if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u); - } - - // 投稿がRenoteかつ投稿者がローカルユーザーかつRenote元の投稿の投稿者がリモートユーザーなら配送 - if (data.renote?.userHost != null) { - const u = await Users.findOneBy({ id: data.renote.userId }); - if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u); - } - - // フォロワーに配送 - if (["public", "home", "followers"].includes(note.visibility)) { - dm.addFollowersRecipe(); - } - - if (["public"].includes(note.visibility)) { - deliverToRelays(user, noteActivity); - } - - dm.execute(); - })(); + }); } - //#endregion } - if (data.channel) { - Channels.increment({ id: data.channel.id }, "notesCount", 1); - Channels.update(data.channel.id, { - lastNotedAt: new Date(), - }); + if (!dontFederateInitially) { + let publishKey: string; + let noteToPublish: Note; + const relays = await getCachedRelays(); - await Notes.countBy({ - userId: user.id, - channelId: data.channel.id, - }).then((count) => { - // この処理が行われるのはノート作成後なので、ノートが一つしかなかったら最初の投稿だと判断できる - // TODO: とはいえノートを削除して何回も投稿すればその分だけインクリメントされる雑さもあるのでどうにかしたい - if (count === 1 && data.channel != null) { - Channels.increment({ id: data.channel.id }, "usersCount", 1); + // Some relays (e.g., aode-relay) deliver posts by boosting them as + // Announce activities. In that case, user is the relay's actor. + const boostedByRelay = + !!user.inbox && relays.map((relay) => relay.inbox).includes(user.inbox); + + if (boostedByRelay && data.renote && data.renote.userHost) { + publishKey = `publishedNote:${data.renote.id}`; + noteToPublish = data.renote; + } else { + publishKey = `publishedNote:${note.id}`; + noteToPublish = note; + } + + const lock = new Mutex(redisClient, "publishedNote"); + await lock.acquire(); + try { + const published = (await redisClient.get(publishKey)) != null; + if (!published) { + await redisClient.set(publishKey, "done", "EX", 30); + if (noteToPublish.renoteId) { + // Prevents other threads from publishing the boosting post + await redisClient.set( + `publishedNote:${noteToPublish.renoteId}`, + "done", + "EX", + 30, + ); + } + publishToNotesStream(toRustObject(noteToPublish)); } - }); + } finally { + await lock.release(); + } } }); -async function renderNoteOrRenoteActivity(data: Option, note: Note) { +async function renderNoteOrRenoteActivity(data: NoteLike, note: Note) { if (data.localOnly) return null; const content = @@ -704,17 +698,17 @@ function incRenoteCount(renote: Note) { async function insertNote( user: { id: User["id"]; host: User["host"] }, - data: Option, + data: NoteLike, tags: string[], emojis: string[], - mentionedUsers: MinimumUser[], + mentionedUsers: UserLike[], ) { - if (data.createdAt === null || data.createdAt === undefined) { - data.createdAt = new Date(); - } - const insert = new Note({ + data.createdAt ??= new Date(); + + const note = new Note({ id: genIdAt(data.createdAt), createdAt: data.createdAt, + scheduledAt: data.scheduledAt ?? null, fileIds: data.files ? data.files.map((file) => file.id) : [], replyId: data.reply ? data.reply.id : null, renoteId: data.renote ? data.renote.id : null, @@ -743,30 +737,30 @@ async function insertNote( attachedFileTypes: data.files ? data.files.map((file) => file.type) : [], - // 以下非正規化データ + // denormalized fields replyUserId: data.reply ? data.reply.userId : null, replyUserHost: data.reply ? data.reply.userHost : null, renoteUserId: data.renote ? data.renote.userId : null, renoteUserHost: data.renote ? data.renote.userHost : null, userHost: user.host, + updatedAt: undefined, + uri: data.uri ?? undefined, + url: data.url ?? undefined, }); - if (data.uri != null) insert.uri = data.uri; - if (data.url != null) insert.url = data.url; - // Append mentions data if (mentionedUsers.length > 0) { - insert.mentions = mentionedUsers.map((u) => u.id); - const profiles = await UserProfiles.findBy({ userId: In(insert.mentions) }); - insert.mentionedRemoteUsers = JSON.stringify( + note.mentions = mentionedUsers.map((u) => u.id); + const profiles = await UserProfiles.findBy({ userId: In(note.mentions) }); + note.mentionedRemoteUsers = JSON.stringify( mentionedUsers .filter((u) => Users.isRemoteUser(u)) .map((u) => { const profile = profiles.find((p) => p.userId === u.id); - const url = profile != null ? profile.url : null; + const url = profile?.url ?? null; return { uri: u.uri, - url: url == null ? undefined : url, + url: url ?? undefined, username: u.username, host: u.host, } as IMentionedRemoteUsers[0]; @@ -776,12 +770,12 @@ async function insertNote( // 投稿を作成 try { - if (insert.hasPoll) { + if (note.hasPoll) { // Start transaction await db.transaction(async (transactionalEntityManager) => { if (!data.poll) throw new Error("Empty poll data"); - await transactionalEntityManager.insert(Note, insert); + await transactionalEntityManager.insert(Note, note); let expiresAt: Date | null; if ( @@ -794,12 +788,12 @@ async function insertNote( } const poll = new Poll({ - noteId: insert.id, + noteId: note.id, choices: data.poll.choices, expiresAt, multiple: data.poll.multiple, votes: new Array(data.poll.choices.length).fill(0), - noteVisibility: insert.visibility, + noteVisibility: note.visibility, userId: user.id, userHost: user.host, }); @@ -807,10 +801,10 @@ async function insertNote( await transactionalEntityManager.insert(Poll, poll); }); } else { - await Notes.insert(insert); + await Notes.insert(note); } - return insert; + return note; } catch (e) { // duplicate key error if (isDuplicateKeyValueError(e)) { @@ -857,7 +851,7 @@ async function notifyToWatchersOfReplyee( } async function createMentionedEvents( - mentionedUsers: MinimumUser[], + mentionedUsers: UserLike[], note: Note, nm: NotificationManager, ) { diff --git a/packages/backend/src/services/note/delete.ts b/packages/backend/src/services/note/delete.ts index c709792fef..966d72a036 100644 --- a/packages/backend/src/services/note/delete.ts +++ b/packages/backend/src/services/note/delete.ts @@ -42,8 +42,12 @@ export default async function ( ) { const deletedAt = new Date(); + // Whether this is a scheduled "draft" post + const isDraft = note.scheduledAt != null; + // この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき if ( + !isDraft && note.renoteId && (await countSameRenotes(user.id, note.renoteId, note.id)) === 0 && deleteFromDb @@ -52,7 +56,7 @@ export default async function ( Notes.decrement({ id: note.renoteId }, "score", 1); } - if (note.replyId && deleteFromDb) { + if (!isDraft && note.replyId != null && deleteFromDb) { await Notes.decrement({ id: note.replyId }, "repliesCount", 1); } @@ -74,7 +78,7 @@ export default async function ( } //#region ローカルの投稿なら削除アクティビティを配送 - if (Users.isLocalUser(user) && !note.localOnly) { + if (!isDraft && Users.isLocalUser(user) && !note.localOnly) { let renote: Note | null = null; // if deletd note is renote diff --git a/packages/backend/src/services/note/polls/vote.ts b/packages/backend/src/services/note/polls/vote.ts index 32213ddcc8..c1070576f1 100644 --- a/packages/backend/src/services/note/polls/vote.ts +++ b/packages/backend/src/services/note/polls/vote.ts @@ -3,7 +3,7 @@ import type { CacheableUser } from "@/models/entities/user.js"; import type { Note } from "@/models/entities/note.js"; import { PollVotes, NoteWatchings, Polls, Blockings } from "@/models/index.js"; import { Not } from "typeorm"; -import { genId } from "backend-rs"; +import { genIdAt } from "backend-rs"; import { createNotification } from "@/services/create-notification.js"; export default async function ( @@ -43,10 +43,12 @@ export default async function ( throw new Error("already voted"); } + const now = new Date(); + // Create vote await PollVotes.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, noteId: note.id, userId: user.id, choice: choice, diff --git a/packages/backend/src/services/note/reaction/create.ts b/packages/backend/src/services/note/reaction/create.ts index 3b8b97cefd..54b0bfaaab 100644 --- a/packages/backend/src/services/note/reaction/create.ts +++ b/packages/backend/src/services/note/reaction/create.ts @@ -13,7 +13,7 @@ import { Blockings, } from "@/models/index.js"; import { IsNull, Not } from "typeorm"; -import { decodeReaction, genId, toDbReaction } from "backend-rs"; +import { decodeReaction, genIdAt, toDbReaction } from "backend-rs"; import { createNotification } from "@/services/create-notification.js"; import deleteReaction from "./delete.js"; import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js"; @@ -47,9 +47,11 @@ export default async ( // TODO: cache reaction = await toDbReaction(reaction, user.host); + const now = new Date(); + const record: NoteReaction = { - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, noteId: note.id, userId: user.id, reaction, diff --git a/packages/backend/src/services/note/unread.ts b/packages/backend/src/services/note/unread.ts index e00dcd07ac..d52ac46d4d 100644 --- a/packages/backend/src/services/note/unread.ts +++ b/packages/backend/src/services/note/unread.ts @@ -42,9 +42,9 @@ export async function insertNoteUnread( // 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する setTimeout(async () => { - const exist = await NoteUnreads.exist({ where: { id: unread.id } }); + const exists = await NoteUnreads.existsBy({ id: unread.id }); - if (!exist) return; + if (!exists) return; if (params.isMentioned) { publishMainStream(userId, "unreadMention", note.id); diff --git a/packages/backend/src/services/send-email.ts b/packages/backend/src/services/send-email.ts index bcbecce356..daf57a68cd 100644 --- a/packages/backend/src/services/send-email.ts +++ b/packages/backend/src/services/send-email.ts @@ -12,7 +12,7 @@ export async function sendEmail( html: string, text: string, ) { - const meta = await fetchMeta(false); + const meta = await fetchMeta(); const iconUrl = `${config.url}/static-assets/mi-white.png`; const emailSettingUrl = `${config.url}/settings/email`; diff --git a/packages/backend/src/services/stream.ts b/packages/backend/src/services/stream.ts index 36914d4d41..f2f5cf76d9 100644 --- a/packages/backend/src/services/stream.ts +++ b/packages/backend/src/services/stream.ts @@ -12,7 +12,7 @@ import type { // AntennaStreamTypes, // BroadcastTypes, // ChannelStreamTypes, - DriveStreamTypes, + // DriveStreamTypes, // GroupMessagingStreamTypes, InternalStreamTypes, MainStreamTypes, @@ -88,17 +88,18 @@ class Publisher { ); }; - public publishDriveStream = ( - userId: User["id"], - type: K, - value?: DriveStreamTypes[K], - ): void => { - this.publish( - `driveStream:${userId}`, - type, - typeof value === "undefined" ? null : value, - ); - }; + /* ported to backend-rs */ + // public publishDriveStream = ( + // userId: User["id"], + // type: K, + // value?: DriveStreamTypes[K], + // ): void => { + // this.publish( + // `driveStream:${userId}`, + // type, + // typeof value === "undefined" ? null : value, + // ); + // }; public publishNoteStream = ( noteId: Note["id"], @@ -193,9 +194,10 @@ class Publisher { // ); // }; - public publishNotesStream = (note: Note): void => { - this.publish("notesStream", null, note); - }; + /* ported to backend-rs */ + // public publishNotesStream = (note: Note): void => { + // this.publish("notesStream", null, note); + // }; /* ported to backend-rs */ // public publishAdminStream = ( @@ -219,9 +221,9 @@ export const publishInternalEvent = publisher.publishInternalEvent; export const publishUserEvent = publisher.publishUserEvent; // export const publishBroadcastStream = publisher.publishBroadcastStream; export const publishMainStream = publisher.publishMainStream; -export const publishDriveStream = publisher.publishDriveStream; +// export const publishDriveStream = publisher.publishDriveStream; export const publishNoteStream = publisher.publishNoteStream; -export const publishNotesStream = publisher.publishNotesStream; +// export const publishNotesStream = publisher.publishNotesStream; // export const publishChannelStream = publisher.publishChannelStream; export const publishUserListStream = publisher.publishUserListStream; // export const publishAntennaStream = publisher.publishAntennaStream; diff --git a/packages/backend/src/services/user-list/push.ts b/packages/backend/src/services/user-list/push.ts index 141584e294..1ec4289672 100644 --- a/packages/backend/src/services/user-list/push.ts +++ b/packages/backend/src/services/user-list/push.ts @@ -3,14 +3,16 @@ import type { User } from "@/models/entities/user.js"; import type { UserList } from "@/models/entities/user-list.js"; import { UserListJoinings, Users } from "@/models/index.js"; import type { UserListJoining } from "@/models/entities/user-list-joining.js"; -import { genId } from "backend-rs"; +import { genIdAt } from "backend-rs"; import { fetchProxyAccount } from "@/misc/fetch-proxy-account.js"; import createFollowing from "@/services/following/create.js"; export async function pushUserToUserList(target: User, list: UserList) { + const now = new Date(); + await UserListJoinings.insert({ - id: genId(), - createdAt: new Date(), + id: genIdAt(now), + createdAt: now, userId: target.id, userListId: list.id, } as UserListJoining); diff --git a/packages/backend/src/services/validate-email-for-account.ts b/packages/backend/src/services/validate-email-for-account.ts index 5aa091a5ac..616dbccebf 100644 --- a/packages/backend/src/services/validate-email-for-account.ts +++ b/packages/backend/src/services/validate-email-for-account.ts @@ -6,7 +6,7 @@ export async function validateEmailForAccount(emailAddress: string): Promise<{ available: boolean; reason: null | "used" | "format" | "disposable" | "mx" | "smtp"; }> { - const meta = await fetchMeta(true); + const meta = await fetchMeta(); const exist = await UserProfiles.countBy({ emailVerified: true, diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json index bee0077992..5e1eeac3d4 100644 --- a/packages/backend/tsconfig.json +++ b/packages/backend/tsconfig.json @@ -1,6 +1,5 @@ { "compilerOptions": { - "allowJs": true, "noEmitOnError": false, "noImplicitAny": true, "noImplicitReturns": true, @@ -8,15 +7,12 @@ "noUnusedLocals": false, "noFallthroughCasesInSwitch": true, "declaration": false, - "sourceMap": false, "target": "es2021", "module": "esnext", "moduleResolution": "node", "allowSyntheticDefaultImports": true, "removeComments": false, "noLib": false, - "strict": true, - "strictNullChecks": true, "strictPropertyInitialization": false, "experimentalDecorators": true, "emitDecoratorMetadata": true, diff --git a/packages/client/.eslintrc.json b/packages/client/.eslintrc.json deleted file mode 100644 index 37d80f6588..0000000000 --- a/packages/client/.eslintrc.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "extends": ["@eslint-sets/vue3", "@eslint-sets/vue3-ts"], - "plugins": ["file-progress"], - "ignorePatterns": ["**/*.json5"], - "rules": { - "file-progress/activate": 1, - "prettier/prettier": "off", - "one-var": ["warn", "never"], - "@typescript-eslint/no-unused-vars": [ - "warn", - { - "argsIgnorePattern": "^_", - "varsIgnorePattern": "^_", - "caughtErrorsIgnorePattern": "^_", - "destructuredArrayIgnorePattern": "^_" - } - ], - "vue/no-setup-props-destructure": "off" - } -} diff --git a/packages/client/package.json b/packages/client/package.json index 55f049afd6..27683f8ec6 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -6,14 +6,13 @@ "watch": "pnpm vite build --watch --mode development", "build": "pnpm vite build", "build:debug": "pnpm run build", - "lint": "pnpm biome check **/*.ts --apply ; pnpm run lint:vue", - "lint:vue": "pnpm eslint src --fix '**/*.vue' --cache ; pnpm run format", + "lint": "pnpm run lint:ts ; pnpm run lint:vue", + "lint:ts": "pnpm biome check **/*.ts --write", + "lint:vue": "pnpm biome check **/*.vue --write", "types:check": "pnpm vue-tsc --noEmit", "format": "pnpm biome format * --write" }, "devDependencies": { - "@eslint-sets/eslint-config-vue3": "5.13.0", - "@eslint-sets/eslint-config-vue3-ts": "3.3.0", "@phosphor-icons/web": "2.1.1", "@rollup/plugin-alias": "5.1.0", "@rollup/plugin-json": "6.1.0", @@ -32,9 +31,9 @@ "@types/textarea-caret": "3.0.3", "@types/throttle-debounce": "5.0.2", "@types/tinycolor2": "1.4.6", - "@types/uuid": "9.0.8", - "@vitejs/plugin-vue": "5.0.4", - "@vue/runtime-core": "3.4.27", + "@types/uuid": "10.0.0", + "@vitejs/plugin-vue": "5.0.5", + "@vue/runtime-core": "3.4.31", "autobind-decorator": "2.4.0", "autosize": "6.0.1", "broadcast-channel": "7.0.0", @@ -43,12 +42,12 @@ "chartjs-chart-matrix": "2.0.1", "chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-zoom": "2.0.1", + "check-password-strength": "2.0.10", "city-timezones": "1.2.1", "compare-versions": "6.1.0", - "cropperjs": "2.0.0-rc", + "cropperjs": "2.0.0-rc.0", "date-fns": "3.6.0", "emojilib": "3.0.12", - "eslint-plugin-file-progress": "1.4.0", "eventemitter3": "5.0.1", "fast-blurhash": "1.1.2", "firefish-js": "workspace:*", @@ -58,10 +57,10 @@ "idb-keyval": "6.2.1", "insert-text-at-cursor": "0.3.0", "json5": "2.2.3", - "katex": "0.16.10", + "katex": "0.16.11", "long": "5.2.3", - "libopenmpt-wasm": "github:TheEssem/libopenmpt-packaging#build", - "matter-js": "0.19.0", + "libopenmpt-wasm": "git+https://github.com/TheEssem/libopenmpt-packaging.git#d05d151a72b638c6312227af0417aca69521172c", + "matter-js": "0.20.0", "mfm-js": "0.24.0", "multer": "1.4.5-lts.1", "moment": "2.30.1", @@ -72,24 +71,23 @@ "qrcode-vue3": "1.6.8", "rollup": "4.17.2", "s-age": "1.1.2", - "sass": "1.77.3", + "sass": "1.77.6", "seedrandom": "3.0.5", "stringz": "2.1.0", "swiper": "11.1.4", - "syuilo-password-strength": "0.0.1", "textarea-caret": "3.1.0", - "throttle-debounce": "5.0.0", + "throttle-debounce": "5.0.2", "tinycolor2": "1.6.0", "tinyld": "1.3.4", - "typescript": "5.4.5", + "typescript": "5.5.3", "unicode-emoji-json": "0.6.0", - "uuid": "9.0.1", - "vite": "5.2.12", + "uuid": "10.0.0", + "vite": "5.3.3", "vite-plugin-compression": "0.5.1", - "vue": "3.4.27", + "vue": "3.4.31", "vue-draggable-plus": "0.5.0", "vue-plyr": "7.0.0", "vue-prism-editor": "2.0.0-alpha.2", - "vue-tsc": "2.0.19" + "vue-tsc": "2.0.24" } } diff --git a/packages/client/src/account.ts b/packages/client/src/account.ts index 1d38b2798f..c4830f1210 100644 --- a/packages/client/src/account.ts +++ b/packages/client/src/account.ts @@ -7,7 +7,7 @@ import { alert, api, popup, popupMenu, waiting } from "@/os"; import icon from "@/scripts/icon"; import { del, get, set } from "@/scripts/idb-proxy"; import { reloadChannel, unisonReload } from "@/scripts/unison-reload"; -import type { MenuButton, MenuUser } from "./types/menu"; +import type { MenuUser } from "./types/menu"; // TODO: 他のタブと永続化されたstateを同期 diff --git a/packages/client/src/components/MkAutocomplete.vue b/packages/client/src/components/MkAutocomplete.vue index 4e890cc03e..b6bc6b65ba 100644 --- a/packages/client/src/components/MkAutocomplete.vue +++ b/packages/client/src/components/MkAutocomplete.vue @@ -100,7 +100,7 @@ import * as os from "@/os"; import { MFM_TAGS } from "@/scripts/mfm-tags"; import { defaultStore } from "@/store"; import { addSkinTone, emojilist } from "@/scripts/emojilist"; -import { instance } from "@/instance"; +import { getInstanceInfo } from "@/instance"; import { i18n } from "@/i18n"; interface EmojiDef { @@ -141,7 +141,7 @@ for (const x of lib) { emjdb.sort((a, b) => a.name.length - b.name.length); // #region Construct Emoji DB -const customEmojis = instance.emojis; +const customEmojis = getInstanceInfo().emojis; const emojiDefinitions: EmojiDef[] = []; for (const x of customEmojis) { diff --git a/packages/client/src/components/MkDonation.vue b/packages/client/src/components/MkDonation.vue index ad9df629f4..a799fef8fc 100644 --- a/packages/client/src/components/MkDonation.vue +++ b/packages/client/src/components/MkDonation.vue @@ -64,7 +64,7 @@ import MkButton from "@/components/MkButton.vue"; import { host } from "@/config"; import { i18n } from "@/i18n"; import * as os from "@/os"; -import { instance } from "@/instance"; +import { getInstanceInfo } from "@/instance"; import icon from "@/scripts/icon"; const show = ref(false); @@ -73,6 +73,7 @@ const emit = defineEmits<{ (ev: "closed"): void; }>(); +const instance = getInstanceInfo(); const hostname = instance.name?.length && instance.name?.length < 38 ? instance.name : host; diff --git a/packages/client/src/components/MkEmojiPicker.vue b/packages/client/src/components/MkEmojiPicker.vue index c731fe9b0e..5bf3d5d829 100644 --- a/packages/client/src/components/MkEmojiPicker.vue +++ b/packages/client/src/components/MkEmojiPicker.vue @@ -164,7 +164,7 @@ diff --git a/packages/client/src/components/MkInstanceTicker.vue b/packages/client/src/components/MkInstanceTicker.vue index f27cce02a3..a2a6585395 100644 --- a/packages/client/src/components/MkInstanceTicker.vue +++ b/packages/client/src/components/MkInstanceTicker.vue @@ -19,7 +19,7 @@ import { ref } from "vue"; import type { entities } from "firefish-js"; import { instanceName, version } from "@/config"; -import { instance as Instance } from "@/instance"; +import { getInstanceInfo } from "@/instance"; import { getProxiedImageUrlNullable } from "@/scripts/media-proxy"; const props = defineProps<{ @@ -28,9 +28,10 @@ const props = defineProps<{ const ticker = ref(null); +// FIXME: the following assumption is not necessarily correct // if no instance data is given, this is for the local instance const instance = props.instance ?? { - faviconUrl: Instance.iconUrl || "/favicon.ico", + faviconUrl: getInstanceInfo().iconUrl || "/favicon.ico", name: instanceName, themeColor: ( document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement @@ -78,8 +79,8 @@ const bg = { function getInstanceIcon(instance): string { return ( - getProxiedImageUrlNullable(instance.iconUrl, "preview") ?? getProxiedImageUrlNullable(instance.faviconUrl, "preview") ?? + getProxiedImageUrlNullable(instance.iconUrl, "preview") ?? "/client-assets/dummy.png" ); } diff --git a/packages/client/src/components/MkMediaCaption.vue b/packages/client/src/components/MkMediaCaption.vue index b09024adb7..24cff479bc 100644 --- a/packages/client/src/components/MkMediaCaption.vue +++ b/packages/client/src/components/MkMediaCaption.vue @@ -76,7 +76,7 @@ import MkButton from "@/components/MkButton.vue"; import bytes from "@/filters/bytes"; import number from "@/filters/number"; import { i18n } from "@/i18n"; -import { instance } from "@/instance"; +import { getInstanceInfo } from "@/instance"; const props = withDefaults( defineProps<{ @@ -109,14 +109,14 @@ const modal = ref | null>(null); const inputValue = ref(props.input.default ? props.input.default : null); const remainingLength = computed(() => { - const maxCaptionLength = instance.maxCaptionTextLength ?? 512; + const maxCaptionLength = getInstanceInfo().maxCaptionTextLength ?? 512; if (typeof inputValue.value !== "string") return maxCaptionLength; return maxCaptionLength - length(inputValue.value); }); function done(canceled: boolean, result?: string | null) { emit("done", { canceled, result }); - modal.value!.close(); + modal.value?.close(); } async function ok() { diff --git a/packages/client/src/components/MkNotification.vue b/packages/client/src/components/MkNotification.vue index e3f8259def..3158ea43ab 100644 --- a/packages/client/src/components/MkNotification.vue +++ b/packages/client/src/components/MkNotification.vue @@ -285,7 +285,7 @@ import * as os from "@/os"; import { useStream } from "@/stream"; import { useTooltip } from "@/scripts/use-tooltip"; import { defaultStore } from "@/store"; -import { instance } from "@/instance"; +import { getInstanceInfo } from "@/instance"; import icon from "@/scripts/icon"; const props = withDefaults( @@ -309,8 +309,9 @@ const hideFollowButton = defaultStore.state.hideFollowButtons; const showEmojiReactions = defaultStore.state.enableEmojiReactions || defaultStore.state.showEmojisInReactionNotifications; -const defaultReaction = ["⭐", "👍", "❤️"].includes(instance.defaultReaction) - ? instance.defaultReaction +const realDefaultReaction = getInstanceInfo().defaultReaction; +const defaultReaction = ["⭐", "👍", "❤️"].includes(realDefaultReaction) + ? realDefaultReaction : "⭐"; let readObserver: IntersectionObserver | undefined; diff --git a/packages/client/src/components/MkNotificationFolded.vue b/packages/client/src/components/MkNotificationFolded.vue index c7a2730cd3..91c6f5d902 100644 --- a/packages/client/src/components/MkNotificationFolded.vue +++ b/packages/client/src/components/MkNotificationFolded.vue @@ -88,7 +88,7 @@ import * as os from "@/os"; import { useStream } from "@/stream"; import { useTooltip } from "@/scripts/use-tooltip"; import { defaultStore } from "@/store"; -import { instance } from "@/instance"; +import { getInstanceInfo } from "@/instance"; import icon from "@/scripts/icon"; import type { NotificationFolded, @@ -116,8 +116,9 @@ const reactionRef = ref | null>(null); const showEmojiReactions = defaultStore.state.enableEmojiReactions || defaultStore.state.showEmojisInReactionNotifications; -const defaultReaction = ["⭐", "👍", "❤️"].includes(instance.defaultReaction) - ? instance.defaultReaction +const realDefaultReaction = getInstanceInfo().defaultReaction; +const defaultReaction = ["⭐", "👍", "❤️"].includes(realDefaultReaction) + ? realDefaultReaction : "⭐"; const users = computed(() => props.notification.users.slice(0, 5)); @@ -178,7 +179,7 @@ useTooltip(reactionRef, (showing) => { ? n.reaction.replace(/^:(\w+):$/, ":$1@.:") : n.reaction, emojis: n.note.emojis, - targetElement: reactionRef.value!.$el, + targetElement: reactionRef.value?.$el, }, {}, "closed", @@ -203,7 +204,7 @@ onMounted(() => { readObserver.observe(elRef.value!); connection = stream.useChannel("main"); - connection.on("readAllNotifications", () => readObserver!.disconnect()); + connection.on("readAllNotifications", () => readObserver?.disconnect()); }); onUnmounted(() => { diff --git a/packages/client/src/components/MkPostForm.vue b/packages/client/src/components/MkPostForm.vue index 85da6ac55a..148450e389 100644 --- a/packages/client/src/components/MkPostForm.vue +++ b/packages/client/src/components/MkPostForm.vue @@ -322,7 +322,7 @@ import { selectFiles } from "@/scripts/select-file"; import { defaultStore, notePostInterruptors, postFormActions } from "@/store"; import MkInfo from "@/components/MkInfo.vue"; import { i18n } from "@/i18n"; -import { instance } from "@/instance"; +import { getInstanceInfo } from "@/instance"; import { getAccounts, openAccountMenu as openAccountMenu_ } from "@/account"; import { me } from "@/me"; import { uploadFile } from "@/scripts/upload"; @@ -497,7 +497,7 @@ const textLength = computed((): number => { }); const maxTextLength = computed((): number => { - return instance ? instance.maxNoteTextLength : 3000; + return getInstanceInfo().maxNoteTextLength ?? 3000; }); const canPost = computed((): boolean => { @@ -1195,6 +1195,13 @@ async function post() { } } + if ( + defaultStore.state.addAlt4MeTag && + files.value.some((f) => f.comment == null || f.comment.length === 0) + ) { + text.value = `${text.value.trimEnd()}\n#Alt4Me`; + } + const processedText = preprocess(text.value); let postData: ApiTypes.NoteSubmitReq = { diff --git a/packages/client/src/components/MkPushNotificationAllowButton.vue b/packages/client/src/components/MkPushNotificationAllowButton.vue index 1d1be0fb05..217f8688b3 100644 --- a/packages/client/src/components/MkPushNotificationAllowButton.vue +++ b/packages/client/src/components/MkPushNotificationAllowButton.vue @@ -58,7 +58,7 @@ import { ref } from "vue"; import { getAccounts } from "@/account"; import { isSignedIn, me } from "@/me"; import MkButton from "@/components/MkButton.vue"; -import { instance } from "@/instance"; +import { getInstanceInfo } from "@/instance"; import { api, apiWithDialog, promiseDialog } from "@/os"; import { i18n } from "@/i18n"; @@ -76,6 +76,7 @@ defineProps<{ showOnlyToRegister?: boolean; }>(); +const { swPublickey } = getInstanceInfo(); // ServiceWorker registration const registration = ref(); // If this browser supports push notification @@ -94,14 +95,14 @@ const pushRegistrationInServer = ref< >(); function subscribe() { - if (!registration.value || !supported.value || !instance.swPublickey) return; + if (!registration.value || !supported.value || !swPublickey) return; // SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters return promiseDialog( registration.value.pushManager .subscribe({ userVisibleOnly: true, - applicationServerKey: urlBase64ToUint8Array(instance.swPublickey), + applicationServerKey: urlBase64ToUint8Array(swPublickey), }) .then( async (subscription) => { @@ -186,12 +187,7 @@ if (navigator.serviceWorker == null) { pushSubscription.value = await registration.value.pushManager.getSubscription(); - if ( - instance.swPublickey && - "PushManager" in window && - isSignedIn(me) && - me.token - ) { + if (swPublickey && "PushManager" in window && isSignedIn(me) && me.token) { supported.value = true; if (pushSubscription.value) { diff --git a/packages/client/src/components/MkQrCode.vue b/packages/client/src/components/MkQrCode.vue index 948de74c13..339ca82dd9 100644 --- a/packages/client/src/components/MkQrCode.vue +++ b/packages/client/src/components/MkQrCode.vue @@ -6,9 +6,14 @@ :value="qrCode" /> - {{ - i18n.ts.gotIt - }} +
+ {{ + i18n.ts.gotIt + }} + {{ + i18n.ts.copyLink + }} +
@@ -19,8 +24,10 @@ import QRCodeVue3 from "qrcode-vue3"; import MkModal from "@/components/MkModal.vue"; import MkButton from "@/components/MkButton.vue"; import { i18n } from "@/i18n"; +import * as os from "@/os"; +import copyToClipboard from "@/scripts/copy-to-clipboard"; -defineProps<{ +const props = defineProps<{ qrCode: string; }>(); @@ -29,6 +36,11 @@ const modal = shallowRef>(); const gotIt = () => { modal.value.close(); }; + +const copyLink = () => { + copyToClipboard(props.qrCode); + os.success(); +};