diff --git a/.config/example.yml b/.config/example.yml index 9082dfb868..17149f6c3a 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -178,19 +178,13 @@ logLevel: [ # Media Proxy #mediaProxy: https://example.com/proxy -# Proxy remote files (default: false) +# Proxy remote files (default: true) #proxyRemoteFiles: true #allowedPrivateNetworks: [ # '127.0.0.1/32' #] -# TWA -#twa: -# nameSpace: android_app -# packageName: tld.domain.twa -# sha256CertFingerprints: ['AB:CD:EF'] - # Upload or download file size limits (bytes) #maxFileSize: 262144000 diff --git a/.gitlab/issue_templates/bug.md b/.gitlab/issue_templates/bug.md index 3bffa21cde..f96bdec01e 100644 --- a/.gitlab/issue_templates/bug.md +++ b/.gitlab/issue_templates/bug.md @@ -3,30 +3,47 @@ 🔒 Found a security vulnerability? [Please disclose it responsibly.](https://firefish.dev/firefish/firefish/-/blob/develop/SECURITY.md) 🤝 By submitting this issue, you agree to follow our [Contribution Guidelines.](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md) --> -**What happened?** _(Please give us a brief description of what happened.)_ +## What happened? -**What did you expect to happen?** _(Please give us a brief description of what you expected to happen.)_ -**Version** _(What version of firefish is your instance running? You can find this by clicking your instance's logo at the bottom left and then clicking instance information.)_ +## What did you expect to happen? -**Instance** _(What instance of firefish are you using?)_ -**What type of issue is this?** _(If this happens on your device and has to do with the user interface, it's client-side. If this happens on either with the API or the backend, or you got a server-side error in the client, it's server-side.)_ +## Version -**What browser are you using? (Client-side issues only)** -**What operating system are you using? (Client-side issues only)** +## What type of issue is this? -**How do you deploy Firefish on your server? (Server-side issues only)** +- [ ] server-side +- [ ] client-side +- [ ] not sure -**What operating system are you using? (Server-side issues only)** +
-**Relevant log output** _(Please copy and paste any relevant log output. You can find your log by inspecting the page, and going to the "console" tab. This will be automatically formatted into code, so no need for backticks.)_ +### Instance -**Contribution Guidelines** + +### 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) +## 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/feature.md b/.gitlab/issue_templates/feature.md index b4af4884b7..4c9ee56226 100644 --- a/.gitlab/issue_templates/feature.md +++ b/.gitlab/issue_templates/feature.md @@ -3,18 +3,22 @@ 🔒 Found a security vulnerability? [Please disclose it responsibly.](https://firefish.dev/firefish/firefish/-/blob/develop/SECURITY.md) 🤝 By submitting this feature request, you agree to follow our [Contribution Guidelines.](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md) --> -**What feature would you like implemented?** _(Please give us a brief description of what you'd like.)_ +## What feature would you like implemented? -**Why should we add this feature?** _(Please give us a brief description of why your feature is important.)_ -**Version** _(What version of firefish is your instance running? You can find this by clicking your instance's logo at the bottom left and then clicking instance information.)_ +## Why should we add this feature? -**Instance** _(What instance of firefish are you using?)_ -**Contribution Guidelines** +## 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 implement this feature?** (optional) +## 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. diff --git a/.gitlab/merge_request_templates/default.md b/.gitlab/merge_request_templates/default.md index 2a1c926223..d13a146da0 100644 --- a/.gitlab/merge_request_templates/default.md +++ b/.gitlab/merge_request_templates/default.md @@ -1,8 +1,9 @@ -**What does this PR do?** _(Please give us a brief description of what this PR does.)_ +## What does this PR do? -**Contribution Guidelines** + +## 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 diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 0083604d44..de0385be2a 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -12,6 +12,7 @@ "esbenp.prettier-vscode", "redhat.vscode-yaml", "yoavbls.pretty-ts-errors", - "biomejs.biome" + "biomejs.biome", + "rust-lang.rust-analyzer" ] } diff --git a/Cargo.lock b/Cargo.lock index 93c475fa0b..c6ba96e683 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,9 +59,9 @@ checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" [[package]] name = "allocator-api2" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "android-tzdata" @@ -132,6 +132,18 @@ version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "arrayvec" version = "0.7.4" @@ -190,11 +202,14 @@ checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" name = "backend-rs" version = "0.0.0" dependencies = [ + "argon2", "async-trait", "basen", + "bcrypt", "cfg-if", "chrono", "cuid2", + "emojis", "idna", "jsonschema", "macro_rs", @@ -238,6 +253,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" + [[package]] name = "base64ct" version = "1.6.0" @@ -250,6 +271,19 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dbe4bb73fd931c4d1aaf53b35d1286c8a948ad00ec92c8e3c856f15fd027f43" +[[package]] +name = "bcrypt" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e65938ed058ef47d92cf8b346cc76ef48984572ade631927e9937b5ffc7662c7" +dependencies = [ + "base64 0.22.0", + "blowfish", + "getrandom", + "subtle", + "zeroize", +] + [[package]] name = "bigdecimal" version = "0.3.1" @@ -303,6 +337,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -312,6 +355,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "borsh" version = "1.4.0" @@ -384,9 +437,9 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cc" -version = "1.0.92" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2678b2e3449475e95b0aa6f9b506a28e61b3dc8996592b983695e8ebb58a8b41" +checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" [[package]] name = "cfg-if" @@ -412,7 +465,17 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.4", + "windows-targets 0.52.5", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", ] [[package]] @@ -633,13 +696,22 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "either" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" dependencies = [ "serde", ] +[[package]] +name = "emojis" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee61eb945bff65ee7d19d157d39c67c33290ff0742907413fd5eefd29edc979" +dependencies = [ + "phf", +] + [[package]] name = "encoding_rs" version = "0.8.34" @@ -1075,6 +1147,15 @@ dependencies = [ "syn 2.0.58", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "ipnet" version = "2.9.0" @@ -1122,7 +1203,7 @@ checksum = "2a071f4f7efc9a9118dfb627a0a94ef247986e1ab8606a4c806ae2b3aa3b6978" dependencies = [ "ahash 0.8.11", "anyhow", - "base64", + "base64 0.21.7", "bytecount", "clap", "fancy-regex", @@ -1175,7 +1256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -1296,9 +1377,9 @@ dependencies = [ [[package]] name = "napi-build" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f9130fccc5f763cf2069b34a089a18f0d0883c66aceb81f2fad541a3d823c43" +checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a" [[package]] name = "napi-derive" @@ -1350,9 +1431,9 @@ dependencies = [ [[package]] name = "num" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +checksum = "3135b08af27d103b0a51f2ae0f8632117b7b185ccf931445affa8df530576a41" dependencies = [ "num-bigint", "num-complex", @@ -1559,6 +1640,17 @@ dependencies = [ "syn 2.0.58", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "paste" version = "1.0.14" @@ -1580,6 +1672,24 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -1801,7 +1911,7 @@ version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "encoding_rs", "futures-core", @@ -1947,7 +2057,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64", + "base64 0.21.7", ] [[package]] @@ -2231,6 +2341,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "slab" version = "0.4.9" @@ -2398,7 +2514,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" dependencies = [ "atoi", - "base64", + "base64 0.21.7", "bigdecimal", "bitflags 2.5.0", "byteorder", @@ -2445,7 +2561,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" dependencies = [ "atoi", - "base64", + "base64 0.21.7", "bigdecimal", "bitflags 2.5.0", "byteorder", @@ -3041,7 +3157,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -3059,7 +3175,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -3079,17 +3195,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] @@ -3100,9 +3217,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" @@ -3112,9 +3229,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" @@ -3124,9 +3241,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" @@ -3136,9 +3259,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" @@ -3148,9 +3271,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" @@ -3160,9 +3283,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" @@ -3172,9 +3295,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" diff --git a/Cargo.toml b/Cargo.toml index f7ba12d884..7ca43c960b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,14 +7,17 @@ macro_rs = { path = "packages/macro-rs" } napi = { version = "2.16.2", default-features = false } napi-derive = "2.16.2" -napi-build = "2.1.2" +napi-build = "2.1.3" +argon2 = "0.5.3" async-trait = "0.1.80" basen = "0.1.0" +bcrypt = "0.15.1" cfg-if = "1.0.0" chrono = "0.4.37" convert_case = "0.6.0" cuid2 = "0.1.2" +emojis = "0.6.1" idna = "0.5.0" jsonschema = "0.17.1" once_cell = "1.19.0" diff --git a/biome.json b/biome.json index 9bf08ad553..21b711f457 100644 --- a/biome.json +++ b/biome.json @@ -1,7 +1,7 @@ { "$schema": "https://biomejs.dev/schemas/1.6.4/schema.json", "organizeImports": { - "enabled": true + "enabled": false }, "linter": { "enabled": true, @@ -21,7 +21,8 @@ "useImportType": "warn", "useShorthandFunctionType": "warn", "useTemplate": "warn", - "noNonNullAssertion": "off" + "noNonNullAssertion": "off", + "useNodejsImportProtocol": "off" } } } diff --git a/docker-compose.example.yml b/docker-compose.example.yml index fc6c0268a2..9cd6d1cdce 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -17,6 +17,7 @@ services: # - web environment: NODE_ENV: production + NODE_OPTIONS: --max-old-space-size=3072 volumes: - ./custom:/firefish/custom:ro - ./files:/firefish/files diff --git a/docs/api-change.md b/docs/api-change.md index f3ed584c32..dcd4329a27 100644 --- a/docs/api-change.md +++ b/docs/api-change.md @@ -2,6 +2,10 @@ Breaking changes are indicated by the :warning: icon. +## Unreleased + +- Added `antennaLimit` field to the response of `meta` and `admin/meta`, and the request of `admin/update-meta` (optional). + ## v20240413 - :warning: Removed `patrons` endpoint. diff --git a/docs/changelog.md b/docs/changelog.md index a818e09835..70f8b34fbe 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,6 +5,10 @@ Critical security updates are indicated by the :warning: icon. - Server administrators should check [notice-for-admins.md](./notice-for-admins.md) as well. - Third-party client/bot developers may want to check [api-change.md](./api-change.md) as well. +## [v20240421](https://firefish.dev/firefish/firefish/-/merge_requests/10756/commits) + +- Fix bugs + ## [v20240413](https://firefish.dev/firefish/firefish/-/merge_requests/10741/commits) - Add "Media" tab to user page diff --git a/docs/downgrade.sql b/docs/downgrade.sql index 0b6f25e08c..44222f818f 100644 --- a/docs/downgrade.sql +++ b/docs/downgrade.sql @@ -1,6 +1,8 @@ BEGIN; DELETE FROM "migrations" WHERE name IN ( + 'AddDriveFileUsage1713451569342', + 'ConvertCwVarcharToText1713225866247', 'FixChatFileConstraint1712855579316', 'DropTimeZone1712425488543', 'ExpandNoteEdit1711936358554', @@ -22,6 +24,15 @@ DELETE FROM "migrations" WHERE name IN ( 'RemoveNativeUtilsMigration1705877093218' ); +-- AddDriveFileUsage +ALTER TABLE "drive_file" DROP COLUMN "usageHint"; +DROP TYPE "drive_file_usage_hint_enum"; + +-- convert-cw-varchar-to-text +DROP INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f"; +ALTER TABLE "note" ALTER COLUMN "cw" TYPE character varying(512); +CREATE INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f" ON "note" USING "pgroonga" ("cw" pgroonga_varchar_full_text_search_ops_v2); + -- fix-chat-file-constraint ALTER TABLE "messaging_message" DROP CONSTRAINT "FK_535def119223ac05ad3fa9ef64b"; ALTER TABLE "messaging_message" ADD CONSTRAINT "FK_535def119223ac05ad3fa9ef64b" FOREIGN KEY ("fileId") REFERENCES "drive_file"("id") ON DELETE CASCADE ON UPDATE NO ACTION; diff --git a/docs/install.md b/docs/install.md index 061000fa32..324923c6a7 100644 --- a/docs/install.md +++ b/docs/install.md @@ -154,7 +154,7 @@ sudo apt install ffmpeg 1. Build ```sh pnpm install --frozen-lockfile - NODE_ENV=production pnpm run build + NODE_ENV=production NODE_OPTIONS='--max-old-space-size=3072' pnpm run build ``` 1. Execute database migrations ```sh @@ -242,6 +242,7 @@ In this instruction, we use [Caddy](https://caddyserver.com/) to make the Firefi WorkingDirectory=/home/firefish/firefish Environment="NODE_ENV=production" Environment="npm_config_cache=/tmp" + Environment="NODE_OPTIONS=--max-old-space-size=3072" # uncomment the following line if you use jemalloc (note that the path varies on different environments) # Environment="LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2" StandardOutput=journal diff --git a/locales/en-US.yml b/locales/en-US.yml index d552e1e3a2..f44c3d1842 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -394,6 +394,7 @@ enableRegistration: "Enable new user registration" invite: "Invite" driveCapacityPerLocalAccount: "Drive capacity per local user" driveCapacityPerRemoteAccount: "Drive capacity per remote user" +antennaLimit: "The maximum number of antennas that each user can create" inMb: "In megabytes" iconUrl: "Icon URL" bannerUrl: "Banner image URL" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index 9dff23f110..6367dda9f4 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -2318,3 +2318,4 @@ markLocalFilesNsfwByDefaultDescription: Indépendamment de ce réglage, les util peuvent supprimer le drapeau « sensible » (NSFW) eux-mêmes. Les fichiers existants ne sont pas affectés. noteEditHistory: Historique des publications +media: Multimédia diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index c496d05381..c8579a4c1a 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -340,6 +340,7 @@ invite: "邀请" driveCapacityPerLocalAccount: "每个本地用户的网盘容量" driveCapacityPerRemoteAccount: "每个远程用户的网盘容量" inMb: "以兆字节 (MegaByte) 为单位" +antennaLimit: "每个用户最多可以创建的天线数量" iconUrl: "图标 URL" bannerUrl: "横幅图 URL" backgroundImageUrl: "背景图 URL" diff --git a/package.json b/package.json index a7bb0a5cc1..93594a5698 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "firefish", - "version": "20240413", + "version": "20240421", "repository": { "type": "git", "url": "https://firefish.dev/firefish/firefish.git" }, - "packageManager": "pnpm@8.15.6", + "packageManager": "pnpm@8.15.7", "private": true, "scripts": { "rebuild": "pnpm run clean && pnpm run build", @@ -26,7 +26,9 @@ "debug": "pnpm run build:debug && pnpm run start", "build:debug": "pnpm run clean && pnpm node ./scripts/dev-build.mjs && pnpm run gulp", "mocha": "pnpm --filter backend run mocha", - "test": "pnpm run mocha", + "test": "pnpm run test:ts && pnpm run test:rs", + "test:ts": "pnpm run mocha", + "test:rs": "cargo test", "format": "pnpm run format:ts; pnpm run format:rs", "format:ts": "pnpm -r --parallel run format", "format:rs": "cargo fmt --all --", @@ -36,11 +38,11 @@ "clean-all": "pnpm run clean && pnpm run clean-cargo && pnpm run clean-npm" }, "dependencies": { - "js-yaml": "4.1.0", "gulp": "4.0.2", "gulp-cssnano": "2.1.3", "gulp-replace": "1.1.4", - "gulp-terser": "2.1.0" + "gulp-terser": "2.1.0", + "js-yaml": "4.1.0" }, "devDependencies": { "@biomejs/biome": "1.6.4", @@ -50,7 +52,7 @@ "@biomejs/cli-linux-x64": "^1.6.4", "@types/node": "20.12.7", "execa": "8.0.1", - "pnpm": "8.15.6", + "pnpm": "8.15.7", "typescript": "5.4.5" } } diff --git a/packages/README.md b/packages/README.md index 56356ec2d0..6637462910 100644 --- a/packages/README.md +++ b/packages/README.md @@ -4,6 +4,7 @@ This directory contains all of the packages Firefish uses. - `backend`: Main backend code written in TypeScript for NodeJS - `backend-rs`: Backend code written in Rust, bound to NodeJS by [NAPI-RS](https://napi.rs/) +- `macro-rs`: Procedural macros for backend-rs - `client`: Web interface written in Vue3 and TypeScript - `sw`: Web [Service Worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) written in TypeScript - `firefish-js`: TypeScript SDK for both backend and client diff --git a/packages/backend-rs/Cargo.toml b/packages/backend-rs/Cargo.toml index 783f630942..af9e10cdc1 100644 --- a/packages/backend-rs/Cargo.toml +++ b/packages/backend-rs/Cargo.toml @@ -17,11 +17,14 @@ macro_rs = { workspace = true } napi = { workspace = true, optional = true, default-features = false, features = ["napi9", "tokio_rt", "chrono_date", "serde-json"] } napi-derive = { workspace = true, optional = true } +argon2 = { workspace = true, features = ["std"] } async-trait = { workspace = true } basen = { workspace = true } +bcrypt = { workspace = true } cfg-if = { workspace = true } chrono = { workspace = true } cuid2 = { workspace = true } +emojis = { workspace = true } idna = { workspace = true } jsonschema = { workspace = true } once_cell = { workspace = true } diff --git a/packages/backend-rs/Makefile b/packages/backend-rs/Makefile index eb4b30c6df..11b614c82a 100644 --- a/packages/backend-rs/Makefile +++ b/packages/backend-rs/Makefile @@ -17,7 +17,7 @@ regenerate-entities: attribute=$$(printf 'cfg_attr(feature = "napi", napi_derive::napi(object, js_name = "%s", use_nullable = true))' "$${jsname}"); \ sed -i "s/NAPI_EXTRA_ATTR_PLACEHOLDER/$${attribute}/" "$${file}"; \ done - sed -i 's/#\[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)\]/#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]\n#[cfg_attr(not(feature = "napi"), derive(Clone))]\n#[cfg_attr(feature = "napi", napi_derive::napi)]/' \ + sed -i 's/#\[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)\]/#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]\n#[cfg_attr(not(feature = "napi"), derive(Clone))]\n#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]/' \ src/model/entity/sea_orm_active_enums.rs cargo fmt --all -- diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts index 617bedf5b9..04b26c3e5a 100644 --- a/packages/backend-rs/index.d.ts +++ b/packages/backend-rs/index.d.ts @@ -3,6 +3,16 @@ /* auto-generated by NAPI-RS */ +export interface EnvConfig { + onlyQueue: boolean + onlyServer: boolean + noDaemons: boolean + disableClustering: boolean + verbose: boolean + withLogTime: boolean + slow: boolean +} +export function readEnvironmentConfig(): EnvConfig export interface ServerConfig { url: string port: number @@ -118,7 +128,8 @@ export interface Acct { } export function stringToAcct(acct: string): Acct export function acctToString(acct: Acct): string -export interface NoteLike { +/** TODO: handle name collisions better */ +export interface NoteLikeForCheckWordMute { fileIds: Array userId: string | null text: string | null @@ -126,15 +137,52 @@ export interface NoteLike { renoteId: string | null replyId: string | null } -export function checkWordMute(note: NoteLike, mutedWordLists: Array>, mutedPatterns: Array): Promise +export function checkWordMute(note: NoteLikeForCheckWordMute, mutedWordLists: 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 +export function sqlLikeEscape(src: string): string +export function safeForSql(src: string): boolean +/** Convert milliseconds to a human readable string */ +export function formatMilliseconds(milliseconds: number): string +/** TODO: handle name collisions better */ +export interface NoteLikeForGetNoteSummary { + fileIds: Array + text: string | null + cw: string | null + hasPoll: boolean +} +export function getNoteSummary(note: NoteLikeForGetNoteSummary): string 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 export function nyaify(text: string, lang?: string | undefined | null): string +export function hashPassword(password: string): string +export function verifyPassword(password: string, hash: string): boolean +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 export interface AbuseUserReport { id: string createdAt: Date @@ -300,6 +348,7 @@ export interface DriveFile { webpublicType: string | null requestHeaders: Json | null requestIp: string | null + usageHint: DriveFileUsageHintEnum | null } export interface DriveFolder { id: string @@ -443,6 +492,7 @@ export interface Meta { recaptchaSecretKey: string | null localDriveCapacityMb: number remoteDriveCapacityMb: number + antennaLimit: number summalyProxy: string | null enableEmail: boolean email: string | null @@ -724,81 +774,85 @@ export interface ReplyMuting { muteeId: string muterId: string } -export const enum AntennaSrcEnum { - All = 0, - Group = 1, - Home = 2, - Instances = 3, - List = 4, - Users = 5 +export enum AntennaSrcEnum { + All = 'all', + Group = 'group', + Home = 'home', + Instances = 'instances', + List = 'list', + Users = 'users' } -export const enum MutedNoteReasonEnum { - Manual = 0, - Other = 1, - Spam = 2, - Word = 3 +export enum DriveFileUsageHintEnum { + UserAvatar = 'userAvatar', + UserBanner = 'userBanner' } -export const enum NoteVisibilityEnum { - Followers = 0, - Hidden = 1, - Home = 2, - Public = 3, - Specified = 4 +export enum MutedNoteReasonEnum { + Manual = 'manual', + Other = 'other', + Spam = 'spam', + Word = 'word' } -export const enum NotificationTypeEnum { - App = 0, - Follow = 1, - FollowRequestAccepted = 2, - GroupInvited = 3, - Mention = 4, - PollEnded = 5, - PollVote = 6, - Quote = 7, - Reaction = 8, - ReceiveFollowRequest = 9, - Renote = 10, - Reply = 11 +export enum NoteVisibilityEnum { + Followers = 'followers', + Hidden = 'hidden', + Home = 'home', + Public = 'public', + Specified = 'specified' } -export const enum PageVisibilityEnum { - Followers = 0, - Public = 1, - Specified = 2 +export enum NotificationTypeEnum { + 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 const enum PollNotevisibilityEnum { - Followers = 0, - Home = 1, - Public = 2, - Specified = 3 +export enum PageVisibilityEnum { + Followers = 'followers', + Public = 'public', + Specified = 'specified' } -export const enum RelayStatusEnum { - Accepted = 0, - Rejected = 1, - Requesting = 2 +export enum PollNotevisibilityEnum { + Followers = 'followers', + Home = 'home', + Public = 'public', + Specified = 'specified' } -export const enum UserEmojimodpermEnum { - Add = 0, - Full = 1, - Mod = 2, - Unauthorized = 3 +export enum RelayStatusEnum { + Accepted = 'accepted', + Rejected = 'rejected', + Requesting = 'requesting' } -export const enum UserProfileFfvisibilityEnum { - Followers = 0, - Private = 1, - Public = 2 +export enum UserEmojimodpermEnum { + Add = 'add', + Full = 'full', + Mod = 'mod', + Unauthorized = 'unauthorized' } -export const enum UserProfileMutingnotificationtypesEnum { - App = 0, - Follow = 1, - FollowRequestAccepted = 2, - GroupInvited = 3, - Mention = 4, - PollEnded = 5, - PollVote = 6, - Quote = 7, - Reaction = 8, - ReceiveFollowRequest = 9, - Renote = 10, - Reply = 11 +export enum UserProfileFfvisibilityEnum { + Followers = 'followers', + Private = 'private', + Public = 'public' +} +export enum UserProfileMutingnotificationtypesEnum { + 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 Signin { id: string diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js index f2942a9aa5..6d64f6ec75 100644 --- a/packages/backend-rs/index.js +++ b/packages/backend-rs/index.js @@ -310,8 +310,9 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, toMastodonId, fromMastodonId, nyaify, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding +const { readEnvironmentConfig, readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getNoteSummary, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding +module.exports.readEnvironmentConfig = readEnvironmentConfig module.exports.readServerConfig = readServerConfig module.exports.stringToAcct = stringToAcct module.exports.acctToString = acctToString @@ -321,10 +322,24 @@ 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.getNoteSummary = getNoteSummary 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.AntennaSrcEnum = AntennaSrcEnum +module.exports.DriveFileUsageHintEnum = DriveFileUsageHintEnum module.exports.MutedNoteReasonEnum = MutedNoteReasonEnum module.exports.NoteVisibilityEnum = NoteVisibilityEnum module.exports.NotificationTypeEnum = NotificationTypeEnum diff --git a/packages/backend-rs/package.json b/packages/backend-rs/package.json index 69ae09fa49..1f3f49e9fb 100644 --- a/packages/backend-rs/package.json +++ b/packages/backend-rs/package.json @@ -33,8 +33,8 @@ }, "scripts": { "artifacts": "napi artifacts", - "build": "napi build --features napi --platform --release ./built/", - "build:debug": "napi build --features napi --platform ./built/", + "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", "test": "pnpm run cargo:test && pnpm run build:debug && ava", "universal": "napi universal", diff --git a/packages/backend-rs/src/config/environment.rs b/packages/backend-rs/src/config/environment.rs new file mode 100644 index 0000000000..7d66aec7ba --- /dev/null +++ b/packages/backend-rs/src/config/environment.rs @@ -0,0 +1,27 @@ +// 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 read_environment_config() -> 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/config/mod.rs b/packages/backend-rs/src/config/mod.rs index 74f47ad347..b708f2b265 100644 --- a/packages/backend-rs/src/config/mod.rs +++ b/packages/backend-rs/src/config/mod.rs @@ -1 +1,2 @@ +pub mod environment; pub mod server; diff --git a/packages/backend-rs/src/misc/check_word_mute.rs b/packages/backend-rs/src/misc/check_word_mute.rs index 801175c2af..18b550c29b 100644 --- a/packages/backend-rs/src/misc/check_word_mute.rs +++ b/packages/backend-rs/src/misc/check_word_mute.rs @@ -4,7 +4,8 @@ use once_cell::sync::Lazy; use regex::Regex; use sea_orm::{prelude::*, QuerySelect}; -#[crate::export(object)] +/// TODO: handle name collisions better +#[crate::export(object, js_name = "NoteLikeForCheckWordMute")] pub struct NoteLike { pub file_ids: Vec, pub user_id: Option, diff --git a/packages/backend-rs/src/misc/emoji.rs b/packages/backend-rs/src/misc/emoji.rs new file mode 100644 index 0000000000..df7d33848c --- /dev/null +++ b/packages/backend-rs/src/misc/emoji.rs @@ -0,0 +1,31 @@ +#[inline] +#[crate::export] +pub fn is_unicode_emoji(s: &str) -> bool { + emojis::get(s).is_some() +} + +#[cfg(test)] +mod unit_test { + use super::is_unicode_emoji; + + #[test] + fn test_unicode_emoji_check() { + assert!(is_unicode_emoji("⭐")); + assert!(is_unicode_emoji("👍")); + assert!(is_unicode_emoji("❤")); + assert!(is_unicode_emoji("♥️")); + assert!(is_unicode_emoji("❤️")); + assert!(is_unicode_emoji("💙")); + assert!(is_unicode_emoji("🩷")); + assert!(is_unicode_emoji("🖖🏿")); + assert!(is_unicode_emoji("🏃‍➡️")); + assert!(is_unicode_emoji("👩‍❤️‍👨")); + assert!(is_unicode_emoji("👩‍👦‍👦")); + assert!(is_unicode_emoji("🏳️‍🌈")); + + assert!(!is_unicode_emoji("⭐⭐")); + assert!(!is_unicode_emoji("x")); + assert!(!is_unicode_emoji("\t")); + assert!(!is_unicode_emoji(":meow_aww:")); + } +} diff --git a/packages/backend-rs/src/misc/escape_sql.rs b/packages/backend-rs/src/misc/escape_sql.rs new file mode 100644 index 0000000000..c575e088ce --- /dev/null +++ b/packages/backend-rs/src/misc/escape_sql.rs @@ -0,0 +1,36 @@ +#[crate::export] +pub fn sql_like_escape(src: &str) -> String { + src.replace('%', r"\%").replace('_', r"\_") +} + +#[crate::export] +pub fn safe_for_sql(src: &str) -> bool { + !src.contains([ + '\0', '\x08', '\x09', '\x1a', '\n', '\r', '"', '\'', '\\', '%', + ]) +} + +#[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"\_اللغة العربية"); + } + + #[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%")); + } +} diff --git a/packages/backend-rs/src/misc/format_milliseconds.rs b/packages/backend-rs/src/misc/format_milliseconds.rs new file mode 100644 index 0000000000..dfa8df6f62 --- /dev/null +++ b/packages/backend-rs/src/misc/format_milliseconds.rs @@ -0,0 +1,46 @@ +/// Convert milliseconds to a human readable string +#[crate::export] +pub fn format_milliseconds(milliseconds: u32) -> String { + let mut seconds = milliseconds / 1000; + let mut minutes = seconds / 60; + let mut hours = minutes / 60; + let days = hours / 24; + + seconds %= 60; + minutes %= 60; + hours %= 24; + + let mut buf: Vec = vec![]; + + if days > 0 { + buf.push(format!("{} day(s)", days)); + } + if hours > 0 { + buf.push(format!("{} hour(s)", hours)); + } + if minutes > 0 { + buf.push(format!("{} minute(s)", minutes)); + } + if seconds > 0 { + buf.push(format!("{} second(s)", seconds)); + } + + buf.join(", ") +} + +#[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)"); + assert_eq!( + format_milliseconds(998244353), + "11 day(s), 13 hour(s), 17 minute(s), 24 second(s)" + ); + } +} diff --git a/packages/backend-rs/src/misc/get_note_summary.rs b/packages/backend-rs/src/misc/get_note_summary.rs new file mode 100644 index 0000000000..3b759b04f5 --- /dev/null +++ b/packages/backend-rs/src/misc/get_note_summary.rs @@ -0,0 +1,90 @@ +/// TODO: handle name collisions better +#[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/meta.rs b/packages/backend-rs/src/misc/meta.rs new file mode 100644 index 0000000000..5aed617038 --- /dev/null +++ b/packages/backend-rs/src/misc/meta.rs @@ -0,0 +1,83 @@ +use crate::database::db_conn; +use crate::model::entity::meta; +use rand::prelude::*; +use sea_orm::{prelude::*, ActiveValue}; +use std::sync::Mutex; + +type Meta = meta::Model; + +static CACHE: Mutex> = Mutex::new(None); +fn update_cache(meta: &Meta) { + let _ = CACHE.lock().map(|mut cache| *cache = Some(meta.clone())); +} + +#[crate::export] +pub async fn fetch_meta(use_cache: bool) -> Result { + // try using cache + if use_cache { + if let Some(cache) = CACHE.lock().ok().and_then(|cache| cache.clone()) { + return Ok(cache); + } + } + + // try fetching from db + let db = db_conn().await?; + let meta = meta::Entity::find().one(db).await?; + if let Some(meta) = meta { + update_cache(&meta); + return Ok(meta); + } + + // create a new meta object and insert into db + let meta = meta::Entity::insert(meta::ActiveModel { + id: ActiveValue::Set("x".to_owned()), + ..Default::default() + }) + .exec_with_returning(db) + .await?; + update_cache(&meta); + Ok(meta) +} + +#[crate::export(object)] +pub struct PugArgs { + pub img: Option, + pub title: String, + pub instance_name: String, + pub desc: Option, + pub icon: Option, + pub splash_icon: Option, + pub theme_color: Option, + pub random_motd: String, + pub private_mode: Option, +} + +#[crate::export] +pub fn meta_to_pug_args(meta: Meta) -> PugArgs { + let mut rng = rand::thread_rng(); + + let splash_icon = meta + .custom_splash_icons + .choose(&mut rng) + .map(|s| s.to_owned()) + .or_else(|| meta.icon_url.to_owned()); + + let random_motd = meta + .custom_motd + .choose(&mut rng) + .map(|s| s.to_owned()) + .unwrap_or_else(|| "Loading...".to_owned()); + + let name = meta.name.unwrap_or_else(|| "Firefish".to_owned()); + PugArgs { + img: meta.banner_url, + title: name.clone(), + instance_name: name.clone(), + desc: meta.description, + icon: meta.icon_url, + splash_icon, + theme_color: meta.theme_color, + random_motd, + private_mode: meta.private_mode, + } +} diff --git a/packages/backend-rs/src/misc/mod.rs b/packages/backend-rs/src/misc/mod.rs index 701e35a4eb..a9d7074dbf 100644 --- a/packages/backend-rs/src/misc/mod.rs +++ b/packages/backend-rs/src/misc/mod.rs @@ -1,5 +1,12 @@ pub mod acct; pub mod check_word_mute; pub mod convert_host; +pub mod emoji; +pub mod escape_sql; +pub mod format_milliseconds; +pub mod get_note_summary; pub mod mastodon_id; +pub mod meta; pub mod nyaify; +pub mod password; +pub mod reaction; diff --git a/packages/backend-rs/src/misc/password.rs b/packages/backend-rs/src/misc/password.rs new file mode 100644 index 0000000000..b21ff73499 --- /dev/null +++ b/packages/backend-rs/src/misc/password.rs @@ -0,0 +1,69 @@ +use argon2::{ + password_hash, + password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, + Argon2, +}; + +#[crate::export] +pub fn hash_password(password: &str) -> Result { + let salt = SaltString::generate(&mut OsRng); + Ok(Argon2::default() + .hash_password(password.as_bytes(), &salt)? + .to_string()) +} + +#[derive(thiserror::Error, Debug)] +pub enum VerifyError { + #[error("An error occured while bcrypt verification: {0}")] + BcryptError(#[from] bcrypt::BcryptError), + #[error("Invalid argon2 password hash: {0}")] + InvalidArgon2Hash(#[from] password_hash::Error), + #[error("An error occured while argon2 verification: {0}")] + Argon2Error(#[from] argon2::Error), +} + +#[crate::export] +pub fn verify_password(password: &str, hash: &str) -> Result { + if is_old_password_algorithm(hash) { + Ok(bcrypt::verify(password, hash)?) + } else { + let parsed_hash = PasswordHash::new(hash)?; + Ok(Argon2::default() + .verify_password(password.as_bytes(), &parsed_hash) + .is_ok()) + } +} + +#[inline] +#[crate::export] +pub fn is_old_password_algorithm(hash: &str) -> bool { + // bcrypt hashes start with $2[ab]$ + hash.starts_with("$2") +} + +#[cfg(test)] +mod unit_test { + use super::{hash_password, is_old_password_algorithm, verify_password}; + + #[test] + fn verify_password_test() { + let password = "omWc*%sD^fn7o2cXmc9e2QasBdrbRuhNB*gx!J5"; + + let hash = hash_password(password).unwrap(); + assert!(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"; + let bcrypt_hash = "$2a$12$WzUc.20jgbHmQjUMqTr8vOhKqYbS1BUvubapv/GLjCK1IN.h4e4la"; + let bcrypt_invalid_hash = "$2a$12$WzUc.20jgbHmQjUMqTr7vOhKqYbS1BUvubapv/GLjCK1IN.h4e4la"; + + 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!(!verify_password(password, argon2_invalid_hash).unwrap()); + assert!(!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 new file mode 100644 index 0000000000..a29ddf95de --- /dev/null +++ b/packages/backend-rs/src/misc/reaction.rs @@ -0,0 +1,191 @@ +use crate::database::db_conn; +use crate::misc::{convert_host::to_puny, emoji::is_unicode_emoji, meta::fetch_meta}; +use crate::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)] +pub struct DecodedReaction { + pub reaction: String, + pub name: Option, + pub host: Option, +} + +#[crate::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 + // Misskey's implementation: https://github.com/misskey-dev/misskey/blob/bba3097765317cbf95d09627961b5b5dce16a972/packages/backend/src/core/ReactionService.ts#L68 + static RE: Lazy = + Lazy::new(|| Regex::new(r"^:([0-9A-Za-z_+-]+)(?:@([0-9A-Za-z_.-]+))?:$").unwrap()); + + if let Some(captures) = RE.captures(reaction) { + let name = &captures[1]; + let host = captures.get(2).map(|s| s.as_str()); + + DecodedReaction { + reaction: format!(":{}@{}:", name, host.unwrap_or(".")), + name: Some(name.to_owned()), + host: host.map(|s| s.to_owned()), + } + } else { + DecodedReaction { + reaction: reaction.to_owned(), + name: None, + host: None, + } + } +} + +#[crate::export] +pub fn count_reactions(reactions: &HashMap) -> HashMap { + let mut res = HashMap::::new(); + + for (reaction, count) in reactions.iter() { + if count > &0 { + let decoded = decode_reaction(reaction).reaction; + let total = res.entry(decoded).or_insert(0); + *total += count; + } + } + + res +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Idna error: {0}")] + IdnaError(#[from] idna::Errors), + #[error("Database error: {0}")] + DbError(#[from] DbErr), +} + +#[crate::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? + // This was introduced in https://firefish.dev/firefish/firefish/-/commit/af730e75b6fc1a57ca680ce83459d7e433b130cf + if reaction.contains('❤') || reaction.contains("♥️") { + return Ok("❤️".to_owned()); + } + + if is_unicode_emoji(reaction) { + return Ok(reaction.to_owned()); + } + + static RE: Lazy = + Lazy::new(|| Regex::new(r"^:([0-9A-Za-z_+-]+)(?:@\.)?:$").unwrap()); + + if let Some(captures) = RE.captures(reaction) { + let name = &captures[1]; + let db = db_conn().await?; + + if let Some(host) = host { + // remote emoji + let ascii_host = to_puny(host)?; + + // TODO: Does SeaORM have the `exists` method? + if emoji::Entity::find() + .filter(emoji::Column::Name.eq(name)) + .filter(emoji::Column::Host.eq(&ascii_host)) + .one(db) + .await? + .is_some() + { + return Ok(format!(":{name}@{ascii_host}:")); + } + } else { + // local emoji + // TODO: Does SeaORM have the `exists` method? + if emoji::Entity::find() + .filter(emoji::Column::Name.eq(name)) + .filter(emoji::Column::Host.is_null()) + .one(db) + .await? + .is_some() + { + return Ok(format!(":{name}:")); + } + } + }; + }; + + Ok(fetch_meta(true).await?.default_reaction) +} + +#[cfg(test)] +mod unit_test { + use super::{decode_reaction, DecodedReaction}; + use pretty_assertions::{assert_eq, assert_ne}; + + #[test] + fn test_decode_reaction() { + let unicode_emoji_1 = DecodedReaction { + reaction: "⭐".to_string(), + name: None, + host: None, + }; + let unicode_emoji_2 = DecodedReaction { + reaction: "🩷".to_string(), + name: None, + host: None, + }; + + assert_eq!(decode_reaction("⭐"), unicode_emoji_1); + assert_eq!(decode_reaction("🩷"), unicode_emoji_2); + + assert_ne!(decode_reaction("⭐"), unicode_emoji_2); + assert_ne!(decode_reaction("🩷"), unicode_emoji_1); + + let unicode_emoji_3 = DecodedReaction { + reaction: "🖖🏿".to_string(), + name: None, + host: None, + }; + assert_eq!(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); + + let remote_emoji_1 = DecodedReaction { + reaction: ":meow_uwu@some-domain.example.org:".to_string(), + name: Some("meow_uwu".to_string()), + host: Some("some-domain.example.org".to_string()), + }; + assert_eq!( + decode_reaction(":meow_uwu@some-domain.example.org:"), + remote_emoji_1 + ); + + let remote_emoji_2 = DecodedReaction { + reaction: ":C++23@xn--eckwd4c7c.example.org:".to_string(), + name: Some("C++23".to_string()), + host: Some("xn--eckwd4c7c.example.org".to_string()), + }; + assert_eq!( + decode_reaction(":C++23@xn--eckwd4c7c.example.org:"), + remote_emoji_2 + ); + + let invalid_reaction_1 = DecodedReaction { + reaction: ":foo".to_string(), + name: None, + host: None, + }; + assert_eq!(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); + } +} diff --git a/packages/backend-rs/src/model/entity/drive_file.rs b/packages/backend-rs/src/model/entity/drive_file.rs index e3e4622a62..a6926e7af2 100644 --- a/packages/backend-rs/src/model/entity/drive_file.rs +++ b/packages/backend-rs/src/model/entity/drive_file.rs @@ -1,5 +1,6 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 +use super::sea_orm_active_enums::DriveFileUsageHintEnum; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] @@ -52,6 +53,8 @@ pub struct Model { pub request_headers: Option, #[sea_orm(column_name = "requestIp")] pub request_ip: Option, + #[sea_orm(column_name = "usageHint")] + pub usage_hint: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/packages/backend-rs/src/model/entity/meta.rs b/packages/backend-rs/src/model/entity/meta.rs index b9a89914bd..fcba9e0be9 100644 --- a/packages/backend-rs/src/model/entity/meta.rs +++ b/packages/backend-rs/src/model/entity/meta.rs @@ -173,6 +173,8 @@ pub struct Model { pub more_urls: Json, #[sea_orm(column_name = "markLocalFilesNsfwByDefault")] pub mark_local_files_nsfw_by_default: bool, + #[sea_orm(column_name = "antennaLimit")] + pub antenna_limit: i32, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/packages/backend-rs/src/model/entity/note.rs b/packages/backend-rs/src/model/entity/note.rs index cb82f3d94a..5903216c1a 100644 --- a/packages/backend-rs/src/model/entity/note.rs +++ b/packages/backend-rs/src/model/entity/note.rs @@ -21,6 +21,7 @@ pub struct Model { #[sea_orm(column_type = "Text", nullable)] pub text: Option, pub name: Option, + #[sea_orm(column_type = "Text", nullable)] pub cw: Option, #[sea_orm(column_name = "userId")] pub user_id: String, 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 861b3a18d0..36281f4dc5 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 @@ -4,7 +4,7 @@ use sea_orm::entity::prelude::*; #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] #[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi)] +#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "antenna_src_enum")] pub enum AntennaSrcEnum { #[sea_orm(string_value = "all")] @@ -22,7 +22,21 @@ pub enum AntennaSrcEnum { } #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] #[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi)] +#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] +#[sea_orm( + rs_type = "String", + db_type = "Enum", + enum_name = "drive_file_usage_hint_enum" +)] +pub enum DriveFileUsageHintEnum { + #[sea_orm(string_value = "userAvatar")] + UserAvatar, + #[sea_orm(string_value = "userBanner")] + UserBanner, +} +#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] +#[cfg_attr(not(feature = "napi"), derive(Clone))] +#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] #[sea_orm( rs_type = "String", db_type = "Enum", @@ -40,7 +54,7 @@ pub enum MutedNoteReasonEnum { } #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] #[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi)] +#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] #[sea_orm( rs_type = "String", db_type = "Enum", @@ -60,7 +74,7 @@ pub enum NoteVisibilityEnum { } #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] #[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi)] +#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] #[sea_orm( rs_type = "String", db_type = "Enum", @@ -94,7 +108,7 @@ pub enum NotificationTypeEnum { } #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] #[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi)] +#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] #[sea_orm( rs_type = "String", db_type = "Enum", @@ -110,7 +124,7 @@ pub enum PageVisibilityEnum { } #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] #[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi)] +#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] #[sea_orm( rs_type = "String", db_type = "Enum", @@ -128,7 +142,7 @@ pub enum PollNotevisibilityEnum { } #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] #[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi)] +#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "relay_status_enum")] pub enum RelayStatusEnum { #[sea_orm(string_value = "accepted")] @@ -140,7 +154,7 @@ pub enum RelayStatusEnum { } #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] #[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi)] +#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] #[sea_orm( rs_type = "String", db_type = "Enum", @@ -158,7 +172,7 @@ pub enum UserEmojimodpermEnum { } #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] #[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi)] +#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] #[sea_orm( rs_type = "String", db_type = "Enum", @@ -174,7 +188,7 @@ pub enum UserProfileFfvisibilityEnum { } #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] #[cfg_attr(not(feature = "napi"), derive(Clone))] -#[cfg_attr(feature = "napi", napi_derive::napi)] +#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] #[sea_orm( rs_type = "String", db_type = "Enum", diff --git a/packages/backend/package.json b/packages/backend/package.json index 5382f1d361..9289c2f7ea 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -22,9 +22,9 @@ "@swc/core-android-arm64": "1.3.11" }, "dependencies": { - "@bull-board/api": "5.15.3", - "@bull-board/koa": "5.15.3", - "@bull-board/ui": "5.15.3", + "@bull-board/api": "5.15.5", + "@bull-board/koa": "5.15.5", + "@bull-board/ui": "5.15.5", "@discordapp/twemoji": "^15.0.3", "@koa/cors": "5.0.0", "@koa/multer": "3.0.2", @@ -33,15 +33,12 @@ "@peertube/http-signature": "1.7.0", "@redocly/openapi-core": "1.11.0", "@sinonjs/fake-timers": "11.2.2", - "@twemoji/parser": "^15.1.1", "adm-zip": "0.5.10", "ajv": "8.12.0", "archiver": "7.0.1", - "argon2": "^0.40.1", - "aws-sdk": "2.1597.0", + "aws-sdk": "2.1599.0", "axios": "^1.6.8", "backend-rs": "workspace:*", - "bcryptjs": "2.4.3", "blurhash": "2.0.5", "bull": "4.12.2", "cacheable-lookup": "TheEssem/cacheable-lookup", @@ -54,7 +51,7 @@ "date-fns": "3.6.0", "decompress": "^4.2.1", "deep-email-validator": "0.1.21", - "deepl-node": "1.12.0", + "deepl-node": "1.13.0", "escape-regexp": "0.0.1", "feed": "4.2.2", "file-type": "19.0.0", @@ -100,7 +97,7 @@ "punycode": "2.3.1", "pureimage": "0.4.13", "qrcode": "1.5.3", - "qs": "6.12.0", + "qs": "6.12.1", "random-seed": "0.3.0", "ratelimiter": "3.4.1", "redis-semaphore": "5.5.1", @@ -130,7 +127,6 @@ "@swc/cli": "0.3.12", "@swc/core": "1.4.13", "@types/adm-zip": "^0.5.5", - "@types/bcryptjs": "2.4.6", "@types/color-convert": "^2.0.3", "@types/content-disposition": "^0.5.8", "@types/escape-regexp": "0.0.3", diff --git a/packages/backend/src/boot/index.ts b/packages/backend/src/boot/index.ts index 8caf7d062e..9854d2dce4 100644 --- a/packages/backend/src/boot/index.ts +++ b/packages/backend/src/boot/index.ts @@ -3,7 +3,7 @@ import chalk from "chalk"; import Xev from "xev"; import Logger from "@/services/logger.js"; -import { envOption } from "../env.js"; +import { envOption } from "@/config/index.js"; import { inspect } from "node:util"; // for typeorm @@ -76,9 +76,7 @@ cluster.on("exit", (worker) => { }); // Display detail of unhandled promise rejection -if (!envOption.quiet) { - process.on("unhandledRejection", console.dir); -} +process.on("unhandledRejection", console.dir); // Display detail of uncaught exception process.on("uncaughtException", (err) => { diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index e54a2889e2..3ba2d0cf50 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -10,7 +10,7 @@ import semver from "semver"; import Logger from "@/services/logger.js"; import loadConfig from "@/config/load.js"; import type { Config } from "@/config/types.js"; -import { envOption } from "@/env.js"; +import { envOption } from "@/config/index.js"; import { showMachineInfo } from "@/misc/show-machine-info.js"; import { db, initDb } from "@/db/postgre.js"; import { inspect } from "node:util"; @@ -28,58 +28,56 @@ const bootLogger = logger.createSubLogger("boot", "magenta", false); const themeColor = chalk.hex("#31748f"); function greet() { - if (!envOption.quiet) { - //#region Firefish logo - console.log( - themeColor( - "██████╗ ██╗██████╗ ███████╗███████╗██╗███████╗██╗ ██╗ ○ ▄ ▄ ", - ), - ); - console.log( - themeColor( - "██╔════╝██║██╔══██╗██╔════╝██╔════╝██║██╔════╝██║ ██║ ⚬ █▄▄ █▄▄ ", - ), - ); - console.log( - themeColor( - "█████╗ ██║██████╔╝█████╗ █████╗ ██║███████╗███████║ ▄▄▄▄▄▄ ▄ ", - ), - ); - console.log( - themeColor( - "██╔══╝ ██║██╔══██╗██╔══╝ ██╔══╝ ██║╚════██║██╔══██║ █ █ █▄▄ ", - ), - ); - console.log( - themeColor( - "██║ ██║██║ ██║███████╗██║ ██║███████║██║ ██║ █ ● ● █ ", - ), - ); - console.log( - themeColor( - "╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ▀▄▄▄▄▄▄▀ ", - ), - ); - //#endregion + //#region Firefish logo + console.log( + themeColor( + "██████╗ ██╗██████╗ ███████╗███████╗██╗███████╗██╗ ██╗ ○ ▄ ▄ ", + ), + ); + console.log( + themeColor( + "██╔════╝██║██╔══██╗██╔════╝██╔════╝██║██╔════╝██║ ██║ ⚬ █▄▄ █▄▄ ", + ), + ); + console.log( + themeColor( + "█████╗ ██║██████╔╝█████╗ █████╗ ██║███████╗███████║ ▄▄▄▄▄▄ ▄ ", + ), + ); + console.log( + themeColor( + "██╔══╝ ██║██╔══██╗██╔══╝ ██╔══╝ ██║╚════██║██╔══██║ █ █ █▄▄ ", + ), + ); + console.log( + themeColor( + "██║ ██║██║ ██║███████╗██║ ██║███████║██║ ██║ █ ● ● █ ", + ), + ); + console.log( + themeColor( + "╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ▀▄▄▄▄▄▄▀ ", + ), + ); + //#endregion - console.log( - " Firefish is an open-source decentralized microblogging platform.", - ); - console.log( - chalk.rgb( - 255, - 136, - 0, - )( - " If you like Firefish, please consider contributing to the repo. https://firefish.dev/firefish/firefish", - ), - ); + console.log( + " Firefish is an open-source decentralized microblogging platform.", + ); + console.log( + chalk.rgb( + 255, + 136, + 0, + )( + " If you like Firefish, please consider contributing to the repo. https://firefish.dev/firefish/firefish", + ), + ); - console.log(""); - console.log( - chalkTemplate`--- ${os.hostname()} {gray (PID: ${process.pid.toString()})} ---`, - ); - } + console.log(""); + console.log( + chalkTemplate`--- ${os.hostname()} {gray (PID: ${process.pid.toString()})} ---`, + ); bootLogger.info("Welcome to Firefish!"); bootLogger.info(`Firefish v${meta.version}`, null, true); diff --git a/packages/backend/src/config/index.ts b/packages/backend/src/config/index.ts index ae197b09ca..fe87e5026a 100644 --- a/packages/backend/src/config/index.ts +++ b/packages/backend/src/config/index.ts @@ -1,3 +1,5 @@ import load from "./load.js"; +import { readEnvironmentConfig } from "backend-rs"; export default load(); +export const envOption = readEnvironmentConfig(); diff --git a/packages/backend/src/config/load.ts b/packages/backend/src/config/load.ts index 2b286b9439..682bf309d2 100644 --- a/packages/backend/src/config/load.ts +++ b/packages/backend/src/config/load.ts @@ -55,6 +55,7 @@ export default function load() { mixin.userAgent = `Firefish/${meta.version} (${config.url})`; mixin.clientEntry = clientManifest["src/init.ts"]; + if (config.proxyRemoteFiles == null) config.proxyRemoteFiles = true; if (!config.redis.prefix) config.redis.prefix = mixin.hostname; if (config.cacheServer && !config.cacheServer.prefix) config.cacheServer.prefix = mixin.hostname; diff --git a/packages/backend/src/daemons/server-stats.ts b/packages/backend/src/daemons/server-stats.ts index 58a9b1491b..df1f9b3032 100644 --- a/packages/backend/src/daemons/server-stats.ts +++ b/packages/backend/src/daemons/server-stats.ts @@ -1,7 +1,7 @@ import si from "systeminformation"; import Xev from "xev"; import * as osUtils from "os-utils"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; const ev = new Xev(); @@ -13,16 +13,15 @@ const round = (num: number) => Math.round(num * 10) / 10; /** * Report server stats regularly */ -export default function () { +export default async function () { const log = [] as any[]; ev.on("requestServerStatsLog", (x) => { ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length || 50)); }); - fetchMeta().then((meta) => { - if (!meta.enableServerMachineStats) return; - }); + const meta = await fetchMeta(true); + if (!meta.enableServerMachineStats) return; async function tick() { const cpu = await cpuUsage(); diff --git a/packages/backend/src/db/postgre.ts b/packages/backend/src/db/postgre.ts index 19f7e7816a..6baccaa271 100644 --- a/packages/backend/src/db/postgre.ts +++ b/packages/backend/src/db/postgre.ts @@ -237,31 +237,3 @@ export async function initDb(force = false) { await db.initialize(); } } - -export async function resetDb() { - const reset = async () => { - await redisClient.flushdb(); - const tables = await db.query(`SELECT relname AS "table" - FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) - WHERE nspname NOT IN ('pg_catalog', 'information_schema') - AND C.relkind = 'r' - AND nspname !~ '^pg_toast';`); - for (const table of tables) { - await db.query(`DELETE FROM "${table.table}" CASCADE`); - } - }; - - for (let i = 1; i <= 3; i++) { - try { - await reset(); - } catch (e) { - if (i === 3) { - throw e; - } else { - await new Promise((resolve) => setTimeout(resolve, 1000)); - continue; - } - } - break; - } -} diff --git a/packages/backend/src/env.ts b/packages/backend/src/env.ts deleted file mode 100644 index a788a0fba2..0000000000 --- a/packages/backend/src/env.ts +++ /dev/null @@ -1,25 +0,0 @@ -const envOption = { - onlyQueue: false, - onlyServer: false, - noDaemons: false, - disableClustering: false, - verbose: false, - withLogTime: false, - quiet: false, - slow: false, -}; - -for (const key of Object.keys(envOption) as (keyof typeof envOption)[]) { - if ( - process.env[ - `MK_${key.replace(/[A-Z]/g, (letter) => `_${letter}`).toUpperCase()}` - ] - ) - envOption[key] = true; -} - -if (process.env.NODE_ENV === "test") envOption.disableClustering = true; -if (process.env.NODE_ENV === "test") envOption.quiet = true; -if (process.env.NODE_ENV === "test") envOption.noDaemons = true; - -export { envOption }; diff --git a/packages/backend/src/migration/1712937600000-antennaLimit.ts b/packages/backend/src/migration/1712937600000-antennaLimit.ts new file mode 100644 index 0000000000..cd8f9ff658 --- /dev/null +++ b/packages/backend/src/migration/1712937600000-antennaLimit.ts @@ -0,0 +1,19 @@ +import type { MigrationInterface, QueryRunner } from "typeorm"; + +export class antennaLimit1712937600000 implements MigrationInterface { + async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "meta" ADD "antennaLimit" integer NOT NULL DEFAULT 5`, + undefined, + ); + await queryRunner.query( + `COMMENT ON COLUMN "meta"."antennaLimit" IS 'Antenna Limit'`, + ); + } + async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "meta" DROP COLUMN "antennaLimit"`, + undefined, + ); + } +} diff --git a/packages/backend/src/migration/1713225866247-convert-cw-varchar-to-text.ts b/packages/backend/src/migration/1713225866247-convert-cw-varchar-to-text.ts new file mode 100644 index 0000000000..93c87a98a8 --- /dev/null +++ b/packages/backend/src/migration/1713225866247-convert-cw-varchar-to-text.ts @@ -0,0 +1,21 @@ +import type { MigrationInterface, QueryRunner } from "typeorm"; + +export class ConvertCwVarcharToText1713225866247 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + queryRunner.query(`DROP INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f"`); + queryRunner.query(`ALTER TABLE "note" ALTER COLUMN "cw" TYPE text`); + queryRunner.query( + `CREATE INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f" ON "note" USING "pgroonga" ("cw")`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + queryRunner.query(`DROP INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f"`); + queryRunner.query( + `ALTER TABLE "note" ALTER COLUMN "cw" TYPE character varying(512)`, + ); + queryRunner.query( + `CREATE INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f" ON "note" USING "pgroonga" ("cw" pgroonga_varchar_full_text_search_ops_v2)`, + ); + } +} diff --git a/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts b/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts new file mode 100644 index 0000000000..3bdb1aafc8 --- /dev/null +++ b/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts @@ -0,0 +1,17 @@ +import type { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddDriveFileUsage1713451569342 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE drive_file_usage_hint_enum AS ENUM ('userAvatar', 'userBanner')`, + ); + await queryRunner.query( + `ALTER TABLE "drive_file" ADD "usageHint" drive_file_usage_hint_enum DEFAULT NULL`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "usageHint"`); + await queryRunner.query(`DROP TYPE drive_file_usage_hint_enum`); + } +} diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index 30a50e5714..e99b17a5f7 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -1,6 +1,6 @@ import { redisClient } from "@/db/redis.js"; import { encode, decode } from "msgpackr"; -import { ChainableCommander } from "ioredis"; +import type { ChainableCommander } from "ioredis"; export class Cache { private ttl: number; diff --git a/packages/backend/src/misc/convert-milliseconds.ts b/packages/backend/src/misc/convert-milliseconds.ts deleted file mode 100644 index d8c163ffda..0000000000 --- a/packages/backend/src/misc/convert-milliseconds.ts +++ /dev/null @@ -1,17 +0,0 @@ -export function convertMilliseconds(ms: number) { - let seconds = Math.round(ms / 1000); - let minutes = Math.round(seconds / 60); - let hours = Math.round(minutes / 60); - const days = Math.round(hours / 24); - seconds %= 60; - minutes %= 60; - hours %= 24; - - const result = []; - if (days > 0) result.push(`${days} day(s)`); - if (hours > 0) result.push(`${hours} hour(s)`); - if (minutes > 0) result.push(`${minutes} minute(s)`); - if (seconds > 0) result.push(`${seconds} second(s)`); - - return result.join(", "); -} diff --git a/packages/backend/src/misc/emoji-regex.ts b/packages/backend/src/misc/emoji-regex.ts deleted file mode 100644 index 72d6a62d9a..0000000000 --- a/packages/backend/src/misc/emoji-regex.ts +++ /dev/null @@ -1,5 +0,0 @@ -import twemoji from "@twemoji/parser/dist/lib/regex.js"; -const twemojiRegex = twemoji.default; - -export const emojiRegex = new RegExp(`(${twemojiRegex.source})`); -export const emojiRegexAtStartToEnd = new RegExp(`^(${twemojiRegex.source})$`); diff --git a/packages/backend/src/misc/fetch-meta.ts b/packages/backend/src/misc/fetch-meta.ts deleted file mode 100644 index fdc978b5a3..0000000000 --- a/packages/backend/src/misc/fetch-meta.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { Meta } from "@/models/entities/meta.js"; - -let cache: Meta; - -export function metaToPugArgs(meta: Meta): object { - let motd = ["Loading..."]; - if (meta.customMotd.length > 0) { - motd = meta.customMotd; - } - let splashIconUrl = meta.iconUrl; - if (meta.customSplashIcons.length > 0) { - splashIconUrl = - meta.customSplashIcons[ - Math.floor(Math.random() * meta.customSplashIcons.length) - ]; - } - - return { - img: meta.bannerUrl, - title: meta.name || "Firefish", - instanceName: meta.name || "Firefish", - desc: meta.description, - icon: meta.iconUrl, - splashIcon: splashIconUrl, - themeColor: meta.themeColor, - randomMOTD: motd[Math.floor(Math.random() * motd.length)], - privateMode: meta.privateMode, - }; -} - -export async function fetchMeta(noCache = false): Promise { - if (!noCache && cache) return cache; - - return await db.transaction(async (transactionalEntityManager) => { - // New IDs are prioritized because multiple records may have been created due to past bugs. - const metas = await transactionalEntityManager.find(Meta, { - order: { - id: "DESC", - }, - }); - - const meta = metas[0]; - - if (meta) { - cache = meta; - return meta; - } else { - // If fetchMeta is called at the same time when meta is empty, this part may be called at the same time, so use fail-safe upsert. - const saved = await transactionalEntityManager - .upsert( - Meta, - { - id: "x", - }, - ["id"], - ) - .then((x) => - transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0]), - ); - - cache = saved; - return saved; - } - }); -} - -setInterval(() => { - fetchMeta(true).then((meta) => { - cache = meta; - }); -}, 1000 * 10); diff --git a/packages/backend/src/misc/fetch-proxy-account.ts b/packages/backend/src/misc/fetch-proxy-account.ts index a277db6fb9..8d015da25d 100644 --- a/packages/backend/src/misc/fetch-proxy-account.ts +++ b/packages/backend/src/misc/fetch-proxy-account.ts @@ -1,9 +1,9 @@ -import { fetchMeta } from "./fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import type { ILocalUser } from "@/models/entities/user.js"; import { Users } from "@/models/index.js"; export async function fetchProxyAccount(): Promise { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.proxyAccountId == null) return null; return (await Users.findOneByOrFail({ id: meta.proxyAccountId, diff --git a/packages/backend/src/misc/get-note-summary.ts b/packages/backend/src/misc/get-note-summary.ts deleted file mode 100644 index 0a662e434e..0000000000 --- a/packages/backend/src/misc/get-note-summary.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { Packed } from "./schema.js"; - -/** - * 投稿を表す文字列を取得します。 - * @param {*} note (packされた)投稿 - */ -export const getNoteSummary = (note: Packed<"Note">): string => { - if (note.deletedAt) { - return "❌"; - } - - let summary = ""; - - // 本文 - if (note.cw != null) { - summary += note.cw; - } else { - summary += note.text ? note.text : ""; - } - - // ファイルが添付されているとき - if ((note.files || []).length !== 0) { - const len = note.files?.length; - summary += ` 📎${len !== 1 ? ` (${len})` : ""}`; - } - - // 投票が添付されているとき - if (note.poll) { - summary += " 📊"; - } - - /* - // 返信のとき - if (note.replyId) { - if (note.reply) { - summary += `\n\nRE: ${getNoteSummary(note.reply)}`; - } else { - summary += '\n\nRE: ...'; - } - } - - // Renoteのとき - if (note.renoteId) { - if (note.renote) { - summary += `\n\nRN: ${getNoteSummary(note.renote)}`; - } else { - summary += '\n\nRN: ...'; - } - } - */ - - return summary.trim(); -}; diff --git a/packages/backend/src/misc/password.ts b/packages/backend/src/misc/password.ts deleted file mode 100644 index c63f89f5c9..0000000000 --- a/packages/backend/src/misc/password.ts +++ /dev/null @@ -1,20 +0,0 @@ -import bcrypt from "bcryptjs"; -import * as argon2 from "argon2"; - -export async function hashPassword(password: string): Promise { - return argon2.hash(password); -} - -export async function comparePassword( - password: string, - hash: string, -): Promise { - if (isOldAlgorithm(hash)) return bcrypt.compare(password, hash); - - return argon2.verify(hash, password); -} - -export function isOldAlgorithm(hash: string): boolean { - // bcrypt hashes start with $2[ab]$ - return hash.startsWith("$2"); -} diff --git a/packages/backend/src/misc/populate-emojis.ts b/packages/backend/src/misc/populate-emojis.ts index f18b23c9a4..4ca60b222f 100644 --- a/packages/backend/src/misc/populate-emojis.ts +++ b/packages/backend/src/misc/populate-emojis.ts @@ -3,8 +3,7 @@ import { Emojis } from "@/models/index.js"; import type { Emoji } from "@/models/entities/emoji.js"; import type { Note } from "@/models/entities/note.js"; import { Cache } from "./cache.js"; -import { isSelfHost, toPuny } from "backend-rs"; -import { decodeReaction } from "./reaction-lib.js"; +import { decodeReaction, isSelfHost, toPuny } from "backend-rs"; import config from "@/config/index.js"; import { query } from "@/prelude/url.js"; import { redisClient } from "@/db/redis.js"; diff --git a/packages/backend/src/misc/post.ts b/packages/backend/src/misc/post.ts index dbe703d1a0..0b107ed009 100644 --- a/packages/backend/src/misc/post.ts +++ b/packages/backend/src/misc/post.ts @@ -12,7 +12,7 @@ export function parse(acct: any): Post { cw: acct.cw, localOnly: acct.localOnly, createdAt: new Date(acct.createdAt), - visibility: "hidden" + (acct.visibility || ""), + visibility: `hidden${acct.visibility || ""}`, }; } diff --git a/packages/backend/src/misc/reaction-lib.ts b/packages/backend/src/misc/reaction-lib.ts deleted file mode 100644 index 4304d3b9e1..0000000000 --- a/packages/backend/src/misc/reaction-lib.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { emojiRegex } from "./emoji-regex.js"; -import { fetchMeta } from "./fetch-meta.js"; -import { Emojis } from "@/models/index.js"; -import { toPuny } from "backend-rs"; -import { IsNull } from "typeorm"; - -export function convertReactions(reactions: Record) { - const result = new Map(); - - for (const reaction in reactions) { - if (reactions[reaction] <= 0) continue; - - const decoded = decodeReaction(reaction).reaction; - result.set(decoded, (result.get(decoded) || 0) + reactions[reaction]); - } - - return Object.fromEntries(result); -} - -export async function toDbReaction( - reaction?: string | null, - reacterHost?: string | null, -): Promise { - if (!reaction) return (await fetchMeta()).defaultReaction; - - reacterHost = reacterHost == null ? null : toPuny(reacterHost); - - if (reaction.includes("❤") || reaction.includes("♥️")) return "❤️"; - - // Allow unicode reactions - const match = emojiRegex.exec(reaction); - if (match) { - const unicode = match[0]; - return unicode; - } - - const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/); - if (custom) { - const name = custom[1]; - const emoji = await Emojis.findOneBy({ - host: reacterHost || IsNull(), - name, - }); - - if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`; - } - - return (await fetchMeta()).defaultReaction; -} - -type DecodedReaction = { - /** - * リアクション名 (Unicode Emoji or ':name@hostname' or ':name@.') - */ - reaction: string; - - /** - * name (カスタム絵文字の場合name, Emojiクエリに使う) - */ - name?: string; - - /** - * host (カスタム絵文字の場合host, Emojiクエリに使う) - */ - host?: string | null; -}; - -export function decodeReaction(str: string): DecodedReaction { - const custom = str.match(/^:([\w+-]+)(?:@([\w.-]+))?:$/); - - if (custom) { - const name = custom[1]; - const host = custom[2] || null; - - return { - reaction: `:${name}@${host || "."}:`, // ローカル分は@以降を省略するのではなく.にする - name, - host, - }; - } - - return { - reaction: str, - name: undefined, - host: undefined, - }; -} diff --git a/packages/backend/src/misc/safe-for-sql.ts b/packages/backend/src/misc/safe-for-sql.ts deleted file mode 100644 index 02eb7f0a26..0000000000 --- a/packages/backend/src/misc/safe-for-sql.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function safeForSql(text: string): boolean { - return !/[\0\x08\x09\x1a\n\r"'\\\%]/g.test(text); -} diff --git a/packages/backend/src/misc/schema.ts b/packages/backend/src/misc/schema.ts index c793d3b2ed..73600832ce 100644 --- a/packages/backend/src/misc/schema.ts +++ b/packages/backend/src/misc/schema.ts @@ -33,8 +33,10 @@ import { packedGalleryPostSchema } from "@/models/schema/gallery-post.js"; import { packedEmojiSchema } from "@/models/schema/emoji.js"; import { packedNoteEdit } from "@/models/schema/note-edit.js"; import { packedNoteFileSchema } from "@/models/schema/note-file.js"; +import { packedAbuseUserReportSchema } from "@/models/schema/abuse-user-report.js"; export const refs = { + AbuseUserReport: packedAbuseUserReportSchema, UserLite: packedUserLiteSchema, UserDetailedNotMeOnly: packedUserDetailedNotMeOnlySchema, MeDetailedOnly: packedMeDetailedOnlySchema, diff --git a/packages/backend/src/misc/should-block-instance.ts b/packages/backend/src/misc/should-block-instance.ts index 35ed307931..465be41f2a 100644 --- a/packages/backend/src/misc/should-block-instance.ts +++ b/packages/backend/src/misc/should-block-instance.ts @@ -1,4 +1,4 @@ -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import type { Instance } from "@/models/entities/instance.js"; import type { Meta } from "@/models/entities/meta.js"; @@ -13,7 +13,7 @@ export async function shouldBlockInstance( host: Instance["host"], meta?: Meta, ): Promise { - const { blockedHosts } = meta ?? (await fetchMeta()); + const { blockedHosts } = meta ?? (await fetchMeta(true)); return blockedHosts.some( (blockedHost) => host === blockedHost || host.endsWith(`.${blockedHost}`), ); @@ -30,7 +30,7 @@ export async function shouldSilenceInstance( host: Instance["host"], meta?: Meta, ): Promise { - const { silencedHosts } = meta ?? (await fetchMeta()); + const { silencedHosts } = meta ?? (await fetchMeta(true)); return silencedHosts.some( (silencedHost) => host === silencedHost || host.endsWith(`.${silencedHost}`), diff --git a/packages/backend/src/misc/skipped-instances.ts b/packages/backend/src/misc/skipped-instances.ts index 785393022a..14b26a3032 100644 --- a/packages/backend/src/misc/skipped-instances.ts +++ b/packages/backend/src/misc/skipped-instances.ts @@ -1,5 +1,5 @@ import { Brackets } from "typeorm"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { Instances } from "@/models/index.js"; import type { Instance } from "@/models/entities/instance.js"; import { DAY } from "@/const.js"; @@ -19,7 +19,7 @@ export async function skippedInstances( hosts: Instance["host"][], ): Promise { // first check for blocked instances since that info may already be in memory - const meta = await fetchMeta(); + const meta = await fetchMeta(true); const shouldSkip = await Promise.all( hosts.map((host) => shouldBlockInstance(host, meta)), ); diff --git a/packages/backend/src/misc/sql-like-escape.ts b/packages/backend/src/misc/sql-like-escape.ts deleted file mode 100644 index 453947d6ec..0000000000 --- a/packages/backend/src/misc/sql-like-escape.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function sqlLikeEscape(s: string) { - return s.replace(/([%_])/g, "\\$1"); -} diff --git a/packages/backend/src/misc/translate.ts b/packages/backend/src/misc/translate.ts index e622171ec6..3395ce93be 100644 --- a/packages/backend/src/misc/translate.ts +++ b/packages/backend/src/misc/translate.ts @@ -1,7 +1,7 @@ import fetch from "node-fetch"; import { Converter } from "opencc-js"; import { getAgentByUrl } from "@/misc/fetch.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import type { PostLanguage } from "@/misc/langmap"; import * as deepl from "deepl-node"; @@ -26,7 +26,7 @@ export async function translate( from: PostLanguage | null, to: PostLanguage, ) { - const instance = await fetchMeta(); + const instance = await fetchMeta(true); 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 3c49e89fd5..81f564115f 100644 --- a/packages/backend/src/models/entities/drive-file.ts +++ b/packages/backend/src/models/entities/drive-file.ts @@ -16,6 +16,8 @@ import { DriveFolder } from "./drive-folder.js"; import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js"; import { NoteFile } from "./note-file.js"; +export type DriveFileUsageHint = "userAvatar" | "userBanner" | null; + @Entity() @Index(["userId", "folderId", "id"]) export class DriveFile { @@ -177,6 +179,14 @@ export class DriveFile { }) public isSensitive: boolean; + // Hint for what this file is used for + @Column({ + type: "enum", + enum: ["userAvatar", "userBanner"], + nullable: true, + }) + public usageHint: DriveFileUsageHint; + /** * 外部の(信頼されていない)URLへの直リンクか否か */ diff --git a/packages/backend/src/models/entities/meta.ts b/packages/backend/src/models/entities/meta.ts index cdb8e14c3f..5e267a8e24 100644 --- a/packages/backend/src/models/entities/meta.ts +++ b/packages/backend/src/models/entities/meta.ts @@ -276,6 +276,12 @@ export class Meta { }) public remoteDriveCapacityMb: number; + @Column("integer", { + default: 5, + comment: "Antenna Limit", + }) + public antennaLimit: number; + @Column("varchar", { length: 128, nullable: true, diff --git a/packages/backend/src/models/entities/note.ts b/packages/backend/src/models/entities/note.ts index 738e43d442..94cd8c7b66 100644 --- a/packages/backend/src/models/entities/note.ts +++ b/packages/backend/src/models/entities/note.ts @@ -72,9 +72,8 @@ export class Note { }) public name: string | null; - @Index() // USING pgroonga pgroonga_varchar_full_text_search_ops_v2 - @Column("varchar", { - length: 512, + @Index() // USING pgroonga + @Column("text", { nullable: true, }) public cw: string | null; diff --git a/packages/backend/src/models/repositories/abuse-user-report.ts b/packages/backend/src/models/repositories/abuse-user-report.ts index 16ce159955..b8d953d052 100644 --- a/packages/backend/src/models/repositories/abuse-user-report.ts +++ b/packages/backend/src/models/repositories/abuse-user-report.ts @@ -2,6 +2,7 @@ import { db } from "@/db/postgre.js"; import { Users } from "../index.js"; import { AbuseUserReport } from "@/models/entities/abuse-user-report.js"; import { awaitAll } from "@/prelude/await-all.js"; +import type { Packed } from "@/misc/schema.js"; export const AbuseUserReportRepository = db .getRepository(AbuseUserReport) @@ -10,7 +11,7 @@ export const AbuseUserReportRepository = db const report = typeof src === "object" ? src : await this.findOneByOrFail({ id: src }); - return await awaitAll({ + const packed: Packed<"AbuseUserReport"> = await awaitAll({ id: report.id, createdAt: report.createdAt.toISOString(), comment: report.comment, @@ -31,9 +32,10 @@ export const AbuseUserReportRepository = db : null, forwarded: report.forwarded, }); + return packed; }, - packMany(reports: any[]) { + packMany(reports: (AbuseUserReport["id"] | AbuseUserReport)[]) { return Promise.all(reports.map((x) => this.pack(x))); }, }); diff --git a/packages/backend/src/models/repositories/channel.ts b/packages/backend/src/models/repositories/channel.ts index 857470f4ec..809129db6c 100644 --- a/packages/backend/src/models/repositories/channel.ts +++ b/packages/backend/src/models/repositories/channel.ts @@ -40,6 +40,7 @@ export const ChannelRepository = db.getRepository(Channel).extend({ name: channel.name, description: channel.description, userId: channel.userId, + bannerId: channel.bannerId, bannerUrl: banner ? DriveFiles.getPublicUrl(banner, false) : null, usersCount: channel.usersCount, notesCount: channel.notesCount, diff --git a/packages/backend/src/models/repositories/drive-file.ts b/packages/backend/src/models/repositories/drive-file.ts index 2321f20d4c..18b139caff 100644 --- a/packages/backend/src/models/repositories/drive-file.ts +++ b/packages/backend/src/models/repositories/drive-file.ts @@ -152,6 +152,7 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({ md5: file.md5, size: file.size, isSensitive: file.isSensitive, + usageHint: file.usageHint, blurhash: file.blurhash, properties: opts.self ? file.properties : this.getPublicProperties(file), url: opts.self ? file.url : this.getPublicUrl(file, false), @@ -193,6 +194,7 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({ md5: file.md5, size: file.size, isSensitive: file.isSensitive, + usageHint: file.usageHint, blurhash: file.blurhash, properties: opts.self ? file.properties : this.getPublicProperties(file), url: opts.self ? file.url : this.getPublicUrl(file, false), diff --git a/packages/backend/src/models/repositories/gallery-post.ts b/packages/backend/src/models/repositories/gallery-post.ts index d91fb9de2a..c4078e9091 100644 --- a/packages/backend/src/models/repositories/gallery-post.ts +++ b/packages/backend/src/models/repositories/gallery-post.ts @@ -19,7 +19,9 @@ export const GalleryPostRepository = db.getRepository(GalleryPost).extend({ createdAt: post.createdAt.toISOString(), updatedAt: post.updatedAt.toISOString(), userId: post.userId, - user: Users.pack(post.user || post.userId, me), + user: Users.pack(post.user || post.userId, me, { + detail: true, + }), title: post.title, description: post.description, fileIds: post.fileIds, diff --git a/packages/backend/src/models/repositories/note-reaction.ts b/packages/backend/src/models/repositories/note-reaction.ts index 20aae2876f..47e16ced0c 100644 --- a/packages/backend/src/models/repositories/note-reaction.ts +++ b/packages/backend/src/models/repositories/note-reaction.ts @@ -2,7 +2,7 @@ import { db } from "@/db/postgre.js"; import { NoteReaction } from "@/models/entities/note-reaction.js"; import { Notes, Users } from "../index.js"; import type { Packed } from "@/misc/schema.js"; -import { decodeReaction } from "@/misc/reaction-lib.js"; +import { decodeReaction } from "backend-rs"; import type { User } from "@/models/entities/user.js"; export const NoteReactionRepository = db.getRepository(NoteReaction).extend({ diff --git a/packages/backend/src/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts index ed6ecc4a5c..c877048709 100644 --- a/packages/backend/src/models/repositories/note.ts +++ b/packages/backend/src/models/repositories/note.ts @@ -12,9 +12,8 @@ import { Channels, } from "../index.js"; import type { Packed } from "@/misc/schema.js"; -import { nyaify } from "backend-rs"; +import { countReactions, decodeReaction, nyaify } from "backend-rs"; import { awaitAll } from "@/prelude/await-all.js"; -import { convertReactions, decodeReaction } from "@/misc/reaction-lib.js"; import type { NoteReaction } from "@/models/entities/note-reaction.js"; import { aggregateNoteEmojis, @@ -214,7 +213,7 @@ export const NoteRepository = db.getRepository(Note).extend({ note.visibility === "specified" ? note.visibleUserIds : undefined, renoteCount: note.renoteCount, repliesCount: note.repliesCount, - reactions: convertReactions(note.reactions), + reactions: countReactions(note.reactions), reactionEmojis: reactionEmoji, emojis: noteEmoji, tags: note.tags.length > 0 ? note.tags : undefined, @@ -233,6 +232,7 @@ export const NoteRepository = db.getRepository(Note).extend({ uri: note.uri || undefined, url: note.url || undefined, updatedAt: note.updatedAt?.toISOString() || undefined, + hasPoll: note.hasPoll, poll: note.hasPoll ? populatePoll(note, meId) : undefined, ...(meId ? { diff --git a/packages/backend/src/models/schema/abuse-user-report.ts b/packages/backend/src/models/schema/abuse-user-report.ts new file mode 100644 index 0000000000..47e56c7415 --- /dev/null +++ b/packages/backend/src/models/schema/abuse-user-report.ts @@ -0,0 +1,69 @@ +export const packedAbuseUserReportSchema = { + type: "object", + properties: { + id: { + type: "string", + optional: false, + nullable: false, + format: "id", + example: "xxxxxxxxxx", + }, + createdAt: { + type: "string", + optional: false, + nullable: false, + format: "date-time", + }, + comment: { + type: "string", + optional: false, + nullable: false, + }, + resolved: { + type: "boolean", + optional: false, + nullable: false, + }, + reporterId: { + type: "string", + optional: false, + nullable: false, + format: "id", + }, + targetUserId: { + type: "string", + optional: false, + nullable: false, + format: "id", + }, + assigneeId: { + type: "string", + optional: false, + nullable: true, + format: "id", + }, + reporter: { + type: "object", + optional: false, + nullable: false, + ref: "UserDetailed", + }, + targetUser: { + type: "object", + optional: false, + nullable: false, + ref: "UserDetailed", + }, + assignee: { + type: "object", + optional: true, + nullable: true, + ref: "UserDetailed", + }, + forwarded: { + type: "boolean", + optional: false, + nullable: false, + }, + }, +} as const; diff --git a/packages/backend/src/models/schema/channel.ts b/packages/backend/src/models/schema/channel.ts index 67833cb0dd..d3ec222c8d 100644 --- a/packages/backend/src/models/schema/channel.ts +++ b/packages/backend/src/models/schema/channel.ts @@ -36,6 +36,13 @@ export const packedChannelSchema = { nullable: true, optional: false, }, + bannerId: { + type: "string", + optional: false, + nullable: true, + format: "id", + example: "xxxxxxxxxx", + }, notesCount: { type: "number", nullable: false, @@ -57,5 +64,10 @@ export const packedChannelSchema = { optional: false, format: "id", }, + hasUnreadNote: { + type: "boolean", + optional: true, + nullable: false, + }, }, } as const; diff --git a/packages/backend/src/models/schema/drive-file.ts b/packages/backend/src/models/schema/drive-file.ts index 30db9e7d48..929dbb472e 100644 --- a/packages/backend/src/models/schema/drive-file.ts +++ b/packages/backend/src/models/schema/drive-file.ts @@ -44,6 +44,12 @@ export const packedDriveFileSchema = { optional: false, nullable: false, }, + usageHint: { + type: "string", + optional: false, + nullable: true, + enum: ["userAvatar", "userBanner"], + }, blurhash: { type: "string", optional: false, diff --git a/packages/backend/src/models/schema/gallery-post.ts b/packages/backend/src/models/schema/gallery-post.ts index 9ac348e1fb..ae22507643 100644 --- a/packages/backend/src/models/schema/gallery-post.ts +++ b/packages/backend/src/models/schema/gallery-post.ts @@ -38,7 +38,7 @@ export const packedGalleryPostSchema = { }, user: { type: "object", - ref: "UserLite", + ref: "UserDetailed", optional: false, nullable: false, }, @@ -79,5 +79,15 @@ export const packedGalleryPostSchema = { optional: false, nullable: false, }, + isLiked: { + type: "boolean", + optional: true, + nullable: false, + }, + likedCount: { + type: "number", + optional: false, + nullable: false, + }, }, } as const; diff --git a/packages/backend/src/models/schema/note.ts b/packages/backend/src/models/schema/note.ts index 7dcdbc9b03..fff872b69f 100644 --- a/packages/backend/src/models/schema/note.ts +++ b/packages/backend/src/models/schema/note.ts @@ -28,7 +28,7 @@ export const packedNoteSchema = { }, cw: { type: "string", - optional: true, + optional: false, nullable: true, }, userId: { @@ -98,7 +98,7 @@ export const packedNoteSchema = { }, fileIds: { type: "array", - optional: true, + optional: false, nullable: false, items: { type: "string", @@ -128,6 +128,11 @@ export const packedNoteSchema = { nullable: false, }, }, + hasPoll: { + type: "boolean", + optional: false, + nullable: false, + }, poll: { type: "object", optional: true, diff --git a/packages/backend/src/queue/index.ts b/packages/backend/src/queue/index.ts index 58a0ae7486..e4e413be52 100644 --- a/packages/backend/src/queue/index.ts +++ b/packages/backend/src/queue/index.ts @@ -5,7 +5,7 @@ import config from "@/config/index.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"; -import { envOption } from "../env.js"; +import { envOption } from "@/config/index.js"; import processDeliver from "./processors/deliver.js"; import processInbox from "./processors/inbox.js"; diff --git a/packages/backend/src/queue/initialize.ts b/packages/backend/src/queue/initialize.ts index 0f9c83132f..a874005fbd 100644 --- a/packages/backend/src/queue/initialize.ts +++ b/packages/backend/src/queue/initialize.ts @@ -34,7 +34,7 @@ export function initialize(name: string, limitPerSec = -1) { function apBackoff(attemptsMade: number, err: Error) { const baseDelay = 60 * 1000; // 1min const maxBackoff = 8 * 60 * 60 * 1000; // 8hours - let backoff = (Math.pow(2, attemptsMade) - 1) * baseDelay; + let backoff = (2 ** attemptsMade - 1) * baseDelay; backoff = Math.min(backoff, maxBackoff); backoff += Math.round(backoff * Math.random() * 0.2); return backoff; diff --git a/packages/backend/src/queue/processors/inbox.ts b/packages/backend/src/queue/processors/inbox.ts index 46f9939a41..0ea72306b6 100644 --- a/packages/backend/src/queue/processors/inbox.ts +++ b/packages/backend/src/queue/processors/inbox.ts @@ -5,7 +5,7 @@ import perform from "@/remote/activitypub/perform.js"; import Logger from "@/services/logger.js"; import { registerOrFetchInstanceDoc } from "@/services/register-or-fetch-instance-doc.js"; import { Instances } from "@/models/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { toPuny, extractHost } from "backend-rs"; import { getApId } from "@/remote/activitypub/type.js"; import { fetchInstanceMetadata } from "@/services/fetch-instance-metadata.js"; @@ -41,7 +41,7 @@ export default async (job: Bull.Job): Promise => { const host = toPuny(new URL(signature.keyId).hostname); // interrupt if blocked - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (await shouldBlockInstance(host, meta)) { return `Blocked request: ${host}`; } diff --git a/packages/backend/src/remote/activitypub/check-fetch.ts b/packages/backend/src/remote/activitypub/check-fetch.ts index c0d40278ee..12ea63a931 100644 --- a/packages/backend/src/remote/activitypub/check-fetch.ts +++ b/packages/backend/src/remote/activitypub/check-fetch.ts @@ -1,7 +1,7 @@ import { URL } from "url"; import httpSignature, { IParsedSignature } from "@peertube/http-signature"; import config from "@/config/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { toPuny } from "backend-rs"; import DbResolver from "@/remote/activitypub/db-resolver.js"; import { getApId } from "@/remote/activitypub/type.js"; @@ -12,7 +12,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(); + const meta = await fetchMeta(true); const required = meta.secureMode || meta.privateMode; try { @@ -27,7 +27,7 @@ export async function hasSignature(req: IncomingMessage): Promise { } export async function checkFetch(req: IncomingMessage): Promise { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.secureMode || meta.privateMode) { if (req.headers.host !== config.host) return 400; diff --git a/packages/backend/src/remote/activitypub/kernel/delete/note.ts b/packages/backend/src/remote/activitypub/kernel/delete/note.ts index 4656480c2f..ae3a593d05 100644 --- a/packages/backend/src/remote/activitypub/kernel/delete/note.ts +++ b/packages/backend/src/remote/activitypub/kernel/delete/note.ts @@ -1,5 +1,5 @@ import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import deleteNode from "@/services/note/delete.js"; +import deleteNote from "@/services/note/delete.js"; import { apLogger } from "../../logger.js"; import DbResolver from "../../db-resolver.js"; import { getApLock } from "@/misc/app-lock.js"; @@ -36,7 +36,7 @@ export default async function ( return "The user trying to delete the post is not the post author"; } - await deleteNode(actor, note); + await deleteNote(actor, note); return "ok: note deleted"; } finally { await lock.release(); diff --git a/packages/backend/src/remote/activitypub/models/image.ts b/packages/backend/src/remote/activitypub/models/image.ts index 2cf0c6c152..a6ac698feb 100644 --- a/packages/backend/src/remote/activitypub/models/image.ts +++ b/packages/backend/src/remote/activitypub/models/image.ts @@ -1,9 +1,12 @@ import { uploadFromUrl } from "@/services/drive/upload-from-url.js"; import type { CacheableRemoteUser } from "@/models/entities/user.js"; import Resolver from "../resolver.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { apLogger } from "../logger.js"; -import type { DriveFile } from "@/models/entities/drive-file.js"; +import type { + DriveFile, + DriveFileUsageHint, +} from "@/models/entities/drive-file.js"; import { DriveFiles } from "@/models/index.js"; import { truncate } from "@/misc/truncate.js"; import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js"; @@ -16,6 +19,7 @@ const logger = apLogger; export async function createImage( actor: CacheableRemoteUser, value: any, + usage: DriveFileUsageHint, ): Promise { // Skip if author is frozen. if (actor.isSuspended) { @@ -34,7 +38,7 @@ export async function createImage( logger.info(`Creating the Image: ${image.url}`); - const instance = await fetchMeta(); + const instance = await fetchMeta(true); let file = await uploadFromUrl({ url: image.url, @@ -43,6 +47,7 @@ export async function createImage( sensitive: image.sensitive, isLink: !instance.cacheRemoteFiles, comment: truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH), + usageHint: usage, }); if (file.isLink) { @@ -73,9 +78,10 @@ export async function createImage( export async function resolveImage( actor: CacheableRemoteUser, value: any, + usage: DriveFileUsageHint, ): Promise { // TODO // Fetch from remote server and register - return await createImage(actor, value); + return await createImage(actor, value, usage); } diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts index ad59930457..b2fd67288c 100644 --- a/packages/backend/src/remote/activitypub/models/note.ts +++ b/packages/backend/src/remote/activitypub/models/note.ts @@ -213,7 +213,8 @@ export async function createNote( ? ( await Promise.all( note.attachment.map( - (x) => limit(() => resolveImage(actor, x)) as Promise, + (x) => + limit(() => resolveImage(actor, x, null)) as Promise, ), ) ).filter((image) => image != null) @@ -616,7 +617,7 @@ export async function updateNote(value: string | IObject, resolver?: Resolver) { fileList.map( (x) => limit(async () => { - const file = await resolveImage(actor, x); + const file = await resolveImage(actor, x, null); const update: Partial = {}; const altText = truncate(x.name, DB_MAX_IMAGE_COMMENT_LENGTH); diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index e91280125f..4baa2c021b 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -10,6 +10,7 @@ import { Followings, UserProfiles, UserPublickeys, + DriveFiles, } from "@/models/index.js"; import type { IRemoteUser, CacheableUser } from "@/models/entities/user.js"; import { User } from "@/models/entities/user.js"; @@ -362,10 +363,14 @@ export async function createPerson( //#region Fetch avatar and header image const [avatar, banner] = await Promise.all( - [person.icon, person.image].map((img) => + [person.icon, person.image].map((img, index) => img == null ? Promise.resolve(null) - : resolveImage(user!, img).catch(() => null), + : resolveImage( + user, + img, + index === 0 ? "userAvatar" : index === 1 ? "userBanner" : null, + ).catch(() => null), ), ); @@ -438,10 +443,14 @@ export async function updatePerson( // Fetch avatar and header image const [avatar, banner] = await Promise.all( - [person.icon, person.image].map((img) => + [person.icon, person.image].map((img, index) => img == null ? Promise.resolve(null) - : resolveImage(user, img).catch(() => null), + : resolveImage( + user, + img, + index === 0 ? "userAvatar" : index === 1 ? "userBanner" : null, + ).catch(() => null), ), ); @@ -561,10 +570,14 @@ export async function updatePerson( } as Partial; if (avatar) { + if (user?.avatarId) + await DriveFiles.update(user.avatarId, { usageHint: null }); updates.avatarId = avatar.id; } if (banner) { + if (user?.bannerId) + await DriveFiles.update(user.bannerId, { usageHint: null }); updates.bannerId = banner.id; } diff --git a/packages/backend/src/remote/activitypub/resolver.ts b/packages/backend/src/remote/activitypub/resolver.ts index 973f12cdc2..79b7962b72 100644 --- a/packages/backend/src/remote/activitypub/resolver.ts +++ b/packages/backend/src/remote/activitypub/resolver.ts @@ -1,7 +1,7 @@ import config from "@/config/index.js"; import type { ILocalUser } from "@/models/entities/user.js"; import { getInstanceActor } from "@/services/instance-actor.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { extractHost, isSelfHost } from "backend-rs"; import { apGet } from "./request.js"; import type { IObject, ICollection, IOrderedCollection } from "./type.js"; @@ -100,7 +100,7 @@ export default class Resolver { return await this.resolveLocal(value); } - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (await shouldBlockInstance(host, meta)) { throw new Error("Instance is blocked"); } diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts index 7e5fb5a281..71d95709b7 100644 --- a/packages/backend/src/server/activitypub.ts +++ b/packages/backend/src/server/activitypub.ts @@ -9,7 +9,7 @@ import renderKey from "@/remote/activitypub/renderer/key.js"; import { renderPerson } from "@/remote/activitypub/renderer/person.js"; import renderEmoji from "@/remote/activitypub/renderer/emoji.js"; import { inbox as processInbox } from "@/queue/index.js"; -import { isSelfHost } from "backend-rs"; +import { fetchMeta, isSelfHost } from "backend-rs"; import { Notes, Users, @@ -25,7 +25,6 @@ import { getSignatureUser, } from "@/remote/activitypub/check-fetch.js"; import { getInstanceActor } from "@/services/instance-actor.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; import renderFollow from "@/remote/activitypub/renderer/follow.js"; import Featured from "./activitypub/featured.js"; import Following from "./activitypub/following.js"; @@ -33,7 +32,7 @@ import Followers from "./activitypub/followers.js"; import Outbox, { packActivity } from "./activitypub/outbox.js"; import { serverLogger } from "./index.js"; import config from "@/config/index.js"; -import Koa from "koa"; +import type Koa from "koa"; import * as crypto from "node:crypto"; import { inspect } from "node:util"; import type { IActivity } from "@/remote/activitypub/type.js"; @@ -238,7 +237,7 @@ router.get("/notes/:note", async (ctx, next) => { ctx.body = renderActivity(await renderNote(note, false)); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.secureMode || meta.privateMode) { ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); } else { @@ -268,7 +267,7 @@ router.get("/notes/:note/activity", async (ctx) => { } ctx.body = renderActivity(await packActivity(note)); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.secureMode || meta.privateMode) { ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); } else { @@ -323,7 +322,7 @@ router.get("/users/:user/publickey", async (ctx) => { if (Users.isLocalUser(user)) { ctx.body = renderActivity(renderKey(user, keypair)); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.secureMode || meta.privateMode) { ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); } else { @@ -343,7 +342,7 @@ async function userInfo(ctx: Router.RouterContext, user: User | null) { } ctx.body = renderActivity(await renderPerson(user as ILocalUser)); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.secureMode || meta.privateMode) { ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); } else { @@ -426,8 +425,8 @@ router.get("/emojis/:emoji", async (ctx) => { return; } - ctx.body = renderActivity(await renderEmoji(emoji)); - const meta = await fetchMeta(); + ctx.body = renderActivity(renderEmoji(emoji)); + const meta = await fetchMeta(true); if (meta.secureMode || meta.privateMode) { ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); } else { @@ -459,7 +458,7 @@ router.get("/likes/:like", async (ctx) => { } ctx.body = renderActivity(await renderLike(reaction, note)); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.secureMode || meta.privateMode) { ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); } else { @@ -497,7 +496,7 @@ router.get( } ctx.body = renderActivity(renderFollow(follower, followee)); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.secureMode || meta.privateMode) { ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); } else { @@ -540,7 +539,7 @@ router.get("/follows/:followRequestId", async (ctx: Router.RouterContext) => { return; } - const meta = await fetchMeta(); + const meta = await fetchMeta(true); 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 464a7f769d..e7ea6f238e 100644 --- a/packages/backend/src/server/activitypub/featured.ts +++ b/packages/backend/src/server/activitypub/featured.ts @@ -5,7 +5,7 @@ import renderOrderedCollection from "@/remote/activitypub/renderer/ordered-colle import renderNote from "@/remote/activitypub/renderer/note.js"; import { Users, Notes, UserNotePinings } from "@/models/index.js"; import { checkFetch } from "@/remote/activitypub/check-fetch.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { setResponseType } from "../activitypub.js"; import type Router from "@koa/router"; @@ -57,7 +57,7 @@ export default async (ctx: Router.RouterContext) => { ctx.body = renderActivity(rendered); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); 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 3c9e5fa201..576a672d6d 100644 --- a/packages/backend/src/server/activitypub/followers.ts +++ b/packages/backend/src/server/activitypub/followers.ts @@ -8,7 +8,7 @@ import renderFollowUser from "@/remote/activitypub/renderer/follow-user.js"; import { Users, Followings, UserProfiles } from "@/models/index.js"; import type { Following } from "@/models/entities/following.js"; import { checkFetch } from "@/remote/activitypub/check-fetch.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { setResponseType } from "../activitypub.js"; import type { FindOptionsWhere } from "typeorm"; import type Router from "@koa/router"; @@ -110,7 +110,7 @@ export default async (ctx: Router.RouterContext) => { ctx.body = renderActivity(rendered); setResponseType(ctx); } - const meta = await fetchMeta(); + const meta = await fetchMeta(true); 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 cfbe985911..76b4e79716 100644 --- a/packages/backend/src/server/activitypub/following.ts +++ b/packages/backend/src/server/activitypub/following.ts @@ -8,7 +8,7 @@ import renderFollowUser from "@/remote/activitypub/renderer/follow-user.js"; import { Users, Followings, UserProfiles } from "@/models/index.js"; import type { Following } from "@/models/entities/following.js"; import { checkFetch } from "@/remote/activitypub/check-fetch.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { setResponseType } from "../activitypub.js"; import type { FindOptionsWhere } from "typeorm"; import type Router from "@koa/router"; @@ -110,7 +110,7 @@ export default async (ctx: Router.RouterContext) => { ctx.body = renderActivity(rendered); setResponseType(ctx); } - const meta = await fetchMeta(); + const meta = await fetchMeta(true); 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 53aa6f4ad5..305102cf12 100644 --- a/packages/backend/src/server/activitypub/outbox.ts +++ b/packages/backend/src/server/activitypub/outbox.ts @@ -11,7 +11,7 @@ import * as url from "@/prelude/url.js"; import { Users, Notes } from "@/models/index.js"; import type { Note } from "@/models/entities/note.js"; import { checkFetch } from "@/remote/activitypub/check-fetch.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { makePaginationQuery } from "../api/common/make-pagination-query.js"; import { setResponseType } from "../activitypub.js"; import type Router from "@koa/router"; @@ -117,7 +117,7 @@ export default async (ctx: Router.RouterContext) => { setResponseType(ctx); } - const meta = await fetchMeta(); + const meta = await fetchMeta(true); 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 620b754f30..5e65636427 100644 --- a/packages/backend/src/server/api/api-handler.ts +++ b/packages/backend/src/server/api/api-handler.ts @@ -2,7 +2,7 @@ import type Koa from "koa"; import type { User } from "@/models/entities/user.js"; import { UserIps } from "@/models/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import type { IEndpoint } from "./endpoints.js"; import authenticate, { AuthenticationError } from "./authenticate.js"; import call from "./call.js"; @@ -84,7 +84,7 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) => // Log IP if (user) { - fetchMeta().then((meta) => { + fetchMeta(true).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 2faef7b0e8..3107156a9b 100644 --- a/packages/backend/src/server/api/call.ts +++ b/packages/backend/src/server/api/call.ts @@ -10,7 +10,7 @@ import endpoints from "./endpoints.js"; import compatibility from "./compatibility.js"; import { ApiError } from "./error.js"; import { apiLogger } from "./logger.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; const accessDenied = { message: "Access denied.", @@ -117,7 +117,7 @@ export default async ( } // private mode - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if ( meta.privateMode && ep.meta.requireCredentialPrivateMode && diff --git a/packages/backend/src/server/api/common/signup.ts b/packages/backend/src/server/api/common/signup.ts index a267baf9c0..58b88b7d02 100644 --- a/packages/backend/src/server/api/common/signup.ts +++ b/packages/backend/src/server/api/common/signup.ts @@ -4,12 +4,11 @@ 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, toPuny } from "backend-rs"; +import { genId, 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"; import config from "@/config/index.js"; -import { hashPassword } from "@/misc/password.js"; export async function signup(opts: { username: User["username"]; @@ -40,7 +39,7 @@ export async function signup(opts: { } // Generate hash of password - hash = await hashPassword(password); + hash = hashPassword(password); } // Generate secret diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts index 78034917f0..4063af5c5c 100644 --- a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts +++ b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts @@ -16,68 +16,7 @@ export const meta = { type: "object", optional: false, nullable: false, - properties: { - id: { - type: "string", - nullable: false, - optional: false, - format: "id", - example: "xxxxxxxxxx", - }, - createdAt: { - type: "string", - nullable: false, - optional: false, - format: "date-time", - }, - comment: { - type: "string", - nullable: false, - optional: false, - }, - resolved: { - type: "boolean", - nullable: false, - optional: false, - example: false, - }, - reporterId: { - type: "string", - nullable: false, - optional: false, - format: "id", - }, - targetUserId: { - type: "string", - nullable: false, - optional: false, - format: "id", - }, - assigneeId: { - type: "string", - nullable: true, - optional: false, - format: "id", - }, - reporter: { - type: "object", - nullable: false, - optional: false, - ref: "User", - }, - targetUser: { - type: "object", - nullable: false, - optional: false, - ref: "User", - }, - assignee: { - type: "object", - nullable: true, - optional: true, - ref: "User", - }, - }, + ref: "AbuseUserReport", }, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts index 5c3e19d9e0..9c7a5180d3 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts @@ -1,8 +1,7 @@ import define from "@/server/api/define.js"; import { Emojis } from "@/models/index.js"; -import { toPuny } from "backend-rs"; +import { sqlLikeEscape, toPuny } from "backend-rs"; import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js"; -import { sqlLikeEscape } from "@/misc/sql-like-escape.js"; import { ApiError } from "@/server/api/error.js"; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts index 434b679608..98a69090db 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -1,8 +1,8 @@ import define from "@/server/api/define.js"; import { Emojis } from "@/models/index.js"; -import { makePaginationQuery } from "../../../common/make-pagination-query.js"; +import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js"; import type { Emoji } from "@/models/entities/emoji.js"; -//import { sqlLikeEscape } from "@/misc/sql-like-escape.js"; +//import { sqlLikeEscape } from "backend-rs"; import { ApiError } from "@/server/api/error.js"; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index a22fbab8f1..ab04800944 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -1,5 +1,5 @@ import config from "@/config/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { MAX_NOTE_TEXT_LENGTH, MAX_CAPTION_TEXT_LENGTH } from "@/const.js"; import define from "@/server/api/define.js"; @@ -24,6 +24,11 @@ export const meta = { optional: false, nullable: false, }, + antennaLimit: { + type: "number", + optional: false, + nullable: false, + }, cacheRemoteFiles: { type: "boolean", optional: false, @@ -466,7 +471,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async () => { - const instance = await fetchMeta(true); + const instance = await fetchMeta(false); return { maintainerName: instance.maintainerName, @@ -487,6 +492,7 @@ export default define(meta, paramDef, async () => { enableGuestTimeline: instance.enableGuestTimeline, driveCapacityPerLocalUserMb: instance.localDriveCapacityMb, driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb, + antennaLimit: instance.antennaLimit, emailRequiredForSignup: instance.emailRequiredForSignup, enableHcaptcha: instance.enableHcaptcha, hcaptchaSiteKey: instance.hcaptchaSiteKey, diff --git a/packages/backend/src/server/api/endpoints/admin/reset-password.ts b/packages/backend/src/server/api/endpoints/admin/reset-password.ts index 5fbed130e6..ace0b581d7 100644 --- a/packages/backend/src/server/api/endpoints/admin/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/admin/reset-password.ts @@ -1,8 +1,7 @@ import define from "@/server/api/define.js"; -// import bcrypt from "bcryptjs"; import rndstr from "rndstr"; import { Users, UserProfiles } from "@/models/index.js"; -import { hashPassword } from "@/misc/password.js"; +import { hashPassword } from "backend-rs"; export const meta = { tags: ["admin"], @@ -48,8 +47,7 @@ export default define(meta, paramDef, async (ps) => { const passwd = rndstr("a-zA-Z0-9", 8); // Generate hash of password - // const hash = bcrypt.hashSync(passwd); - const hash = await hashPassword(passwd); + const hash = hashPassword(passwd); await UserProfiles.update( { diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts index 1e6ebeda93..8a892c3606 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-users.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts @@ -1,6 +1,6 @@ import { Users } from "@/models/index.js"; import define from "@/server/api/define.js"; -import { sqlLikeEscape } from "@/misc/sql-like-escape.js"; +import { sqlLikeEscape } from "backend-rs"; export const meta = { tags: ["admin"], 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 604ef3a0fc..e5234ea720 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -94,6 +94,7 @@ export const paramDef = { defaultDarkTheme: { type: "string", nullable: true }, localDriveCapacityMb: { type: "integer" }, remoteDriveCapacityMb: { type: "integer" }, + antennaLimit: { type: "integer" }, cacheRemoteFiles: { type: "boolean" }, markLocalFilesNsfwByDefault: { type: "boolean" }, emailRequiredForSignup: { type: "boolean" }, @@ -327,6 +328,10 @@ export default define(meta, paramDef, async (ps, me) => { set.remoteDriveCapacityMb = ps.remoteDriveCapacityMb; } + if (ps.antennaLimit !== undefined) { + set.antennaLimit = ps.antennaLimit; + } + if (ps.cacheRemoteFiles !== undefined) { set.cacheRemoteFiles = ps.cacheRemoteFiles; } diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index 792301d4de..aa5dcee044 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 { genId } from "backend-rs"; +import { fetchMeta, genId } from "backend-rs"; import { Antennas, UserLists, UserGroupJoinings } from "@/models/index.js"; import { ApiError } from "@/server/api/error.js"; import { publishInternalEvent } from "@/services/stream.js"; @@ -109,10 +109,12 @@ export default define(meta, paramDef, async (ps, user) => { let userList; let userGroupJoining; + const instance = await fetchMeta(true); + const antennas = await Antennas.findBy({ userId: user.id, }); - if (antennas.length > 5 && !user.isAdmin) { + if (antennas.length >= instance.antennaLimit) { throw new ApiError(meta.errors.tooManyAntennas); } diff --git a/packages/backend/src/server/api/endpoints/channels/search.ts b/packages/backend/src/server/api/endpoints/channels/search.ts index b2fab701c5..ed44250a37 100644 --- a/packages/backend/src/server/api/endpoints/channels/search.ts +++ b/packages/backend/src/server/api/endpoints/channels/search.ts @@ -2,7 +2,7 @@ import define from "@/server/api/define.js"; import { Brackets } from "typeorm"; import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js"; import { Channels } from "@/models/index.js"; -import { sqlLikeEscape } from "@/misc/sql-like-escape.js"; +import { sqlLikeEscape } from "backend-rs"; export const meta = { tags: ["channels"], diff --git a/packages/backend/src/server/api/endpoints/channels/update.ts b/packages/backend/src/server/api/endpoints/channels/update.ts index 0de7a837a1..fdd21da65f 100644 --- a/packages/backend/src/server/api/endpoints/channels/update.ts +++ b/packages/backend/src/server/api/endpoints/channels/update.ts @@ -83,7 +83,7 @@ export default define(meta, paramDef, async (ps, me) => { await Channels.update(channel.id, { ...(ps.name !== undefined ? { name: ps.name } : {}), ...(ps.description !== undefined ? { description: ps.description } : {}), - ...(banner ? { bannerId: banner.id } : {}), + ...(banner ? { bannerId: banner.id } : { bannerId: null }), }); return await Channels.pack(channel.id, me); diff --git a/packages/backend/src/server/api/endpoints/custom-motd.ts b/packages/backend/src/server/api/endpoints/custom-motd.ts index 2939355b94..ac1012258d 100644 --- a/packages/backend/src/server/api/endpoints/custom-motd.ts +++ b/packages/backend/src/server/api/endpoints/custom-motd.ts @@ -1,5 +1,5 @@ // import { IsNull } from 'typeorm'; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import define from "@/server/api/define.js"; export const meta = { @@ -27,7 +27,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async () => { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); 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 f63a1b9600..4eb35aa3e5 100644 --- a/packages/backend/src/server/api/endpoints/custom-splash-icons.ts +++ b/packages/backend/src/server/api/endpoints/custom-splash-icons.ts @@ -1,5 +1,5 @@ // import { IsNull } from 'typeorm'; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import define from "@/server/api/define.js"; export const meta = { @@ -27,7 +27,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async () => { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); 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 164e7b8f93..c04f219a9b 100644 --- a/packages/backend/src/server/api/endpoints/drive.ts +++ b/packages/backend/src/server/api/endpoints/drive.ts @@ -1,4 +1,4 @@ -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { DriveFiles } from "@/models/index.js"; import define from "@/server/api/define.js"; @@ -35,7 +35,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, user) => { - const instance = await fetchMeta(true); + const instance = await fetchMeta(false); // Calculate drive usage const usage = await DriveFiles.calcDriveUsageOf(user.id); 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 a3e3fafa2f..44e388a9bd 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -2,7 +2,7 @@ import { addFile } from "@/services/drive/add-file.js"; import { DriveFiles } from "@/models/index.js"; import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js"; import { IdentifiableError } from "@/misc/identifiable-error.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { MINUTE } from "@/const.js"; import define from "@/server/api/define.js"; import { apiLogger } from "@/server/api/logger.js"; @@ -96,7 +96,7 @@ export default define( name = null; } - const instanceMeta = await fetchMeta(); + const instanceMeta = await fetchMeta(true); try { // Create file diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index 27a6dabb49..362ab098fb 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -1,7 +1,6 @@ import define from "@/server/api/define.js"; import { Instances } from "@/models/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { sqlLikeEscape } from "@/misc/sql-like-escape.js"; +import { fetchMeta, sqlLikeEscape } from "backend-rs"; export const meta = { tags: ["federation"], @@ -101,7 +100,7 @@ export default define(meta, paramDef, async (ps, me) => { } if (typeof ps.blocked === "boolean") { - const meta = await fetchMeta(true); + const meta = await fetchMeta(false); if (ps.blocked) { if (meta.blockedHosts.length === 0) { return []; @@ -117,7 +116,7 @@ export default define(meta, paramDef, async (ps, me) => { } if (typeof ps.silenced === "boolean") { - const meta = await fetchMeta(true); + const meta = await fetchMeta(false); if (ps.silenced) { if (meta.silencedHosts.length === 0) { return []; diff --git a/packages/backend/src/server/api/endpoints/hashtags/search.ts b/packages/backend/src/server/api/endpoints/hashtags/search.ts index 1dc1fb4922..8fb5b23f62 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/search.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/search.ts @@ -1,6 +1,6 @@ import define from "@/server/api/define.js"; import { Hashtags } from "@/models/index.js"; -import { sqlLikeEscape } from "@/misc/sql-like-escape.js"; +import { sqlLikeEscape } from "backend-rs"; export const meta = { tags: ["hashtags"], diff --git a/packages/backend/src/server/api/endpoints/hashtags/trend.ts b/packages/backend/src/server/api/endpoints/hashtags/trend.ts index fe8bba95fd..531a494248 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/trend.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/trend.ts @@ -1,9 +1,8 @@ import { Brackets } from "typeorm"; import define from "@/server/api/define.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta, safeForSql } from "backend-rs"; import { Notes } from "@/models/index.js"; import type { Note } from "@/models/entities/note.js"; -import { safeForSql } from "@/misc/safe-for-sql.js"; import { normalizeForSearch } from "@/misc/normalize-for-search.js"; /* @@ -67,7 +66,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async () => { - const instance = await fetchMeta(true); + const instance = await fetchMeta(false); 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 0e52dd0d78..6c99217e7d 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 @@ -9,7 +9,7 @@ import { import config from "@/config/index.js"; import { procedures, hash } from "@/server/api/2fa.js"; import { publishMainStream } from "@/services/stream.js"; -import { comparePassword } from "@/misc/password.js"; +import { verifyPassword } from "backend-rs"; const rpIdHashReal = hash(Buffer.from(config.hostname, "utf-8")); @@ -40,8 +40,8 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - // Compare password - const same = await comparePassword(ps.password, profile.password!); + // Compare passwords + const same = verifyPassword(ps.password, profile.password!); if (!same) { throw new Error("incorrect password"); 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 b275b5705d..4991e8fc90 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,9 +2,8 @@ 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 } from "backend-rs"; +import { genId, verifyPassword } from "backend-rs"; import { hash } from "@/server/api/2fa.js"; -import { comparePassword } from "@/misc/password.js"; const randomBytes = promisify(crypto.randomBytes); @@ -25,8 +24,8 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - // Compare password - const same = await comparePassword(ps.password, profile.password!); + // Compare passwords + const same = verifyPassword(ps.password, profile.password!); if (!same) { throw new Error("incorrect password"); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts index 52e1df39f4..c0e6137d5d 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts @@ -3,7 +3,7 @@ import * as QRCode from "qrcode"; import config from "@/config/index.js"; import { UserProfiles } from "@/models/index.js"; import define from "@/server/api/define.js"; -import { comparePassword } from "@/misc/password.js"; +import { verifyPassword } from "backend-rs"; export const meta = { requireCredential: true, @@ -22,8 +22,8 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - // Compare password - const same = await comparePassword(ps.password, profile.password!); + // Compare passwords + const same = verifyPassword(ps.password, profile.password!); if (!same) { throw new Error("incorrect password"); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts index 0cdf8780ef..4259d8f70d 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts @@ -1,4 +1,4 @@ -import { comparePassword } from "@/misc/password.js"; +import { verifyPassword } from "backend-rs"; import define from "@/server/api/define.js"; import { UserProfiles, UserSecurityKeys, Users } from "@/models/index.js"; import { publishMainStream } from "@/services/stream.js"; @@ -21,8 +21,8 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - // Compare password - const same = await comparePassword(ps.password, profile.password!); + // Compare passwords + const same = verifyPassword(ps.password, profile.password!); if (!same) { throw new Error("incorrect password"); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts index c4e78eecb5..240ff2b34e 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts @@ -1,7 +1,7 @@ import { publishMainStream } from "@/services/stream.js"; import define from "@/server/api/define.js"; import { Users, UserProfiles } from "@/models/index.js"; -import { comparePassword } from "@/misc/password.js"; +import { verifyPassword } from "backend-rs"; export const meta = { requireCredential: true, @@ -20,8 +20,8 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - // Compare password - const same = await comparePassword(ps.password, profile.password!); + // Compare passwords + const same = verifyPassword(ps.password, profile.password!); if (!same) { throw new Error("incorrect password"); diff --git a/packages/backend/src/server/api/endpoints/i/change-password.ts b/packages/backend/src/server/api/endpoints/i/change-password.ts index b0dc8bba60..1634676748 100644 --- a/packages/backend/src/server/api/endpoints/i/change-password.ts +++ b/packages/backend/src/server/api/endpoints/i/change-password.ts @@ -1,6 +1,6 @@ import define from "@/server/api/define.js"; import { UserProfiles } from "@/models/index.js"; -import { hashPassword, comparePassword } from "@/misc/password.js"; +import { hashPassword, verifyPassword } from "backend-rs"; export const meta = { requireCredential: true, @@ -20,8 +20,8 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - // Compare password - const same = await comparePassword(ps.currentPassword, profile.password!); + // Compare passwords + const same = verifyPassword(ps.currentPassword, profile.password!); if (!same) { throw new Error("incorrect password"); diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts index 606cde82e1..538798261d 100644 --- a/packages/backend/src/server/api/endpoints/i/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts @@ -1,7 +1,7 @@ import { UserProfiles, Users } from "@/models/index.js"; import { deleteAccount } from "@/services/delete-account.js"; import define from "@/server/api/define.js"; -import { comparePassword } from "@/misc/password.js"; +import { verifyPassword } from "backend-rs"; export const meta = { requireCredential: true, @@ -24,8 +24,8 @@ export default define(meta, paramDef, async (ps, user) => { return; } - // Compare password - const same = await comparePassword(ps.password, profile.password!); + // Compare passwords + const same = verifyPassword(ps.password, profile.password!); if (!same) { throw new Error("incorrect password"); 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 b8b52be98f..225306ebc5 100644 --- a/packages/backend/src/server/api/endpoints/i/import-posts.ts +++ b/packages/backend/src/server/api/endpoints/i/import-posts.ts @@ -3,7 +3,7 @@ import { createImportPostsJob } from "@/queue/index.js"; import { ApiError } from "@/server/api/error.js"; import { DriveFiles } from "@/models/index.js"; import { DAY } from "@/const.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; export const meta = { secure: true, @@ -45,7 +45,7 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const file = await DriveFiles.findOneBy({ id: ps.fileId }); - const instanceMeta = await fetchMeta(); + const instanceMeta = await fetchMeta(true); if (instanceMeta.experimentalFeatures?.postImports === false) throw new ApiError(meta.errors.importsDisabled); diff --git a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts index c1b4325adb..fd3023ab7a 100644 --- a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts +++ b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts @@ -6,7 +6,7 @@ import { import generateUserToken from "@/server/api/common/generate-native-user-token.js"; import define from "@/server/api/define.js"; import { Users, UserProfiles } from "@/models/index.js"; -import { comparePassword } from "@/misc/password.js"; +import { verifyPassword } from "backend-rs"; export const meta = { requireCredential: true, @@ -28,8 +28,8 @@ export default define(meta, paramDef, async (ps, user) => { const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - // Compare password - const same = await comparePassword(ps.password, profile.password!); + // Compare passwords + const same = verifyPassword(ps.password, profile.password!); if (!same) { throw new Error("incorrect password"); diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts index a48252ed1a..234127f584 100644 --- a/packages/backend/src/server/api/endpoints/i/update-email.ts +++ b/packages/backend/src/server/api/endpoints/i/update-email.ts @@ -7,7 +7,7 @@ import { sendEmail } from "@/services/send-email.js"; import { ApiError } from "@/server/api/error.js"; import { validateEmailForAccount } from "@/services/validate-email-for-account.js"; import { HOUR } from "@/const.js"; -import { comparePassword } from "@/misc/password.js"; +import { verifyPassword } from "backend-rs"; export const meta = { requireCredential: true, @@ -46,8 +46,8 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - // Compare password - const same = await comparePassword(ps.password, profile.password!); + // Compare passwords + const same = verifyPassword(ps.password, profile.password!); if (!same) { throw new ApiError(meta.errors.incorrectPassword); diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 4389688a12..4f65c59a9e 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -13,6 +13,7 @@ import { normalizeForSearch } from "@/misc/normalize-for-search.js"; import { verifyLink } from "@/services/fetch-rel-me.js"; import { ApiError } from "@/server/api/error.js"; import define from "@/server/api/define.js"; +import { DriveFile } from "@/models/entities/drive-file"; export const meta = { tags: ["account"], @@ -241,8 +242,9 @@ export default define(meta, paramDef, async (ps, _user, token) => { if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes; + let avatar: DriveFile | null = null; if (ps.avatarId) { - const avatar = await DriveFiles.findOneBy({ id: ps.avatarId }); + avatar = await DriveFiles.findOneBy({ id: ps.avatarId }); if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar); @@ -250,8 +252,9 @@ export default define(meta, paramDef, async (ps, _user, token) => { throw new ApiError(meta.errors.avatarNotAnImage); } + let banner: DriveFile | null = null; if (ps.bannerId) { - const banner = await DriveFiles.findOneBy({ id: ps.bannerId }); + banner = await DriveFiles.findOneBy({ id: ps.bannerId }); if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner); @@ -328,6 +331,20 @@ export default define(meta, paramDef, async (ps, _user, token) => { updateUsertags(user, tags); //#endregion + // Update old/new avatar usage hints + if (avatar) { + if (user.avatarId) + await DriveFiles.update(user.avatarId, { usageHint: null }); + await DriveFiles.update(avatar.id, { usageHint: "userAvatar" }); + } + + // Update old/new banner usage hints + if (banner) { + if (user.bannerId) + await DriveFiles.update(user.bannerId, { usageHint: null }); + await DriveFiles.update(banner.id, { usageHint: "userBanner" }); + } + if (Object.keys(updates).length > 0) await Users.update(user.id, updates); if (Object.keys(profileUpdates).length > 0) await UserProfiles.update(user.id, profileUpdates); diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 2a674b52c3..ec8f701976 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -1,7 +1,7 @@ import JSON5 from "json5"; import { IsNull, MoreThan } from "typeorm"; import config from "@/config/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { Ads, Emojis, Users } from "@/models/index.js"; import { MAX_NOTE_TEXT_LENGTH, MAX_CAPTION_TEXT_LENGTH } from "@/const.js"; import define from "@/server/api/define.js"; @@ -126,6 +126,11 @@ export const meta = { optional: false, nullable: false, }, + antennaLimit: { + type: "number", + optional: false, + nullable: false, + }, cacheRemoteFiles: { type: "boolean", optional: false, @@ -398,7 +403,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, me) => { - const instance = await fetchMeta(true); + const instance = await fetchMeta(false); const emojis = await Emojis.find({ where: { @@ -445,6 +450,7 @@ export default define(meta, paramDef, async (ps, me) => { enableGuestTimeline: instance.enableGuestTimeline, driveCapacityPerLocalUserMb: instance.localDriveCapacityMb, driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb, + antennaLimit: instance.antennaLimit, emailRequiredForSignup: instance.emailRequiredForSignup, enableHcaptcha: instance.enableHcaptcha, hcaptchaSiteKey: instance.hcaptchaSiteKey, diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 270c33abd0..c2302f4c8d 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -114,7 +114,7 @@ export const paramDef = { enum: Object.keys(langmap), nullable: true, }, - cw: { type: "string", nullable: true, maxLength: 100 }, + cw: { type: "string", nullable: true, maxLength: MAX_NOTE_TEXT_LENGTH }, localOnly: { type: "boolean", default: false }, noExtractMentions: { type: "boolean", default: false }, noExtractHashtags: { type: "boolean", default: false }, 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 142b380f71..476375dc0b 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -1,4 +1,4 @@ -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { Notes } from "@/models/index.js"; import { activeUsersChart } from "@/services/chart/index.js"; import define from "@/server/api/define.js"; @@ -64,7 +64,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, user) => { - const m = await fetchMeta(); + const m = await fetchMeta(true); 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 c9800f2e1f..e6ab910040 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -1,5 +1,5 @@ import { Brackets } from "typeorm"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { Followings, Notes } from "@/models/index.js"; import { activeUsersChart } from "@/services/chart/index.js"; import define from "@/server/api/define.js"; @@ -71,7 +71,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, user) => { - const m = await fetchMeta(); + const m = await fetchMeta(true); 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 b9cb68c2a0..2a99c1236c 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -1,5 +1,5 @@ import { Brackets } from "typeorm"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { Notes } from "@/models/index.js"; import { activeUsersChart } from "@/services/chart/index.js"; import define from "@/server/api/define.js"; @@ -74,7 +74,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, user) => { - const m = await fetchMeta(); + const m = await fetchMeta(true); 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/make-private.ts b/packages/backend/src/server/api/endpoints/notes/make-private.ts index 7b9ebc4d1a..5ddf1f3bf1 100644 --- a/packages/backend/src/server/api/endpoints/notes/make-private.ts +++ b/packages/backend/src/server/api/endpoints/notes/make-private.ts @@ -53,7 +53,7 @@ export default define(meta, paramDef, async (ps, user) => { throw new ApiError(meta.errors.accessDenied); } - await deleteNote(user, note, false, false); + await deleteNote(user, note, false); await Notes.update(note.id, { visibility: "specified", visibleUserIds: [], 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 f71822f926..073a8f8569 100644 --- a/packages/backend/src/server/api/endpoints/notes/recommended-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/recommended-timeline.ts @@ -1,5 +1,5 @@ import { Brackets } from "typeorm"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { Notes } from "@/models/index.js"; import { activeUsersChart } from "@/services/chart/index.js"; import define from "@/server/api/define.js"; @@ -74,7 +74,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, user) => { - const m = await fetchMeta(); + const m = await fetchMeta(true); 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/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts index e87725e342..f449ea081a 100644 --- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts +++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts @@ -1,6 +1,6 @@ import { Brackets } from "typeorm"; import { Notes } from "@/models/index.js"; -import { safeForSql } from "@/misc/safe-for-sql.js"; +import { safeForSql } from "backend-rs"; import { normalizeForSearch } from "@/misc/normalize-for-search.js"; import define from "@/server/api/define.js"; import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js"; diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts index b159a91944..f28208cba9 100644 --- a/packages/backend/src/server/api/endpoints/notes/search.ts +++ b/packages/backend/src/server/api/endpoints/notes/search.ts @@ -1,11 +1,11 @@ import { Notes } from "@/models/index.js"; -import { Note } from "@/models/entities/note.js"; +import type { Note } from "@/models/entities/note.js"; import define from "@/server/api/define.js"; import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js"; import { generateVisibilityQuery } from "@/server/api/common/generate-visibility-query.js"; import { generateMutedUserQuery } from "@/server/api/common/generate-muted-user-query.js"; import { generateBlockedUserQuery } from "@/server/api/common/generate-block-query.js"; -import { sqlLikeEscape } from "@/misc/sql-like-escape.js"; +import { sqlLikeEscape } from "backend-rs"; import type { SelectQueryBuilder } from "typeorm"; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts index 6d6519e47b..65241becae 100644 --- a/packages/backend/src/server/api/endpoints/pinned-users.ts +++ b/packages/backend/src/server/api/endpoints/pinned-users.ts @@ -1,6 +1,6 @@ import { IsNull } from "typeorm"; import { Users } from "@/models/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { stringToAcct } from "backend-rs"; import type { User } from "@/models/entities/user.js"; import define from "@/server/api/define.js"; @@ -31,7 +31,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, me) => { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); const users = await Promise.all( meta.pinnedUsers @@ -44,9 +44,7 @@ export default define(meta, paramDef, async (ps, me) => { ), ); - return await Users.packMany( - users.filter((x) => x !== undefined) as User[], - me, - { detail: true }, - ); + return await Users.packMany(users.filter((x) => x != null) as User[], me, { + detail: true, + }); }); diff --git a/packages/backend/src/server/api/endpoints/recommended-instances.ts b/packages/backend/src/server/api/endpoints/recommended-instances.ts index b235678428..5c5e267b2e 100644 --- a/packages/backend/src/server/api/endpoints/recommended-instances.ts +++ b/packages/backend/src/server/api/endpoints/recommended-instances.ts @@ -1,5 +1,5 @@ // import { IsNull } from 'typeorm'; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import define from "@/server/api/define.js"; export const meta = { @@ -27,7 +27,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async () => { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); const instances = await Promise.all(meta.recommendedInstances.map((x) => x)); return instances; }); diff --git a/packages/backend/src/server/api/endpoints/reset-password.ts b/packages/backend/src/server/api/endpoints/reset-password.ts index ff5c8d987f..b69b1b17d3 100644 --- a/packages/backend/src/server/api/endpoints/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/reset-password.ts @@ -1,6 +1,6 @@ import { UserProfiles, PasswordResetRequests } from "@/models/index.js"; import define from "@/server/api/define.js"; -import { hashPassword } from "@/misc/password.js"; +import { hashPassword } from "backend-rs"; export const meta = { tags: ["reset password"], @@ -32,7 +32,7 @@ export default define(meta, paramDef, async (ps, user) => { } // Generate hash of password - const hash = await hashPassword(ps.password); + const hash = hashPassword(ps.password); await UserProfiles.update(req.userId, { password: hash, diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts index d3b6a08074..1a1ecad688 100644 --- a/packages/backend/src/server/api/endpoints/server-info.ts +++ b/packages/backend/src/server/api/endpoints/server-info.ts @@ -1,7 +1,7 @@ import * as os from "node:os"; import si from "systeminformation"; import define from "@/server/api/define.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; export const meta = { requireCredential: false, @@ -30,7 +30,7 @@ export default define(meta, paramDef, async () => { } } - const instanceMeta = await fetchMeta(); + const instanceMeta = await fetchMeta(true); if (!instanceMeta.enableServerMachineStats) { return { machine: "Not specified", diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts index 528d3106e0..69b3f6779b 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 } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { genId } from "backend-rs"; import { SwSubscriptions } from "@/models/index.js"; import define from "@/server/api/define.js"; @@ -64,7 +64,7 @@ export default define(meta, paramDef, async (ps, me) => { publickey: ps.publickey, }); - const instance = await fetchMeta(true); + const instance = await fetchMeta(false); // if already subscribed if (subscription != null) { 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 1b43762cb6..fda4aa0bb8 100644 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -4,7 +4,7 @@ import { publishAdminStream } from "@/services/stream.js"; import { AbuseUserReports, UserProfiles, Users } from "@/models/index.js"; import { genId } from "backend-rs"; import { sendEmail } from "@/services/send-email.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { getUser } from "@/server/api/common/getters.js"; import { ApiError } from "@/server/api/error.js"; import define from "@/server/api/define.js"; @@ -86,7 +86,7 @@ export default define(meta, paramDef, async (ps, me) => { ], }); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); for (const moderator of moderators) { publishAdminStream(moderator.id, "newAbuseUserReport", { id: report.id, diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts index 517ef615b1..fe15ae18c0 100644 --- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts +++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -2,7 +2,7 @@ import { Brackets } from "typeorm"; import { Followings, Users } from "@/models/index.js"; import type { User } from "@/models/entities/user.js"; import define from "@/server/api/define.js"; -import { sqlLikeEscape } from "@/misc/sql-like-escape.js"; +import { sqlLikeEscape } from "backend-rs"; export const meta = { tags: ["users"], diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts index a15a0feb4b..df0701709b 100644 --- a/packages/backend/src/server/api/endpoints/users/search.ts +++ b/packages/backend/src/server/api/endpoints/users/search.ts @@ -2,7 +2,7 @@ import { Brackets } from "typeorm"; import { UserProfiles, Users } from "@/models/index.js"; import type { User } from "@/models/entities/user.js"; import define from "@/server/api/define.js"; -import { sqlLikeEscape } from "@/misc/sql-like-escape.js"; +import { sqlLikeEscape } from "backend-rs"; export const meta = { tags: ["users"], diff --git a/packages/backend/src/server/api/limiter.ts b/packages/backend/src/server/api/limiter.ts index f03f8754cf..d4a9353ade 100644 --- a/packages/backend/src/server/api/limiter.ts +++ b/packages/backend/src/server/api/limiter.ts @@ -2,7 +2,7 @@ import Limiter from "ratelimiter"; import Logger from "@/services/logger.js"; import { redisClient } from "@/db/redis.js"; import type { IEndpointMeta } from "./endpoints.js"; -import { convertMilliseconds } from "@/misc/convert-milliseconds.js"; +import { formatMilliseconds } from "backend-rs"; const logger = new Logger("limiter"); @@ -78,7 +78,7 @@ export const limiter = ( if (info.remaining === 0) { reject({ message: "RATE_LIMIT_EXCEEDED", - remainingTime: convertMilliseconds(info.resetMs - Date.now()), + remainingTime: formatMilliseconds(info.resetMs - Date.now()), }); } else { ok(); diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts index b392403578..b2259e6ed5 100644 --- a/packages/backend/src/server/api/mastodon/converters.ts +++ b/packages/backend/src/server/api/mastodon/converters.ts @@ -1,4 +1,4 @@ -import { Entity } from "megalodon"; +import type { Entity } from "megalodon"; import { toMastodonId } from "backend-rs"; function simpleConvert(data: any) { @@ -15,7 +15,19 @@ export function convertAnnouncement(announcement: Entity.Announcement) { return simpleConvert(announcement); } export function convertAttachment(attachment: Entity.Attachment) { - return simpleConvert(attachment); + const converted = simpleConvert(attachment); + // ref: https://github.com/whitescent/Mastify/pull/102 + if (converted.meta == null) return converted; + const result = { + ...converted, + meta: { + ...converted.meta, + original: { + ...converted.meta, + }, + }, + }; + return result; } export function convertFilter(filter: Entity.Filter) { return simpleConvert(filter); diff --git a/packages/backend/src/server/api/mastodon/endpoints/meta.ts b/packages/backend/src/server/api/mastodon/endpoints/meta.ts index fbad7d5ef4..5c304929a1 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/meta.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/meta.ts @@ -1,6 +1,6 @@ import { Entity } from "megalodon"; import config from "@/config/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { Users, Notes } from "@/models/index.js"; import { IsNull } from "typeorm"; import { MAX_NOTE_TEXT_LENGTH, FILE_TYPE_BROWSERSAFE } from "@/const.js"; @@ -10,7 +10,7 @@ export async function getInstance( contact: Entity.Account, ) { const [meta, totalUsers, totalStatuses] = await Promise.all([ - fetchMeta(), + fetchMeta(true), 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 5e6c0edaae..6fa70717e7 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/status.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts @@ -1,17 +1,15 @@ import Router from "@koa/router"; import { getClient } from "../ApiMastodonCompatibleService.js"; -import { emojiRegexAtStartToEnd } from "@/misc/emoji-regex.js"; import querystring from "node:querystring"; import qs from "qs"; import { convertTimelinesArgsId, limitToInt } from "./timeline.js"; -import { fromMastodonId } from "backend-rs"; +import { fetchMeta, fromMastodonId, isUnicodeEmoji } from "backend-rs"; import { convertAccount, convertAttachment, convertPoll, convertStatus, } from "../converters.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; import { apiLogger } from "@/server/api/logger.js"; import { inspect } from "node:util"; @@ -38,7 +36,7 @@ export function apiStatusMastodon(router: Router): void { } const text = body.status; const removed = text.replace(/@\S+/g, "").replace(/\s|​/g, ""); - const isDefaultEmoji = emojiRegexAtStartToEnd.test(removed); + const isDefaultEmoji = isUnicodeEmoji(removed); const isCustomEmoji = /^:[a-zA-Z0-9@_]+:$/.test(removed); if ((body.in_reply_to_id && isDefaultEmoji) || isCustomEmoji) { const a = await client.createEmojiReaction( @@ -213,7 +211,7 @@ export function apiStatusMastodon(router: Router): void { router.post<{ Params: { id: string } }>( "/v1/statuses/:id/favourite", async (ctx) => { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); @@ -235,7 +233,7 @@ export function apiStatusMastodon(router: Router): void { router.post<{ Params: { id: string } }>( "/v1/statuses/:id/unfavourite", async (ctx) => { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); 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/private/signin.ts b/packages/backend/src/server/api/private/signin.ts index 8692b01ad1..a7eb623062 100644 --- a/packages/backend/src/server/api/private/signin.ts +++ b/packages/backend/src/server/api/private/signin.ts @@ -10,12 +10,12 @@ import { AttestationChallenges, } from "@/models/index.js"; import type { ILocalUser } from "@/models/entities/user.js"; -import { genId } from "backend-rs"; import { - comparePassword, + genId, hashPassword, - isOldAlgorithm, -} from "@/misc/password.js"; + isOldPasswordAlgorithm, + verifyPassword, +} from "backend-rs"; import { verifyLogin, hash } from "@/server/api/2fa.js"; import { randomBytes } from "node:crypto"; import { IsNull } from "typeorm"; @@ -91,11 +91,11 @@ export default async (ctx: Koa.Context) => { const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - // Compare password - const same = await comparePassword(password, profile.password!); + // Compare passwords + const same = verifyPassword(password, profile.password!); - if (same && isOldAlgorithm(profile.password!)) { - profile.password = await hashPassword(password); + if (same && isOldPasswordAlgorithm(profile.password!)) { + profile.password = hashPassword(password); await UserProfiles.save(profile); } diff --git a/packages/backend/src/server/api/private/signup.ts b/packages/backend/src/server/api/private/signup.ts index 8179300884..5af5d65b50 100644 --- a/packages/backend/src/server/api/private/signup.ts +++ b/packages/backend/src/server/api/private/signup.ts @@ -1,20 +1,17 @@ import type Koa from "koa"; import rndstr from "rndstr"; -import { fetchMeta } from "@/misc/fetch-meta.js"; import { verifyHcaptcha, verifyRecaptcha } from "@/misc/captcha.js"; import { Users, RegistrationTickets, UserPendings } from "@/models/index.js"; import { signup } from "@/server/api/common/signup.js"; import config from "@/config/index.js"; import { sendEmail } from "@/services/send-email.js"; -import { genId } from "backend-rs"; +import { fetchMeta, genId, hashPassword } from "backend-rs"; import { validateEmailForAccount } from "@/services/validate-email-for-account.js"; -import { hashPassword } from "@/misc/password.js"; -import { inspect } from "node:util"; export default async (ctx: Koa.Context) => { const body = ctx.request.body; - const instance = await fetchMeta(true); + const instance = await fetchMeta(false); // Verify *Captcha // ただしテスト時はこの機構は障害となるため無効にする @@ -85,7 +82,7 @@ export default async (ctx: Koa.Context) => { const code = rndstr("a-z0-9", 16); // Generate hash of password - const hash = await hashPassword(password); + const hash = hashPassword(password); await UserPendings.insert({ id: genId(), 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 97295af57a..1760d5abf7 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -1,6 +1,5 @@ import Channel from "../channel.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { checkWordMute } from "backend-rs"; +import { checkWordMute, fetchMeta } from "backend-rs"; import { isInstanceMuted } from "@/misc/is-instance-muted.js"; import { isUserRelated } from "@/misc/is-user-related.js"; import type { Packed } from "@/misc/schema.js"; @@ -17,7 +16,7 @@ export default class extends Channel { } public async init(params: any) { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); 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 9052e7c2a5..5100a48efd 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -1,6 +1,5 @@ import Channel from "../channel.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { checkWordMute } from "backend-rs"; +import { checkWordMute, fetchMeta } from "backend-rs"; import { isUserRelated } from "@/misc/is-user-related.js"; import { isInstanceMuted } from "@/misc/is-instance-muted.js"; import type { Packed } from "@/misc/schema.js"; @@ -17,7 +16,7 @@ export default class extends Channel { } public async init(params: any) { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); 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 bd31c94f9d..2c9a38d677 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -1,6 +1,5 @@ import Channel from "../channel.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { checkWordMute } from "backend-rs"; +import { checkWordMute, fetchMeta } from "backend-rs"; import { isUserRelated } from "@/misc/is-user-related.js"; import type { Packed } from "@/misc/schema.js"; @@ -16,7 +15,7 @@ export default class extends Channel { } public async init(params: any) { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); 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 26c3cbfc68..5d0d6fc602 100644 --- a/packages/backend/src/server/api/stream/channels/recommended-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/recommended-timeline.ts @@ -1,6 +1,5 @@ import Channel from "../channel.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { checkWordMute } from "backend-rs"; +import { checkWordMute, fetchMeta } from "backend-rs"; import { isUserRelated } from "@/misc/is-user-related.js"; import { isInstanceMuted } from "@/misc/is-instance-muted.js"; import type { Packed } from "@/misc/schema.js"; @@ -17,7 +16,7 @@ export default class extends Channel { } public async init(params: any) { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if ( meta.disableRecommendedTimeline && !this.user!.isAdmin && @@ -37,7 +36,7 @@ export default class extends Channel { // チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または // チャンネルの投稿ではなく、全体公開のローカルの投稿 または // フォローしているチャンネルの投稿 の場合だけ - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if ( !( note.user.host != null && diff --git a/packages/backend/src/server/api/streaming.ts b/packages/backend/src/server/api/streaming.ts index a25984ec3e..12f97d8018 100644 --- a/packages/backend/src/server/api/streaming.ts +++ b/packages/backend/src/server/api/streaming.ts @@ -1,6 +1,6 @@ import type * as http from "node:http"; -import { EventEmitter } from "events"; -import type { ParsedUrlQuery } from "querystring"; +import { EventEmitter } from "node:events"; +import type { ParsedUrlQuery } from "node:querystring"; import * as websocket from "websocket"; import { subscriber as redisClient } from "@/db/redis.js"; diff --git a/packages/backend/src/server/file/byte-range-readable.ts b/packages/backend/src/server/file/byte-range-readable.ts index 96dcbc4a52..9699f95092 100644 --- a/packages/backend/src/server/file/byte-range-readable.ts +++ b/packages/backend/src/server/file/byte-range-readable.ts @@ -1,4 +1,4 @@ -import { Readable, ReadableOptions } from "node:stream"; +import { Readable, type ReadableOptions } from "node:stream"; import { Buffer } from "node:buffer"; import * as fs from "node:fs"; diff --git a/packages/backend/src/server/index.ts b/packages/backend/src/server/index.ts index 2a6dfdf674..17358a4758 100644 --- a/packages/backend/src/server/index.ts +++ b/packages/backend/src/server/index.ts @@ -13,15 +13,14 @@ import koaLogger from "koa-logger"; import * as slow from "koa-slow"; import { IsNull } from "typeorm"; -import config from "@/config/index.js"; +import config, { envOption } from "@/config/index.js"; import Logger from "@/services/logger.js"; import { Users } from "@/models/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { genIdenticon } from "@/misc/gen-identicon.js"; import { createTemp } from "@/misc/create-temp.js"; import { stringToAcct } from "backend-rs"; -import { envOption } from "@/env.js"; -import megalodon, { MegalodonInterface } from "megalodon"; +import megalodon, { type MegalodonInterface } from "megalodon"; import activityPub from "./activitypub.js"; import nodeinfo from "./nodeinfo.js"; import wellKnown from "./well-known.js"; @@ -126,7 +125,7 @@ router.get("/avatar/@:acct", async (ctx) => { }); router.get("/identicon/:x", async (ctx) => { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.enableIdenticonGeneration) { const [temp, cleanup] = await createTemp(); await genIdenticon(ctx.params.x, fs.createWriteStream(temp)); diff --git a/packages/backend/src/server/nodeinfo.ts b/packages/backend/src/server/nodeinfo.ts index 1cb8eb1eaf..7359878b19 100644 --- a/packages/backend/src/server/nodeinfo.ts +++ b/packages/backend/src/server/nodeinfo.ts @@ -1,6 +1,6 @@ import Router from "@koa/router"; import config from "@/config/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { Users, Notes } from "@/models/index.js"; import { IsNull, MoreThan } from "typeorm"; import { MAX_NOTE_TEXT_LENGTH, MAX_CAPTION_TEXT_LENGTH } from "@/const.js"; @@ -27,7 +27,7 @@ const nodeinfo2 = async () => { const now = Date.now(); const [meta, total, activeHalfyear, activeMonth, localPosts] = await Promise.all([ - fetchMeta(true), + fetchMeta(false), Users.count({ where: { host: IsNull() } }), Users.count({ where: { diff --git a/packages/backend/src/server/web/feed.ts b/packages/backend/src/server/web/feed.ts index e6b09b4f4f..f3b3c97c32 100644 --- a/packages/backend/src/server/web/feed.ts +++ b/packages/backend/src/server/web/feed.ts @@ -2,7 +2,16 @@ import { Feed } from "feed"; import { In, IsNull } from "typeorm"; import config from "@/config/index.js"; 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"; + +/** + * If there is this part in the note, it will cause CDATA to be terminated early. + */ +function escapeCDATA(str: string) { + return str.replaceAll("]]>", "]]]]>"); +} export default async function ( user: User, @@ -15,7 +24,7 @@ export default async function ( const author = { link: `${config.url}/@${user.username}`, email: `${user.username}@${config.host}`, - name: user.name || user.username, + name: escapeCDATA(user.name || user.username), }; const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); @@ -44,11 +53,13 @@ export default async function ( title: `${author.name} (@${user.username}@${config.host})`, updated: notes[0].createdAt, generator: "Firefish", - description: `${user.notesCount} Notes, ${ - profile.ffVisibility === "public" ? user.followingCount : "?" - } Following, ${ - profile.ffVisibility === "public" ? user.followersCount : "?" - } Followers${profile.description ? ` · ${profile.description}` : ""}`, + description: escapeCDATA( + `${user.notesCount} Notes, ${ + profile.ffVisibility === "public" ? user.followingCount : "?" + } Following, ${ + profile.ffVisibility === "public" ? user.followersCount : "?" + } Followers${profile.description ? ` · ${profile.description}` : ""}`, + ), link: author.link, image: await Users.getAvatarUrl(user), feedLinks: { @@ -88,19 +99,23 @@ export default async function ( } feed.addItem({ - title: title - .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "") - .substring(0, 100), + title: escapeCDATA( + title + .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "") + .substring(0, 100), + ), link: `${config.url}/notes/${note.id}`, date: note.createdAt, description: note.cw - ? note.cw.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "") + ? escapeCDATA(note.cw.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "")) : undefined, - content: contentStr.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, ""), + content: escapeCDATA( + contentStr.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, ""), + ), }); } - async function noteToString(note, isTheNote = false) { + async function noteToString(note: Note, isTheNote = false) { const author = isTheNote ? null : await Users.findOneBy({ id: note.userId }); @@ -135,7 +150,10 @@ export default async function ( }">${file.name}`; } } - outstr += `${note.cw ? note.cw + "
" : ""}${note.text || ""}${fileEle}`; + + outstr += `${note.cw ? note.cw + "
" : ""}${ + getNoteHtml(note) || "" + }${fileEle}`; if (isTheNote) { outstr += ` { const url = decodeURI(ctx.path); if (url === bullBoardPath || url.startsWith(`${bullBoardPath}/`)) { + if (!url.startsWith(`${bullBoardPath}/static/`)) { + ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); + } + const token = ctx.cookies.get("token"); if (token == null) { ctx.status = 401; @@ -326,7 +328,7 @@ const getFeed = async ( noRenotes: string, noReplies: string, ) => { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.privateMode) { return; } @@ -475,7 +477,7 @@ const userPage: Router.Middleware = async (ctx, next) => { } const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); const me = profile.fields ? profile.fields .filter((filed) => filed.value?.match(/^https?:/)) @@ -518,22 +520,22 @@ router.get("/notes/:note", async (ctx, next) => { }); try { - 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(); + const meta = await fetchMeta(true); 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), }); ctx.set("Cache-Control", "public, max-age=15"); @@ -558,7 +560,7 @@ router.get("/posts/:note", async (ctx, next) => { if (note) { const _note = await Notes.pack(note); const profile = await UserProfiles.findOneByOrFail({ userId: note.userId }); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); await ctx.render("note", { ...metaToPugArgs(meta), note: _note, @@ -596,7 +598,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(); + const meta = await fetchMeta(true); await ctx.render("page", { ...metaToPugArgs(meta), page: _page, @@ -628,7 +630,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(); + const meta = await fetchMeta(true); await ctx.render("clip", { ...metaToPugArgs(meta), clip: _clip, @@ -653,7 +655,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(); + const meta = await fetchMeta(true); await ctx.render("gallery-post", { ...metaToPugArgs(meta), post: _post, @@ -679,7 +681,7 @@ router.get("/channels/:channel", async (ctx, next) => { if (channel) { const _channel = await Channels.pack(channel); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); await ctx.render("channel", { ...metaToPugArgs(meta), channel: _channel, @@ -732,7 +734,7 @@ router.get("/api/v1/streaming", async (ctx) => { // Render base html for all requests router.get("(.*)", async (ctx) => { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); 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 bbcf639ffe..a4c615c7ab 100644 --- a/packages/backend/src/server/web/manifest.ts +++ b/packages/backend/src/server/web/manifest.ts @@ -1,5 +1,5 @@ import type Koa from "koa"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import config from "@/config/index.js"; import manifest from "./manifest.json" assert { type: "json" }; @@ -8,7 +8,7 @@ export const manifestHandler = async (ctx: Koa.Context) => { //const res = structuredClone(manifest); const res = JSON.parse(JSON.stringify(manifest)); - const instance = await fetchMeta(true); + const instance = await fetchMeta(false); res.short_name = instance.name || "Firefish"; res.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 07d3bf7f2c..f59f3f357a 100644 --- a/packages/backend/src/server/web/url-preview.ts +++ b/packages/backend/src/server/web/url-preview.ts @@ -1,6 +1,6 @@ import type Koa from "koa"; import summaly from "summaly"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import Logger from "@/services/logger.js"; import config from "@/config/index.js"; import { query } from "@/prelude/url.js"; @@ -22,7 +22,7 @@ export const urlPreviewHandler = async (ctx: Koa.Context) => { return; } - const meta = await fetchMeta(); + const meta = await fetchMeta(true); logger.info( meta.summalyProxy diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index bdbe153fbe..738d3ffc01 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -72,8 +72,8 @@ html div#splash img#splashIcon(src= splashIcon || `/static-assets/splash.svg?${ timestamp }`) span#splashText - block randomMOTD - = randomMOTD + block randomMotd + = randomMotd div#splashSpinner diff --git a/packages/backend/src/services/chart/charts/active-users.ts b/packages/backend/src/services/chart/charts/active-users.ts index 3f4b7e3381..067334005e 100644 --- a/packages/backend/src/services/chart/charts/active-users.ts +++ b/packages/backend/src/services/chart/charts/active-users.ts @@ -1,4 +1,3 @@ -import type { KVs } from "../core.js"; import Chart from "../core.js"; import type { User } from "@/models/entities/user.js"; import { name, schema } from "./entities/active-users.js"; diff --git a/packages/backend/src/services/create-system-user.ts b/packages/backend/src/services/create-system-user.ts index 345de7ad17..802c59b288 100644 --- a/packages/backend/src/services/create-system-user.ts +++ b/packages/backend/src/services/create-system-user.ts @@ -4,17 +4,16 @@ import { genRsaKeyPair } from "@/misc/gen-key-pair.js"; import { User } from "@/models/entities/user.js"; import { UserProfile } from "@/models/entities/user-profile.js"; import { IsNull } from "typeorm"; -import { genId } from "backend-rs"; +import { genId, 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"; -import { hashPassword } from "@/misc/password.js"; export async function createSystemUser(username: string) { const password = uuid(); // Generate hash of password - const hash = await hashPassword(password); + const hash = hashPassword(password); // Generate secret const secret = generateNativeUserToken(); diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts index 6320277eef..d180bbabf3 100644 --- a/packages/backend/src/services/drive/add-file.ts +++ b/packages/backend/src/services/drive/add-file.ts @@ -6,7 +6,7 @@ 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 { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { contentDisposition } from "@/misc/content-disposition.js"; import { getFileInfo } from "@/misc/get-file-info.js"; import { @@ -16,6 +16,7 @@ import { UserProfiles, } from "@/models/index.js"; import { DriveFile } from "@/models/entities/drive-file.js"; +import type { DriveFileUsageHint } from "@/models/entities/drive-file.js"; import type { IRemoteUser, User } from "@/models/entities/user.js"; import { genId } from "backend-rs"; import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js"; @@ -65,6 +66,7 @@ function urlPathJoin( * @param type Content-Type for original * @param hash Hash for original * @param size Size for original + * @param usage Optional usage hint for file (f.e. "userAvatar") */ async function save( file: DriveFile, @@ -73,11 +75,12 @@ async function save( type: string, hash: string, size: number, + usage: DriveFileUsageHint = null, ): Promise { // thunbnail, webpublic を必要なら生成 const alts = await generateAlts(path, type, !file.uri); - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if (meta.useObjectStorage) { //#region ObjectStorage params @@ -161,6 +164,7 @@ async function save( file.md5 = hash; file.size = size; file.storedInternal = false; + file.usageHint = usage ?? null; return await DriveFiles.insert(file).then((x) => DriveFiles.findOneByOrFail(x.identifiers[0]), @@ -204,6 +208,7 @@ async function save( file.type = type; file.md5 = hash; file.size = size; + file.usageHint = usage ?? null; return await DriveFiles.insert(file).then((x) => DriveFiles.findOneByOrFail(x.identifiers[0]), @@ -360,7 +365,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(); + const meta = await fetchMeta(true); const params = { Bucket: meta.objectStorageBucket, @@ -450,6 +455,9 @@ type AddFileArgs = { requestIp?: string | null; requestHeaders?: Record | null; + + /** Whether this file has a known use case, like user avatar or instance icon */ + usageHint?: DriveFileUsageHint; }; /** @@ -469,6 +477,7 @@ export async function addFile({ sensitive = null, requestIp = null, requestHeaders = null, + usageHint = null, }: AddFileArgs): Promise { const info = await getFileInfo(path); logger.info(`${JSON.stringify(info)}`); @@ -495,7 +504,7 @@ export async function addFile({ const usage = await DriveFiles.calcDriveUsageOf(user); const u = await Users.findOneBy({ id: user.id }); - const instance = await fetchMeta(); + const instance = await fetchMeta(true); let driveCapacity = 1024 * 1024 * @@ -567,7 +576,7 @@ export async function addFile({ : null; const folder = await fetchFolder(); - const instance = await fetchMeta(); + const instance = await fetchMeta(true); let file = new DriveFile(); file.id = genId(); @@ -581,6 +590,7 @@ export async function addFile({ file.isLink = isLink; file.requestIp = requestIp; file.requestHeaders = requestHeaders; + file.usageHint = usageHint; file.isSensitive = user ? Users.isLocalUser(user) && (instance!.markLocalFilesNsfwByDefault || profile!.alwaysMarkNsfw) @@ -639,6 +649,7 @@ export async function addFile({ info.type.mime, info.md5, info.size, + usageHint, ); } diff --git a/packages/backend/src/services/drive/delete-file.ts b/packages/backend/src/services/drive/delete-file.ts index 16c0219e71..b4b5580a1c 100644 --- a/packages/backend/src/services/drive/delete-file.ts +++ b/packages/backend/src/services/drive/delete-file.ts @@ -2,7 +2,7 @@ import type { DriveFile } from "@/models/entities/drive-file.js"; import { InternalStorage } from "./internal-storage.js"; import { DriveFiles } from "@/models/index.js"; import { createDeleteObjectStorageFileJob } from "@/queue/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import { getS3 } from "./s3.js"; import { v4 as uuid } from "uuid"; @@ -82,7 +82,7 @@ async function postProcess(file: DriveFile, isExpired = false) { } export async function deleteObjectStorageFile(key: string) { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); const s3 = getS3(meta); diff --git a/packages/backend/src/services/drive/upload-from-url.ts b/packages/backend/src/services/drive/upload-from-url.ts index 551d3757ca..e7b084bda1 100644 --- a/packages/backend/src/services/drive/upload-from-url.ts +++ b/packages/backend/src/services/drive/upload-from-url.ts @@ -3,7 +3,10 @@ import type { User } from "@/models/entities/user.js"; import { createTemp } from "@/misc/create-temp.js"; import { downloadUrl, isPrivateIp } from "@/misc/download-url.js"; import type { DriveFolder } from "@/models/entities/drive-folder.js"; -import type { DriveFile } from "@/models/entities/drive-file.js"; +import type { + DriveFile, + DriveFileUsageHint, +} from "@/models/entities/drive-file.js"; import { DriveFiles } from "@/models/index.js"; import { driveLogger } from "./logger.js"; import { addFile } from "./add-file.js"; @@ -13,7 +16,11 @@ const logger = driveLogger.createSubLogger("downloader"); type Args = { url: string; - user: { id: User["id"]; host: User["host"] } | null; + user: { + id: User["id"]; + host: User["host"]; + driveCapacityOverrideMb: User["driveCapacityOverrideMb"]; + } | null; folderId?: DriveFolder["id"] | null; uri?: string | null; sensitive?: boolean; @@ -22,6 +29,7 @@ type Args = { comment?: string | null; requestIp?: string | null; requestHeaders?: Record | null; + usageHint?: DriveFileUsageHint; }; export async function uploadFromUrl({ @@ -35,6 +43,7 @@ export async function uploadFromUrl({ comment = null, requestIp = null, requestHeaders = null, + usageHint = null, }: Args): Promise { const parsedUrl = new URL(url); if ( @@ -75,9 +84,10 @@ export async function uploadFromUrl({ sensitive, requestIp, requestHeaders, + usageHint, }); logger.succ(`Got: ${driveFile.id}`); - return driveFile!; + return driveFile; } catch (e) { logger.error(`Failed to create drive file:\n${inspect(e)}`); throw e; diff --git a/packages/backend/src/services/fetch-rel-me.ts b/packages/backend/src/services/fetch-rel-me.ts index c9a37d1c88..70faa01aa7 100644 --- a/packages/backend/src/services/fetch-rel-me.ts +++ b/packages/backend/src/services/fetch-rel-me.ts @@ -1,4 +1,5 @@ import { Window } from "happy-dom"; +import type { HTMLAnchorElement, HTMLLinkElement } from "happy-dom"; import config from "@/config/index.js"; async function getRelMeLinks(url: string): Promise { diff --git a/packages/backend/src/services/logger.ts b/packages/backend/src/services/logger.ts index 63eb3d00b9..f4b4454ef8 100644 --- a/packages/backend/src/services/logger.ts +++ b/packages/backend/src/services/logger.ts @@ -2,8 +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 { envOption } from "@/env.js"; -import config from "@/config/index.js"; +import config, { envOption } from "@/config/index.js"; import * as SyslogPro from "syslog-pro"; @@ -29,9 +28,9 @@ export default class Logger { if (config.syslog) { this.syslogClient = new SyslogPro.RFC5424({ - applacationName: "Firefish", + applicationName: "Firefish", timestamp: true, - encludeStructuredData: true, + includeStructuredData: true, color: true, extendedColor: true, server: { @@ -56,7 +55,6 @@ export default class Logger { subDomains: Domain[] = [], store = true, ): void { - if (envOption.quiet) return; if ( !(typeof config.logLevel === "undefined") && !config.logLevel.includes(level) @@ -146,12 +144,12 @@ export default class Logger { } } + // Used when the process can't continue (fatal error) public error( x: string | Error, data?: Record | null, important = false, ): void { - // 実行を継続できない状況で使う if (x instanceof Error) { data = data || {}; data.e = x; @@ -168,30 +166,30 @@ export default class Logger { } } + // Used when the process can continue but some action should be taken public warn( message: string, data?: Record | null, important = false, ): void { - // 実行を継続できるが改善すべき状況で使う this.log("warning", message, data, important); } + // Used when something is successful public succ( message: string, data?: Record | null, important = false, ): void { - // 何かに成功した状況で使う this.log("success", message, data, important); } + // Used for debugging (information necessary for developers but unnecessary for users) public debug( message: string, data?: Record | null, important = false, ): void { - // Used for debugging (information necessary for developers but unnecessary for users) // Fixed if statement is ignored when logLevel includes debug if ( config.logLevel?.includes("debug") || @@ -202,12 +200,12 @@ export default class Logger { } } + // Other generic logs public info( message: string, data?: Record | null, important = false, ): void { - // それ以外 this.log("info", message, data, important); } } diff --git a/packages/backend/src/services/note/delete.ts b/packages/backend/src/services/note/delete.ts index ac3515cfae..be3bf1e8b2 100644 --- a/packages/backend/src/services/note/delete.ts +++ b/packages/backend/src/services/note/delete.ts @@ -38,7 +38,6 @@ async function recalculateNotesCountOfLocalUser(user: { export default async function ( user: { id: User["id"]; uri: User["uri"]; host: User["host"] }, note: Note, - quiet = false, deleteFromDb = true, ) { const deletedAt = new Date(); @@ -67,87 +66,80 @@ export default async function ( } const instanceNotesCountDecreasement: Record = {}; - if (!quiet) { - // Only broadcast "deleted" to local if the note is deleted from db + // Only broadcast "deleted" to local if the note is deleted from db + if (deleteFromDb) { + publishNoteStream(note.id, "deleted", { + deletedAt: deletedAt, + }); + } + + //#region ローカルの投稿なら削除アクティビティを配送 + if (Users.isLocalUser(user) && !note.localOnly) { + let renote: Note | null = null; + + // if deletd note is renote + if ( + note.renoteId && + note.text == null && + !note.hasPoll && + (note.fileIds == null || note.fileIds.length === 0) + ) { + renote = await Notes.findOneBy({ + id: note.renoteId, + }); + } + + const content = renderActivity( + renote + ? renderUndo( + renderAnnounce( + renote.uri || `${config.url}/notes/${renote.id}`, + note, + ), + user, + ) + : renderDelete(renderTombstone(`${config.url}/notes/${note.id}`), user), + ); + + deliverToConcerned(user, note, content); + } + + // also deliever delete activity to cascaded notes + for (const cascadingNote of cascadingNotes) { if (deleteFromDb) { - publishNoteStream(note.id, "deleted", { + // For other notes, publishNoteStream is also required. + publishNoteStream(cascadingNote.id, "deleted", { deletedAt: deletedAt, }); } - //#region ローカルの投稿なら削除アクティビティを配送 - if (Users.isLocalUser(user) && !note.localOnly) { - let renote: Note | null = null; - - // if deletd note is renote - if ( - note.renoteId && - note.text == null && - !note.hasPoll && - (note.fileIds == null || note.fileIds.length === 0) - ) { - renote = await Notes.findOneBy({ - id: note.renoteId, - }); - } - - const content = renderActivity( - renote - ? renderUndo( - renderAnnounce( - renote.uri || `${config.url}/notes/${renote.id}`, - note, - ), - user, - ) - : renderDelete( - renderTombstone(`${config.url}/notes/${note.id}`), - user, - ), - ); - - deliverToConcerned(user, note, content); + if (!cascadingNote.user) continue; + if (!Users.isLocalUser(cascadingNote.user)) { + if (!Users.isRemoteUser(cascadingNote.user)) continue; + instanceNotesCountDecreasement[cascadingNote.user.host] ??= 0; + instanceNotesCountDecreasement[cascadingNote.user.host]++; + continue; // filter out remote users } + affectedLocalUsers[cascadingNote.user.id] ??= cascadingNote.user; + if (cascadingNote.localOnly) continue; // filter out local-only notes + const content = renderActivity( + renderDelete( + renderTombstone(`${config.url}/notes/${cascadingNote.id}`), + cascadingNote.user, + ), + ); + deliverToConcerned(cascadingNote.user, cascadingNote, content); + } + //#endregion - // also deliever delete activity to cascaded notes - for (const cascadingNote of cascadingNotes) { - if (deleteFromDb) { - // For other notes, publishNoteStream is also required. - publishNoteStream(cascadingNote.id, "deleted", { - deletedAt: deletedAt, - }); - } - - if (!cascadingNote.user) continue; - if (!Users.isLocalUser(cascadingNote.user)) { - if (!Users.isRemoteUser(cascadingNote.user)) continue; - instanceNotesCountDecreasement[cascadingNote.user.host] ??= 0; - instanceNotesCountDecreasement[cascadingNote.user.host]++; - continue; // filter out remote users - } - affectedLocalUsers[cascadingNote.user.id] ??= cascadingNote.user; - if (cascadingNote.localOnly) continue; // filter out local-only notes - const content = renderActivity( - renderDelete( - renderTombstone(`${config.url}/notes/${cascadingNote.id}`), - cascadingNote.user, - ), - ); - deliverToConcerned(cascadingNote.user, cascadingNote, content); - } - //#endregion - - if (Users.isRemoteUser(user)) { - instanceNotesCountDecreasement[user.host] ??= 0; - instanceNotesCountDecreasement[user.host]++; - } - for (const [host, count] of Object.entries( - instanceNotesCountDecreasement, - )) { - registerOrFetchInstanceDoc(host).then((i) => { - Instances.decrement({ id: i.id }, "notesCount", count); - }); - } + if (Users.isRemoteUser(user)) { + instanceNotesCountDecreasement[user.host] ??= 0; + instanceNotesCountDecreasement[user.host]++; + } + for (const [host, count] of Object.entries(instanceNotesCountDecreasement)) { + registerOrFetchInstanceDoc(host).then((i) => { + Instances.decrement({ id: i.id }, "notesCount", count); + }); } if (deleteFromDb) { diff --git a/packages/backend/src/services/note/reaction/create.ts b/packages/backend/src/services/note/reaction/create.ts index a07ffdabad..3b8b97cefd 100644 --- a/packages/backend/src/services/note/reaction/create.ts +++ b/packages/backend/src/services/note/reaction/create.ts @@ -2,7 +2,6 @@ import { publishNoteStream } from "@/services/stream.js"; import { renderLike } from "@/remote/activitypub/renderer/like.js"; import DeliverManager from "@/remote/activitypub/deliver-manager.js"; import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import { toDbReaction, decodeReaction } from "@/misc/reaction-lib.js"; import type { User, IRemoteUser } from "@/models/entities/user.js"; import type { Note } from "@/models/entities/note.js"; import { @@ -14,7 +13,7 @@ import { Blockings, } from "@/models/index.js"; import { IsNull, Not } from "typeorm"; -import { genId } from "backend-rs"; +import { decodeReaction, genId, 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"; @@ -95,7 +94,7 @@ export default async ( const emoji = await Emojis.findOne({ where: { - name: decodedReaction.name, + name: decodedReaction.name ?? undefined, host: decodedReaction.host ?? IsNull(), }, select: ["name", "host", "originalUrl", "publicUrl"], diff --git a/packages/backend/src/services/note/reaction/delete.ts b/packages/backend/src/services/note/reaction/delete.ts index 49879a0c02..e5416a78a8 100644 --- a/packages/backend/src/services/note/reaction/delete.ts +++ b/packages/backend/src/services/note/reaction/delete.ts @@ -7,7 +7,7 @@ import { IdentifiableError } from "@/misc/identifiable-error.js"; import type { User, IRemoteUser } from "@/models/entities/user.js"; import type { Note } from "@/models/entities/note.js"; import { NoteReactions, Users, Notes } from "@/models/index.js"; -import { decodeReaction } from "@/misc/reaction-lib.js"; +import { decodeReaction } from "backend-rs"; export default async ( user: { id: User["id"]; host: User["host"] }, diff --git a/packages/backend/src/services/note/read.ts b/packages/backend/src/services/note/read.ts index 3c49501416..d7fda27a85 100644 --- a/packages/backend/src/services/note/read.ts +++ b/packages/backend/src/services/note/read.ts @@ -1,12 +1,7 @@ import { publishMainStream } from "@/services/stream.js"; import type { Note } from "@/models/entities/note.js"; import type { User } from "@/models/entities/user.js"; -import { - NoteUnreads, - Users, - Followings, - ChannelFollowings, -} from "@/models/index.js"; +import { NoteUnreads, Followings, ChannelFollowings } from "@/models/index.js"; import { Not, IsNull, In } from "typeorm"; import type { Channel } from "@/models/entities/channel.js"; import { readNotificationByQuery } from "@/server/api/common/read-notification.js"; @@ -120,34 +115,4 @@ export default async function ( ]), }); } - - // if (readAntennaNotes.length > 0) { - // await AntennaNotes.update( - // { - // antennaId: In(myAntennas.map((a) => a.id)), - // noteId: In(readAntennaNotes.map((n) => n.id)), - // }, - // { - // read: true, - // }, - // ); - - // // TODO: まとめてクエリしたい - // for (const antenna of myAntennas) { - // const count = await AntennaNotes.countBy({ - // antennaId: antenna.id, - // read: false, - // }); - - // if (count === 0) { - // publishMainStream(userId, "readAntenna", antenna); - // } - // } - - // Users.getHasUnreadAntenna(userId).then((unread) => { - // if (!unread) { - // publishMainStream(userId, "readAllAntennas"); - // } - // }); - // } } diff --git a/packages/backend/src/services/push-notification.ts b/packages/backend/src/services/push-notification.ts index 09749059a9..3f1f2cfb1a 100644 --- a/packages/backend/src/services/push-notification.ts +++ b/packages/backend/src/services/push-notification.ts @@ -1,9 +1,8 @@ import push from "web-push"; import config from "@/config/index.js"; import { SwSubscriptions } from "@/models/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta, getNoteSummary } from "backend-rs"; import type { Packed } from "@/misc/schema.js"; -import { getNoteSummary } from "@/misc/get-note-summary.js"; // Defined also packages/sw/types.ts#L14-L21 type pushNotificationsTypes = { @@ -17,15 +16,15 @@ type pushNotificationsTypes = { // プッシュメッセージサーバーには文字数制限があるため、内容を削減します function truncateNotification(notification: Packed<"Notification">): any { - if (notification.note) { + if (notification.note != null) { return { ...notification, note: { ...notification.note, - // textをgetNoteSummaryしたものに置き換える + // replace the text with summary text: getNoteSummary( - notification.type === "renote" - ? (notification.note.renote as Packed<"Note">) + notification.type === "renote" && notification.note.renote != null + ? notification.note.renote : notification.note, ), @@ -45,7 +44,7 @@ export async function pushNotification( type: T, body: pushNotificationsTypes[T], ) { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); if ( !meta.enableServiceWorker || diff --git a/packages/backend/src/services/send-email.ts b/packages/backend/src/services/send-email.ts index aa96cfc014..11a899d267 100644 --- a/packages/backend/src/services/send-email.ts +++ b/packages/backend/src/services/send-email.ts @@ -1,5 +1,5 @@ import * as nodemailer from "nodemailer"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; import Logger from "@/services/logger.js"; import config from "@/config/index.js"; import { inspect } from "node:util"; @@ -12,7 +12,7 @@ export async function sendEmail( html: string, text: string, ) { - const meta = await fetchMeta(true); + const meta = await fetchMeta(false); const iconUrl = `${config.url}/static-assets/mi-white.png`; const emailSettingUrl = `${config.url}/settings/email`; diff --git a/packages/backend/src/services/validate-email-for-account.ts b/packages/backend/src/services/validate-email-for-account.ts index 4d05afcc6d..5aa091a5ac 100644 --- a/packages/backend/src/services/validate-email-for-account.ts +++ b/packages/backend/src/services/validate-email-for-account.ts @@ -1,12 +1,12 @@ import { validate as validateEmail } from "deep-email-validator"; import { UserProfiles } from "@/models/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; +import { fetchMeta } from "backend-rs"; export async function validateEmailForAccount(emailAddress: string): Promise<{ available: boolean; reason: null | "used" | "format" | "disposable" | "mx" | "smtp"; }> { - const meta = await fetchMeta(); + const meta = await fetchMeta(true); const exist = await UserProfiles.countBy({ emailVerified: true, diff --git a/packages/client/.eslintrc.json b/packages/client/.eslintrc.json index b0e97b2fa6..37d80f6588 100644 --- a/packages/client/.eslintrc.json +++ b/packages/client/.eslintrc.json @@ -4,10 +4,10 @@ "ignorePatterns": ["**/*.json5"], "rules": { "file-progress/activate": 1, - "prettier/prettier": 0, - "one-var": ["error", "never"], + "prettier/prettier": "off", + "one-var": ["warn", "never"], "@typescript-eslint/no-unused-vars": [ - "error", + "warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", diff --git a/packages/client/@types/global.d.ts b/packages/client/@types/global.d.ts index c757482900..3ac4f09b0c 100644 --- a/packages/client/@types/global.d.ts +++ b/packages/client/@types/global.d.ts @@ -1,3 +1,4 @@ +// biome-ignore lint/suspicious/noExplicitAny: type FIXME = any; declare const _LANGS_: string[][]; diff --git a/packages/client/@types/window.d.ts b/packages/client/@types/window.d.ts new file mode 100644 index 0000000000..1ae20c0d20 --- /dev/null +++ b/packages/client/@types/window.d.ts @@ -0,0 +1,6 @@ +declare global { + interface Window { + __misskey_input_ref__?: HTMLInputElement | null; + } +} +export type {}; diff --git a/packages/client/package.json b/packages/client/package.json index 594f07c607..97a5f83ef7 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -67,9 +67,9 @@ "photoswipe": "5.4.3", "prismjs": "1.29.0", "punycode": "2.3.1", - "rollup": "4.14.1", + "rollup": "4.14.2", "s-age": "1.1.2", - "sass": "1.74.1", + "sass": "1.75.0", "seedrandom": "3.0.5", "stringz": "2.1.0", "swiper": "11.1.1", @@ -88,6 +88,6 @@ "vue-draggable-plus": "^0.4.0", "vue-plyr": "^7.0.0", "vue-prism-editor": "2.0.0-alpha.2", - "vue-tsc": "2.0.12" + "vue-tsc": "2.0.13" } } diff --git a/packages/client/src/components/MkAbuseReport.vue b/packages/client/src/components/MkAbuseReport.vue index 5536523948..b190652052 100644 --- a/packages/client/src/components/MkAbuseReport.vue +++ b/packages/client/src/components/MkAbuseReport.vue @@ -67,6 +67,7 @@ {{ emoji.emoji }} ({{ emoji.aliasOf }}) import { onMounted, ref } from "vue"; +import type { entities } from "firefish-js"; import * as os from "@/os"; const props = defineProps<{ userIds: string[]; }>(); -const users = ref([]); +const users = ref([]); onMounted(async () => { users.value = await os.api("users/show", { diff --git a/packages/client/src/components/MkButton.vue b/packages/client/src/components/MkButton.vue index 48864be62a..ab62fd166c 100644 --- a/packages/client/src/components/MkButton.vue +++ b/packages/client/src/components/MkButton.vue @@ -16,7 +16,7 @@ v-else class="bghgjjyj _button" :class="{ inline, primary, gradate, danger, rounded, full, mini }" - :to="to" + :to="to!" @mousedown="onMousedown" >
@@ -36,6 +36,7 @@ const props = defineProps<{ gradate?: boolean; rounded?: boolean; inline?: boolean; + // FIXME: if `link`, `to` is necessary link?: boolean; to?: string; autofocus?: boolean; @@ -47,7 +48,7 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (ev: "click", payload: MouseEvent): void; + click: [payload: MouseEvent]; }>(); const el = ref(null); @@ -61,11 +62,19 @@ onMounted(() => { } }); -function distance(p, q): number { +function distance( + p: { x: number; y: number }, + q: { x: number; y: number }, +): number { return Math.hypot(p.x - q.x, p.y - q.y); } -function calcCircleScale(boxW, boxH, circleCenterX, circleCenterY): number { +function calcCircleScale( + boxW: number, + boxH: number, + circleCenterX: number, + circleCenterY: number, +): number { const origin = { x: circleCenterX, y: circleCenterY }; const dist1 = distance({ x: 0, y: 0 }, origin); const dist2 = distance({ x: boxW, y: 0 }, origin); @@ -79,8 +88,8 @@ function onMousedown(evt: MouseEvent): void { const rect = target.getBoundingClientRect(); const ripple = document.createElement("div"); - ripple.style.top = (evt.clientY - rect.top - 1).toString() + "px"; - ripple.style.left = (evt.clientX - rect.left - 1).toString() + "px"; + ripple.style.top = `${(evt.clientY - rect.top - 1).toString()}px`; + ripple.style.left = `${(evt.clientX - rect.left - 1).toString()}px`; ripples.value!.appendChild(ripple); @@ -97,7 +106,7 @@ function onMousedown(evt: MouseEvent): void { vibrate(10); window.setTimeout(() => { - ripple.style.transform = "scale(" + scale / 2 + ")"; + ripple.style.transform = `scale(${scale / 2})`; }, 1); window.setTimeout(() => { ripple.style.transition = "all 1s ease"; diff --git a/packages/client/src/components/MkCaptcha.vue b/packages/client/src/components/MkCaptcha.vue index 146c512fb8..135eee8e90 100644 --- a/packages/client/src/components/MkCaptcha.vue +++ b/packages/client/src/components/MkCaptcha.vue @@ -50,7 +50,7 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (ev: "update:modelValue", v: string | null): void; + "update:modelValue": [v: string | null]; }>(); const available = ref(false); @@ -93,7 +93,9 @@ if (loaded) { src: src.value, }), ) - ).addEventListener("load", () => (available.value = true)); + ) + // biome-ignore lint/suspicious/noAssignInExpressions: assign it intentially + .addEventListener("load", () => (available.value = true)); } function reset() { diff --git a/packages/client/src/components/MkChannelFollowButton.vue b/packages/client/src/components/MkChannelFollowButton.vue index c1910bc595..3ff907b25b 100644 --- a/packages/client/src/components/MkChannelFollowButton.vue +++ b/packages/client/src/components/MkChannelFollowButton.vue @@ -24,13 +24,14 @@ diff --git a/packages/client/src/components/MkChannelPreview.vue b/packages/client/src/components/MkChannelPreview.vue index f824a1b2f5..b7462f5504 100644 --- a/packages/client/src/components/MkChannelPreview.vue +++ b/packages/client/src/components/MkChannelPreview.vue @@ -52,11 +52,12 @@ diff --git a/packages/client/src/components/MkCode.core.vue b/packages/client/src/components/MkCode.core.vue index 11720b4f62..7e3cdac562 100644 --- a/packages/client/src/components/MkCode.core.vue +++ b/packages/client/src/components/MkCode.core.vue @@ -29,6 +29,7 @@ if (props.lang != null && !(props.lang in Prism.languages)) { const { lang } = props; loadLanguage(props.lang).then( // onLoaded + // biome-ignore lint/suspicious/noAssignInExpressions: assign intentionally () => (prismLang.value = lang), // onError () => {}, diff --git a/packages/client/src/components/MkContainer.vue b/packages/client/src/components/MkContainer.vue index 404f984415..6f7f19dbf0 100644 --- a/packages/client/src/components/MkContainer.vue +++ b/packages/client/src/components/MkContainer.vue @@ -1,5 +1,6 @@ - diff --git a/packages/client/src/components/MkFollowButton.vue b/packages/client/src/components/MkFollowButton.vue index ffe1de72af..0920001c7b 100644 --- a/packages/client/src/components/MkFollowButton.vue +++ b/packages/client/src/components/MkFollowButton.vue @@ -8,7 +8,7 @@