chore(workspace): tighten quality gates (#2230)

This commit is contained in:
Hampus
2026-08-31 04:43:20 +02:00
committed by GitHub
parent e6e4c6f7b5
commit 5c2dca1c51
123 changed files with 4925 additions and 5640 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ ARG USER_GID=1000
ARG NODE_MAJOR=24
ARG ELP_VERSION=2026-02-27
ARG PNPM_VERSION=10.29.3
ARG WASM_BINDGEN_VERSION=0.2.122
ARG WASM_BINDGEN_VERSION=0.2.123
ENV DEBIAN_FRONTEND=noninteractive
+10 -4
View File
@@ -7,10 +7,16 @@ QUICK=0
SKIP_INSTALL=0
for arg in "$@"; do
case "$arg" in
--quick) QUICK=1 ;;
--skip-install) SKIP_INSTALL=1 ;;
-h|--help) sed -n '2,25p' "$0"; exit 0 ;;
*) echo "unknown argument: $arg" >&2; exit 2 ;;
--quick) QUICK=1 ;;
--skip-install) SKIP_INSTALL=1 ;;
-h | --help)
sed -n '2,25p' "$0"
exit 0
;;
*)
echo "unknown argument: $arg" >&2
exit 2
;;
esac
done
+35 -12
View File
@@ -153,7 +153,7 @@ jobs:
rust:
runs-on: ubuntu-24.04
timeout-minutes: 30
timeout-minutes: 60
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
@@ -178,20 +178,40 @@ jobs:
with:
workspaces: |
. -> target
fluxer_desktop/native/rust -> target
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install cargo-deny
run: cargo install cargo-deny --version 0.19.6 --locked
- name: Check Rust dependencies
run: cargo deny --locked check -D warnings
- name: Check desktop native dependencies
run: tools/ci/check-desktop-native-workspaces.sh dependencies
- name: Install native dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
pkg-config \
build-essential \
libcurl4-openssl-dev \
libvips-dev \
binutils \
clang \
cmake \
libavfilter-dev \
libclang-dev \
libcurl4-openssl-dev \
libfido2-dev \
libheif-dev \
libwebp-dev
libpipewire-0.3-dev \
libspa-0.2-dev \
libssl-dev \
libudev-dev \
libvips-dev \
libwebp-dev \
meson \
ninja-build \
pkg-config \
zlib1g-dev
- name: Install Node.js dependencies
run: pnpm --filter fluxer_admin install
@@ -199,17 +219,20 @@ jobs:
- name: Check formatting
run: cargo fmt --all -- --check
- name: Check formatting (desktop native)
run: cargo fmt --manifest-path fluxer_desktop/native/rust/Cargo.toml --all -- --check
- name: Check formatting (desktop native workspaces)
run: tools/ci/check-desktop-native-workspaces.sh fmt
- name: Clippy (warnings as errors)
run: cargo clippy --workspace -- -D warnings
run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
- name: Clippy (desktop native workspaces on Linux, warnings as errors)
run: tools/ci/check-desktop-native-workspaces.sh clippy
- name: Run tests
run: cargo test --workspace
run: cargo test --workspace --all-features --locked
- name: Run desktop native tests
run: cargo test --manifest-path fluxer_desktop/native/rust/Cargo.toml
- name: Run desktop native workspace tests on Linux
run: tools/ci/check-desktop-native-workspaces.sh test
gateway:
runs-on: ubuntu-24.04
Generated
+78 -367
View File
@@ -49,14 +49,13 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "ammonia"
version = "4.1.2"
version = "4.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17e913097e1a2124b46746c980134e8c954bc17a6a59bb3fde96f088d126dde6"
checksum = "dc6d763210e2eb7670d1a5183a08bebefa3f97db2a738a684f2ce00bd49f681d"
dependencies = [
"cssparser 0.35.0",
"html5ever 0.35.0",
"cssparser",
"html5ever",
"maplit",
"tendril 0.4.3",
"url",
]
@@ -127,9 +126,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.102"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "arc-swap"
@@ -182,7 +181,7 @@ dependencies = [
"ring",
"rustls-native-certs",
"rustls-pki-types",
"rustls-webpki 0.103.13",
"rustls-webpki",
"serde",
"serde_json",
"serde_nanos",
@@ -190,7 +189,7 @@ dependencies = [
"thiserror",
"time",
"tokio",
"tokio-rustls 0.26.4",
"tokio-rustls",
"tokio-stream",
"tokio-util",
"tokio-websockets",
@@ -528,23 +527,17 @@ dependencies = [
"aws-smithy-async",
"aws-smithy-runtime-api",
"aws-smithy-types",
"h2 0.3.27",
"h2 0.4.14",
"http 0.2.12",
"h2",
"http 1.4.2",
"http-body 0.4.6",
"hyper 0.14.32",
"hyper 1.10.1",
"hyper-rustls 0.24.2",
"hyper-rustls 0.27.9",
"hyper",
"hyper-rustls",
"hyper-util",
"pin-project-lite",
"rustls 0.21.12",
"rustls 0.23.40",
"rustls",
"rustls-native-certs",
"rustls-pki-types",
"tokio",
"tokio-rustls 0.26.4",
"tokio-rustls",
"tower",
"tracing",
]
@@ -709,7 +702,7 @@ dependencies = [
"http 1.4.2",
"http-body 1.0.1",
"http-body-util",
"hyper 1.10.1",
"hyper",
"hyper-util",
"itoa",
"matchit",
@@ -933,9 +926,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chacha20"
version = "0.10.0"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
@@ -1218,9 +1211,9 @@ dependencies = [
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
version = "0.9.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
dependencies = [
"crossbeam-utils",
]
@@ -1268,42 +1261,19 @@ dependencies = [
"hybrid-array",
]
[[package]]
name = "cssparser"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e901edd733a1472f944a45116df3f846f54d37e67e68640ac8bb69689aca2aa"
dependencies = [
"cssparser-macros 0.6.1",
"dtoa-short",
"itoa",
"phf 0.11.3",
"smallvec",
]
[[package]]
name = "cssparser"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98"
dependencies = [
"cssparser-macros 0.7.0",
"cssparser-macros",
"dtoa-short",
"itoa",
"phf 0.13.1",
"phf",
"smallvec",
]
[[package]]
name = "cssparser-macros"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "cssparser-macros"
version = "0.7.0"
@@ -1652,7 +1622,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -1779,7 +1749,7 @@ dependencies = [
"clap",
"fluxer_common",
"hmac 0.13.0",
"hyper 1.10.1",
"hyper",
"hyper-util",
"image",
"libc",
@@ -1923,8 +1893,7 @@ dependencies = [
"libc",
"moka",
"rmp-serde",
"rustls 0.23.40",
"rustls-pemfile",
"rustls",
"scylla",
"serde",
"serde_json",
@@ -2101,16 +2070,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "futf"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
dependencies = [
"mac",
"new_debug_unreachable",
]
[[package]]
name = "futures"
version = "0.3.32"
@@ -2283,28 +2242,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.3.27"
version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d"
dependencies = [
"bytes",
"fnv",
"futures-core",
"futures-sink",
"futures-util",
"http 0.2.12",
"indexmap",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "h2"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733"
checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16"
dependencies = [
"atomic-waker",
"bytes",
@@ -2400,17 +2340,6 @@ dependencies = [
"digest 0.11.3",
]
[[package]]
name = "html5ever"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4"
dependencies = [
"log",
"markup5ever 0.35.0",
"match_token",
]
[[package]]
name = "html5ever"
version = "0.39.0"
@@ -2418,7 +2347,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8"
dependencies = [
"log",
"markup5ever 0.39.0",
"markup5ever",
]
[[package]]
@@ -2497,30 +2426,6 @@ dependencies = [
"typenum",
]
[[package]]
name = "hyper"
version = "0.14.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
dependencies = [
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"h2 0.3.27",
"http 0.2.12",
"http-body 0.4.6",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"socket2 0.5.10",
"tokio",
"tower-service",
"tracing",
"want",
]
[[package]]
name = "hyper"
version = "1.10.1"
@@ -2531,7 +2436,7 @@ dependencies = [
"bytes",
"futures-channel",
"futures-core",
"h2 0.4.14",
"h2",
"http 1.4.2",
"http-body 1.0.1",
"httparse",
@@ -2543,21 +2448,6 @@ dependencies = [
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [
"futures-util",
"http 0.2.12",
"hyper 0.14.32",
"log",
"rustls 0.21.12",
"tokio",
"tokio-rustls 0.24.1",
]
[[package]]
name = "hyper-rustls"
version = "0.27.9"
@@ -2565,12 +2455,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
dependencies = [
"http 1.4.2",
"hyper 1.10.1",
"hyper",
"hyper-util",
"rustls 0.23.40",
"rustls",
"rustls-native-certs",
"tokio",
"tokio-rustls 0.26.4",
"tokio-rustls",
"tower-service",
]
@@ -2586,12 +2476,12 @@ dependencies = [
"futures-util",
"http 1.4.2",
"http-body 1.0.1",
"hyper 1.10.1",
"hyper",
"ipnet",
"libc",
"percent-encoding",
"pin-project-lite",
"socket2 0.6.3",
"socket2 0.5.10",
"tokio",
"tower-service",
"tracing",
@@ -2985,29 +2875,12 @@ dependencies = [
"twox-hash",
]
[[package]]
name = "mac"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "maplit"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
[[package]]
name = "markup5ever"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3"
dependencies = [
"log",
"tendril 0.4.3",
"web_atoms 0.1.3",
]
[[package]]
name = "markup5ever"
version = "0.39.0"
@@ -3015,19 +2888,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de"
dependencies = [
"log",
"tendril 0.5.0",
"web_atoms 0.2.4",
]
[[package]]
name = "match_token"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf"
dependencies = [
"proc-macro2",
"quote",
"syn",
"tendril",
"web_atoms",
]
[[package]]
@@ -3192,7 +3054,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -3363,55 +3225,25 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "phf"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
dependencies = [
"phf_macros 0.11.3",
"phf_shared 0.11.3",
]
[[package]]
name = "phf"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
dependencies = [
"phf_macros 0.13.1",
"phf_shared 0.13.1",
"phf_macros",
"phf_shared",
"serde",
]
[[package]]
name = "phf_codegen"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
dependencies = [
"phf_generator 0.11.3",
"phf_shared 0.11.3",
]
[[package]]
name = "phf_codegen"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1"
dependencies = [
"phf_generator 0.13.1",
"phf_shared 0.13.1",
]
[[package]]
name = "phf_generator"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [
"phf_shared 0.11.3",
"rand 0.8.6",
"phf_generator",
"phf_shared",
]
[[package]]
@@ -3421,20 +3253,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
dependencies = [
"fastrand",
"phf_shared 0.13.1",
]
[[package]]
name = "phf_macros"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
dependencies = [
"phf_generator 0.11.3",
"phf_shared 0.11.3",
"proc-macro2",
"quote",
"syn",
"phf_shared",
]
[[package]]
@@ -3443,22 +3262,13 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
dependencies = [
"phf_generator 0.13.1",
"phf_shared 0.13.1",
"phf_generator",
"phf_shared",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "phf_shared"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
dependencies = [
"siphasher",
]
[[package]]
name = "phf_shared"
version = "0.13.1"
@@ -3759,8 +3569,8 @@ dependencies = [
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls 0.23.40",
"socket2 0.6.3",
"rustls",
"socket2 0.5.10",
"thiserror",
"tokio",
"tracing",
@@ -3780,7 +3590,7 @@ dependencies = [
"rand 0.9.4",
"ring",
"rustc-hash",
"rustls 0.23.40",
"rustls",
"rustls-pki-types",
"slab",
"thiserror",
@@ -3798,7 +3608,7 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2 0.6.3",
"socket2 0.5.10",
"tracing",
"windows-sys 0.59.0",
]
@@ -4016,15 +3826,15 @@ dependencies = [
"http 1.4.2",
"http-body 1.0.1",
"http-body-util",
"hyper 1.10.1",
"hyper-rustls 0.27.9",
"hyper",
"hyper-rustls",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls 0.23.40",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
"serde",
@@ -4032,7 +3842,7 @@ dependencies = [
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tokio-rustls 0.26.4",
"tokio-rustls",
"tokio-util",
"tower",
"tower-http",
@@ -4069,7 +3879,7 @@ dependencies = [
"futures",
"getrandom 0.2.17",
"http 1.4.2",
"hyper 1.10.1",
"hyper",
"reqwest",
"reqwest-middleware",
"retry-policies",
@@ -4156,19 +3966,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.21.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
dependencies = [
"log",
"ring",
"rustls-webpki 0.101.7",
"sct",
"windows-sys 0.52.0",
]
[[package]]
@@ -4182,7 +3980,7 @@ dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki 0.103.13",
"rustls-webpki",
"subtle",
"zeroize",
]
@@ -4199,15 +3997,6 @@ dependencies = [
"security-framework",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.14.1"
@@ -4229,14 +4018,14 @@ dependencies = [
"jni",
"log",
"once_cell",
"rustls 0.23.40",
"rustls",
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki 0.103.13",
"rustls-webpki",
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -4245,16 +4034,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.101.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "rustls-webpki"
version = "0.103.13"
@@ -4347,23 +4126,13 @@ version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdd0be4d296f048bfb06dd01bbc80ef789ddd2e55583e8d2e6b804942abfabc2"
dependencies = [
"cssparser 0.37.0",
"cssparser",
"ego-tree",
"getopts",
"html5ever 0.39.0",
"html5ever",
"precomputed-hash",
"selectors",
"tendril 0.5.0",
]
[[package]]
name = "sct"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
dependencies = [
"ring",
"untrusted",
"tendril",
]
[[package]]
@@ -4467,12 +4236,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8adfa1c298912827b8a28b223b3b874357397ae706e6190acd9bf28cee99114d"
dependencies = [
"bitflags",
"cssparser 0.37.0",
"cssparser",
"derive_more",
"log",
"new_debug_unreachable",
"phf 0.13.1",
"phf_codegen 0.13.1",
"phf",
"phf_codegen",
"precomputed-hash",
"rustc-hash",
"servo_arc",
@@ -4766,9 +4535,9 @@ dependencies = [
[[package]]
name = "spin"
version = "0.10.0"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3"
[[package]]
name = "spki"
@@ -4786,19 +4555,6 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "string_cache"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f"
dependencies = [
"new_debug_unreachable",
"parking_lot",
"phf_shared 0.11.3",
"precomputed-hash",
"serde",
]
[[package]]
name = "string_cache"
version = "0.9.0"
@@ -4807,30 +4563,18 @@ checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901"
dependencies = [
"new_debug_unreachable",
"parking_lot",
"phf_shared 0.13.1",
"phf_shared",
"precomputed-hash",
]
[[package]]
name = "string_cache_codegen"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0"
dependencies = [
"phf_generator 0.11.3",
"phf_shared 0.11.3",
"proc-macro2",
"quote",
]
[[package]]
name = "string_cache_codegen"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69"
dependencies = [
"phf_generator 0.13.1",
"phf_shared 0.13.1",
"phf_generator",
"phf_shared",
"proc-macro2",
"quote",
]
@@ -4905,18 +4649,7 @@ dependencies = [
"getrandom 0.4.2",
"once_cell",
"rustix",
"windows-sys 0.61.2",
]
[[package]]
name = "tendril"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
dependencies = [
"futf",
"mac",
"utf-8",
"windows-sys 0.52.0",
]
[[package]]
@@ -5087,7 +4820,7 @@ dependencies = [
"log",
"parking_lot",
"percent-encoding",
"phf 0.13.1",
"phf",
"pin-project-lite",
"postgres-protocol",
"postgres-types",
@@ -5104,32 +4837,22 @@ version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c2ad44aa0ae96db89c4742212ed41645b2f597311ff6e1945542a4d9fadc2fb"
dependencies = [
"rustls 0.23.40",
"rustls",
"rustls-native-certs",
"sha2 0.11.0",
"tokio",
"tokio-postgres",
"tokio-rustls 0.26.4",
"tokio-rustls",
"x509-cert",
]
[[package]]
name = "tokio-rustls"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
dependencies = [
"rustls 0.21.12",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
"rustls 0.23.40",
"rustls",
"tokio",
]
@@ -5173,7 +4896,7 @@ dependencies = [
"ring",
"rustls-pki-types",
"tokio",
"tokio-rustls 0.26.4",
"tokio-rustls",
"tokio-util",
"webpki-roots 0.26.11",
]
@@ -5714,28 +5437,16 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "web_atoms"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414"
dependencies = [
"phf 0.11.3",
"phf_codegen 0.11.3",
"string_cache 0.8.9",
"string_cache_codegen 0.5.4",
]
[[package]]
name = "web_atoms"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538"
dependencies = [
"phf 0.13.1",
"phf_codegen 0.13.1",
"string_cache 0.9.0",
"string_cache_codegen 0.6.1",
"phf",
"phf_codegen",
"string_cache",
"string_cache_codegen",
]
[[package]]
@@ -5806,7 +5517,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
+35 -14
View File
@@ -1,13 +1,9 @@
# cargo-deny configuration for the Fluxer workspace.
#
# Applies to the root workspace (Cargo.toml at the repo root) AND to every
# per-addon crate under fluxer_desktop/native/* (each addon has its own
# [workspace], so we invoke cargo-deny with --config pointing here).
#
# Used by the native desktop security gate in CI.
# Applies to the root workspace (Cargo.toml at the repo root).
[graph]
all-features = false
all-features = true
no-default-features = false
[output]
@@ -44,13 +40,11 @@ allow = [
"BSD-3-Clause",
"ISC",
"MPL-2.0",
"Unicode-DFS-2016",
"Unicode-3.0",
"Zlib",
"CC0-1.0",
"AGPL-3.0-or-later",
"BSL-1.0",
"OpenSSL",
"CDLA-Permissive-2.0",
]
# Explicitly deny GPL-only / strong-copyleft licenses that don't compose with
@@ -72,21 +66,48 @@ license-files = [
[bans]
multiple-versions = "warn"
wildcards = "deny"
# Per-addon crates path-depend on ../rust (the shared `fluxer_desktop_native`
# crate) without a version. cargo-deny flags that as a wildcard; we allow it
# because path deps can't realistically pin a SemVer range, and this only
# affects intra-repo workspace links (registry wildcards remain denied).
# Internal workspace crates use path dependencies without registry versions.
# Registry wildcards remain denied.
allow-wildcard-paths = true
highlight = "all"
workspace-default-features = "allow"
external-default-features = "allow"
# Keep desktop packaging and native addons away from the obsolete libfuse2 stack.
# Keep workspace artifacts away from the obsolete libfuse2 stack.
# AppImage packaging must use the static electron-builder runtime instead.
deny = [
{ crate = "fuse", reason = "libfuse2-based Rust wrapper; use a maintained FUSE3-native crate only if Fluxer ever needs FUSE directly" },
{ crate = "fuse-sys", reason = "libfuse2 FFI crate; Fluxer AppImages must not reintroduce libfuse2 through native Rust dependencies" },
]
skip = []
skip = [
{ crate = "block-buffer@0.10.4", reason = "transitive dependency requires the older digest API" },
{ crate = "const-oid@0.9.6", reason = "transitive dependency requires the older digest API" },
{ crate = "cpufeatures@0.2.17", reason = "transitive dependency requires the older crypto API" },
{ crate = "crypto-common@0.1.7", reason = "transitive dependency requires the older digest API" },
{ crate = "digest@0.10.7", reason = "transitive dependency requires the older digest API" },
{ crate = "foldhash@0.1.5", reason = "transitive dependency requires the older hashbrown API" },
{ crate = "getrandom@0.2.17", reason = "transitive dependency requires the older randomness API" },
{ crate = "getrandom@0.3.4", reason = "transitive dependency requires the prior randomness API" },
{ crate = "hashbrown@0.14.5", reason = "transitive dependency requires the older hashbrown API" },
{ crate = "hashbrown@0.15.5", reason = "transitive dependency requires the older hashbrown API" },
{ crate = "hashbrown@0.16.1", reason = "transitive dependency requires the prior hashbrown API" },
{ crate = "hmac@0.12.1", reason = "transitive dependency requires the older digest API" },
{ crate = "http@0.2.12", reason = "transitive dependency requires the older HTTP API" },
{ crate = "http-body@0.4.6", reason = "transitive dependency requires the older HTTP body API" },
{ crate = "r-efi@5.3.0", reason = "transitive dependency requires the older WASI API" },
{ crate = "rand@0.8.6", reason = "transitive dependency requires the older randomness API" },
{ crate = "rand@0.9.4", reason = "transitive dependency requires the prior randomness API" },
{ crate = "rand_chacha@0.3.1", reason = "transitive dependency requires the older randomness API" },
{ crate = "rand_chacha@0.9.0", reason = "transitive dependency requires the prior randomness API" },
{ crate = "rand_core@0.6.4", reason = "transitive dependency requires the older randomness API" },
{ crate = "rand_core@0.9.5", reason = "transitive dependency requires the prior randomness API" },
{ crate = "sha1@0.10.6", reason = "transitive dependency requires the older digest API" },
{ crate = "sha2@0.10.9", reason = "transitive dependency requires the older digest API" },
{ crate = "socket2@0.5.10", reason = "transitive dependency requires the older socket API" },
{ crate = "wasi@0.11.1+wasi-snapshot-preview1", reason = "transitive dependency requires the legacy WASI API" },
{ crate = "windows-sys@0.52.0", reason = "transitive dependency requires the older Windows API" },
{ crate = "windows-sys@0.59.0", reason = "transitive dependency requires the prior Windows API" },
{ crate = "wit-bindgen@0.51.0", reason = "transitive dependency requires the older WASI binding API" },
]
skip-tree = []
# ---------------------------------------------------------------------------
+1 -1
View File
@@ -6,7 +6,7 @@ license.workspace = true
build = "build.rs"
[dependencies]
anyhow = "1.0.102"
anyhow = "1.0.104"
axum = { version = "0.8.9", features = ["macros"] }
base64 = "0.22.1"
chrono = { version = "0.4", default-features = false, features = ["serde"] }
@@ -40,8 +40,7 @@ export class DMPermissionValidator {
if (isBugHunterBotUser(senderUser)) {
return;
}
const isShadowbannedSpammer =
!senderUser.isBot && (senderUser.flags & UserFlags.SPAMMER) === UserFlags.SPAMMER;
const isShadowbannedSpammer = !senderUser.isBot && (senderUser.flags & UserFlags.SPAMMER) === UserFlags.SPAMMER;
const [senderBlockedTarget, targetBlockedSender, areFriends, targetSettings, senderSettings] = await Promise.all([
this.deps.userRepository.getRelationship(senderId, recipientId, RelationshipTypes.BLOCKED),
this.deps.userRepository.getRelationship(recipientId, senderId, RelationshipTypes.BLOCKED),
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
import {beforeAll, beforeEach, describe, expect, it} from 'vitest';
import {Logger} from '@fluxer/logger/src/Logger';
import {beforeAll, beforeEach, describe, expect, it, vi} from 'vitest';
import {createTestAccount} from '../../auth/tests/AuthTestUtils';
import type {MediaProxyMetadataRequest} from '../../infrastructure/IMediaService';
import {setInjectedMediaService} from '../../middleware/ServiceRegistry';
@@ -71,6 +72,7 @@ describe('Attachment Upload Validation', () => {
const guild = await createGuild(harness, account.token, 'Storage Unavailable Test Guild');
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
const channelId = guild.system_channel_id ?? channel.id;
const errorLoggerSpy = vi.spyOn(Logger.prototype, 'error').mockImplementation(() => {});
harness.storageService.configure({shouldFailUpload: true});
try {
const payload = {
@@ -83,8 +85,25 @@ describe('Attachment Upload Validation', () => {
const body = JSON.parse(text) as {code?: string};
expect(response.status).toBe(HTTP_STATUS.SERVICE_UNAVAILABLE);
expect(body.code).toBe(APIErrorCodes.SERVICE_UNAVAILABLE);
expect(errorLoggerSpy).toHaveBeenCalledTimes(1);
expect(errorLoggerSpy).toHaveBeenCalledWith(
{
err: expect.objectContaining({
cause: expect.objectContaining({message: 'Mock storage upload failure'}),
code: APIErrorCodes.SERVICE_UNAVAILABLE,
message: 'Attachment storage is temporarily unavailable',
status: HTTP_STATUS.SERVICE_UNAVAILABLE,
}),
method: 'POST',
path: `/channels/${channelId}/messages`,
requestId: expect.any(String),
status: HTTP_STATUS.SERVICE_UNAVAILABLE,
},
'Request failed',
);
} finally {
harness.storageService.configure({shouldFailUpload: false});
errorLoggerSpy.mockRestore();
}
});
});
@@ -160,12 +179,9 @@ describe('Attachment Upload Validation', () => {
content: 'Filename mismatch test',
attachments: [{id: 0, filename: 'expected.txt'}],
};
const {response, text, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
{index: 0, filename: 'different.txt', data: fileData},
]);
if (response.status !== 200) {
console.log('Error response:', text);
}
expect(response.status).toBe(200);
expect(json.attachments).toBeDefined();
expect(json.attachments).not.toBeNull();
@@ -798,8 +798,7 @@ suite('PostgresKvQueryExecutor differential', () => {
const corpus = new Map<string, Array<Row>>();
const shapeCache = new Map<string, Array<Shape>>();
const unstableShapes: Array<string> = [];
const reproducibleOrders: Array<string> = [];
const traffic = {legacy: 0, next: 0, shapes: 0, improved: 0};
const traffic = {legacy: 0, next: 0, shapes: 0};
const keysetOrderDeltas: Array<string> = [];
const legacyThrowDeltas: Array<string> = [];
@@ -906,7 +905,6 @@ suite('PostgresKvQueryExecutor differential', () => {
executor: LegacyPostgresKvQueryExecutor,
tableName: string,
shape: Shape,
candidate: string,
): Promise<boolean> {
const observed = new Set<string>();
for (const ordering of HEAP_ORDERINGS) {
@@ -914,7 +912,6 @@ suite('PostgresKvQueryExecutor differential', () => {
}
await restoreOne(kv, tableName, HEAP_ORDERINGS[0]!);
if (observed.size === 1) return false;
if (observed.has(candidate)) reproducibleOrders.push(`${tableName} ${shape.name}`);
return true;
}
@@ -950,7 +947,6 @@ suite('PostgresKvQueryExecutor differential', () => {
traffic.legacy += left.rowsRead;
traffic.next += right.rowsRead;
traffic.shapes += 1;
if (right.rowsRead < left.rowsRead) traffic.improved += 1;
if (left.fingerprint === right.fingerprint) {
if (right.rowsRead > left.rowsRead) {
mismatches.push({
@@ -976,7 +972,7 @@ suite('PostgresKvQueryExecutor differential', () => {
});
return;
}
if (!(await legacyIsOrderUnstable(kv, executor, tableName, shape, right.fingerprint))) {
if (!(await legacyIsOrderUnstable(kv, executor, tableName, shape))) {
mismatches.push({
kind: sameMultiset ? 'order' : 'rowset',
table: tableName,
@@ -1159,7 +1155,6 @@ suite('PostgresKvQueryExecutor differential', () => {
}
}
expect(shapeCount).toBeGreaterThan(3000);
console.log(`select sweep: tables=${specs.length} shapes=${shapeCount}`);
expect(mismatches.length, report(mismatches)).toBe(0);
}, 1_800_000);
@@ -1210,7 +1205,6 @@ suite('PostgresKvQueryExecutor differential', () => {
traffic.legacy += left.rowsRead;
traffic.next += right.rowsRead;
traffic.shapes += 1;
if (right.rowsRead < left.rowsRead) traffic.improved += 1;
if (right.rowsRead > left.rowsRead) {
mismatches.push({
kind: 'rowsread',
@@ -1309,7 +1303,7 @@ suite('PostgresKvQueryExecutor differential', () => {
keysetOrderDeltas.push(`${spec.name} ${label}`);
continue;
}
if (await legacyIsOrderUnstable(NEXT_TABLE, readLegacy, spec.name, shape, fingerprint(rightAll))) {
if (await legacyIsOrderUnstable(NEXT_TABLE, readLegacy, spec.name, shape)) {
unstableShapes.push(`${spec.name} ${label}`);
continue;
}
@@ -1454,14 +1448,5 @@ suite('PostgresKvQueryExecutor differential', () => {
}
expect(traffic.shapes).toBeGreaterThan(3000);
expect(traffic.next).toBeLessThan(traffic.legacy);
console.log(
`row payload traffic: shapes=${traffic.shapes} legacyRows=${traffic.legacy} nextRows=${traffic.next} reduction=${(100 - (traffic.next / traffic.legacy) * 100).toFixed(1)}% shapesImproved=${traffic.improved}`,
);
console.log(
`accepted deltas: order-unstable=${unstableShapes.length} (legacy-reproducible=${reproducibleOrders.length}) keyset-order=${keysetOrderDeltas.length} legacy-throw=${legacyThrowDeltas.length}`,
);
console.log(`order-unstable sample: ${unstableShapes.slice(0, 5).join(' | ')}`);
console.log(`keyset-order sample: ${keysetOrderDeltas.slice(0, 5).join(' | ')}`);
console.log(`legacy-throw: ${[...new Set(legacyThrowDeltas.map((entry) => entry.split(': ')[1]))].join(' | ')}`);
});
});
@@ -210,9 +210,9 @@ suite('postgres kv upgrade safety', () => {
if (pages > 200) throw new Error('runaway paging loop');
}
} catch (error) {
return {keys, error: (error as Error).message, pages};
return {keys, error: (error as Error).message};
}
return {keys, error: null, pages};
return {keys, error: null};
}
it('pages consistently with only the old image running', async () => {
@@ -232,27 +232,18 @@ suite('postgres kv upgrade safety', () => {
it('ROLLING RESTART: an old replica consumes a token minted by the new one', async () => {
const first = await next.executePagedQuery(usersScan(), {pageSize: 7, pageState: null});
expect(first.pageState).toBeTruthy();
const token = JSON.parse(Buffer.from(first.pageState!, 'base64url').toString('utf8'));
let failure: string | null = null;
let rows = 0;
try {
const second = await legacy.executePagedQuery(usersScan(), {pageSize: 7, pageState: first.pageState});
rows = second.rows.length;
} catch (error) {
failure = (error as Error).message;
}
console.log(`NEW->OLD token=${JSON.stringify(token)} legacy=${failure ?? `${rows} rows`}`);
expect(failure).toBeNull();
const second = await legacy.executePagedQuery(usersScan(), {pageSize: 7, pageState: first.pageState});
expect(second.rows).toHaveLength(7);
});
it('ROLLING RESTART: alternating old and new replicas drain the scan', async () => {
const a = await drain(['next', 'legacy'], 7);
const b = await drain(['legacy', 'next'], 7);
console.log(`new-first err=${a.error} pages=${a.pages} total=${a.keys.length} unique=${new Set(a.keys).size}`);
console.log(`old-first err=${b.error} pages=${b.pages} total=${b.keys.length} unique=${new Set(b.keys).size}`);
expect(a.error).toBeNull();
expect(a.keys).toHaveLength(40);
expect(new Set(a.keys).size).toBe(40);
expect(b.error).toBeNull();
expect(b.keys).toHaveLength(40);
expect(new Set(b.keys).size).toBe(40);
});
@@ -262,9 +253,9 @@ suite('postgres kv upgrade safety', () => {
const dual = Buffer.from(JSON.stringify({offset: first.rows.length, after: cursor.after})).toString('base64url');
const viaOld = await legacy.executePagedQuery(usersScan(), {pageSize: 7, pageState: dual});
const viaNew = await next.executePagedQuery(usersScan(), {pageSize: 7, pageState: dual});
console.log(`dual token -> old replica: ${viaOld.rows.length} rows, new replica: ${viaNew.rows.length} rows`);
expect(viaOld.rows.length).toBe(7);
expect(viaNew.rows.length).toBe(7);
expect(viaOld.rows).toHaveLength(7);
expect(viaNew.rows).toHaveLength(7);
expect(fp(viaNew.rows)).toBe(fp(viaOld.rows));
});
it('deletes exactly the same rows as the old image', async () => {
@@ -282,8 +273,7 @@ suite('postgres kv upgrade safety', () => {
const left = await raw.query<{row_key: string}>(
`SELECT row_key FROM ${KV} WHERE table_name = 'read_states' AND row_key LIKE '%400%' ORDER BY row_key`,
);
console.log(`rows left after legacy delete + new delete: ${left.rows.length}`);
expect(left.rows.length).toBe(0);
expect(left.rows).toHaveLength(0);
});
it('writes byte-identical row_key and partition_key to the old image', async () => {
@@ -333,8 +323,6 @@ suite('postgres kv upgrade safety', () => {
const cIndexes = await raw.query<{tablename: string}>(
`SELECT tablename FROM pg_indexes WHERE indexname IN ('${OLD}_row_key_c_idx', '${NEW}_row_key_c_idx')`,
);
console.log(`key diffs=${diff.rows[0]!.n} index-definition diffs=${schemaDiff.rows[0]!.n}`);
console.log(collations.rows.map((r) => `${r.tablename}.${r.attname}=${r.collname}`).join(' '));
expect(Number(diff.rows[0]!.n)).toBe(0);
expect(Number(schemaDiff.rows[0]!.n)).toBe(0);
expect(collations.rows.filter((r) => r.tablename === OLD).map((r) => r.collname)).toEqual(['default', 'default']);
@@ -358,7 +346,6 @@ suite('postgres kv upgrade safety', () => {
const indexes = await raw.query<{indexname: string}>(
`SELECT indexname FROM pg_indexes WHERE tablename = '${OLD}' ORDER BY indexname`,
);
console.log(`after boot on a legacy table: ${indexes.rows.map((r) => r.indexname).join(', ')}`);
expect(collations.rows.map((r) => r.collname)).toEqual(['default', 'default']);
expect(indexes.rows.map((r) => r.indexname)).toContain(`${OLD}_row_key_c_idx`);
});
@@ -371,10 +358,11 @@ suite('postgres kv upgrade safety', () => {
ensurePostgresKvSchema(new TableClient(raw, BOOT)),
ensurePostgresKvSchema(new TableClient(raw, BOOT)),
]);
const failures = results.filter((r) => r.status === 'rejected');
console.log(`concurrent boots rejected: ${failures.length}`);
for (const f of failures) console.log(` ${(f as PromiseRejectedResult).reason}`);
expect(failures.length).toBe(0);
expect(results).toEqual([
{status: 'fulfilled', value: undefined},
{status: 'fulfilled', value: undefined},
{status: 'fulfilled', value: undefined},
]);
}, 120_000);
it('backfills the messages partition key once and never scans for it again', async () => {
@@ -416,7 +404,6 @@ suite('postgres kv upgrade safety', () => {
await insertLegacy('m2');
await ensurePostgresKvSchema(backfillClient);
const skipped = await partitionOf('m2');
console.log(`second boot left the new legacy row alone: partition_key=${skipped === legacyKey('m2')}`);
expect(skipped).toBe(legacyKey('m2'));
expect(await partitionOf('m1')).toBe(`"c"${SEP}"b"`);
await raw.query(`DROP TABLE IF EXISTS ${BACKFILL}`);
@@ -432,7 +419,6 @@ suite('postgres kv upgrade safety', () => {
);
const after = await next.executeQuery(q);
await raw.query(`UPDATE pg_index SET indisvalid = true WHERE indexrelid = '${KV}_row_key_c_idx'::regclass`);
console.log(`index valid after boot: ${check.rows[0]?.indisvalid}`);
expect(String(after.map((r) => Object.values(r).map(String).join(',')))).toBe(
String(before.map((r) => Object.values(r).map(String).join(','))),
);
@@ -441,7 +427,6 @@ suite('postgres kv upgrade safety', () => {
describe('multi-range plan shape on a populated upgraded database', () => {
const PERF = 'kv_ranges_perf';
const SEP = String.fromCharCode(31);
const BASE = 1456074443984486400n;
let perf: IPostgresClient;
@@ -481,6 +466,7 @@ FROM generate_series($1::bigint, $1::bigint + 19999) u, generate_series(1, 3) s`
`SELECT kv.row_key, kv.row_data FROM ${PERF} kv WHERE kv.table_name = $1 AND (kv.expires_at IS NULL OR kv.expires_at > now())`,
['push_subscriptions'],
);
expect(Number.isFinite(scan.ms)).toBe(true);
const deltas: Array<string> = [];
for (const size of [5, 10, 25, 50, 100]) {
const params = {user_ids: Array.from({length: size}, (_, i) => BASE + BigInt(i * 3))};
@@ -494,9 +480,10 @@ FROM generate_series($1::bigint, $1::bigint + 19999) u, generate_series(1, 3) s`
`SELECT kv.row_key, kv.row_data FROM ${PERF} kv WHERE kv.table_name = $1${fragments.predicate} AND (kv.expires_at IS NULL OR kv.expires_at > now())`,
['push_subscriptions', ...fragments.params],
);
const verdict = `n=${size} pushdown=${pushed.ms.toFixed(2)}ms scan=${scan.ms.toFixed(2)}ms`;
console.log(verdict);
if (pushed.ms > scan.ms) deltas.push(`${verdict}\n${pushed.plan}`);
expect(Number.isFinite(pushed.ms)).toBe(true);
if (pushed.ms > scan.ms) {
deltas.push(`n=${size} pushdown=${pushed.ms.toFixed(2)}ms scan=${scan.ms.toFixed(2)}ms\n${pushed.plan}`);
}
}
expect(deltas.join('\n')).toBe('');
const overCap = buildCandidatePlan(meta, {
@@ -506,17 +493,13 @@ FROM generate_series($1::bigint, $1::bigint + 19999) u, generate_series(1, 3) s`
expect(overCap.exact).toBe(true);
const overCapGroups = planFragmentGroups(overCap.candidates);
expect(overCapGroups.map((group) => group.params.length)).toEqual([512, 512, 376]);
let chunkedMs = 0;
for (const fragments of overCapGroups) {
const pushed = await explain(
`SELECT kv.row_key, kv.row_data FROM ${PERF} kv WHERE kv.table_name = $1${fragments.predicate} AND (kv.expires_at IS NULL OR kv.expires_at > now())`,
['push_subscriptions', ...fragments.params],
);
expect(pushed.plan).not.toContain('Seq Scan');
chunkedMs += pushed.ms;
}
console.log(`n=700 chunked=${chunkedMs.toFixed(2)}ms scan=${scan.ms.toFixed(2)}ms`);
expect(SEP).toBe('\u001f');
}, 300_000);
});
});
@@ -547,8 +530,11 @@ describe('candidate plan shapes reached by real queries', () => {
: plan.candidates.kind === 'rowKeys'
? plan.candidates.rowKeys.length
: 0;
console.log(`${c.name} -> kind=${plan.candidates.kind} n=${size} exact=${plan.exact}`);
expect({kind: plan.candidates.kind, size, exact: plan.exact}, c.name).toEqual({
kind: 'ranges',
size: 100,
exact: true,
});
}
expect(true).toBe(true);
});
});
@@ -859,9 +859,7 @@ export class GuildOperationsService {
const channels = await this.channelRepository.listGuildChannels(guildId);
await Promise.all(channels.map((channel) => this.channelRepository.deleteAllChannelMessages(channel.id)));
await Promise.all(
channels.map((channel) =>
deleteChannelMessageSearchDocuments(channel.id, {context: {source: 'guild_delete'}}),
),
channels.map((channel) => deleteChannelMessageSearchDocuments(channel.id, {context: {source: 'guild_delete'}})),
);
await Promise.all(channels.map((channel) => this.channelService.attachments.purgeChannelAttachments(channel)));
const discoveryRow = await this.discoveryRepository.findByGuildId(guildId);
@@ -171,7 +171,6 @@ export async function stripNonJpegImageMetadataForUpload(
return {body: await image.gif().toBuffer(), contentType: 'image/gif'};
case 'webp':
return {body: await image.webp({lossless: true}).toBuffer(), contentType: 'image/webp'};
case 'avif':
case 'heif':
return {body: await image.avif().toBuffer(), contentType: 'image/avif'};
case 'tiff':
@@ -460,7 +459,6 @@ async function stripNonJpegImageFileToFile(
case 'webp':
await image.webp({lossless: true}).toFile(outputPath);
return {contentType: 'image/webp', ...dimensions};
case 'avif':
case 'heif':
await image.avif().toFile(outputPath);
return {contentType: 'image/avif', ...dimensions};
@@ -1,8 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {AppErrorHandler} from '@fluxer/errors/src/domains/core/ErrorHandlers';
import {ServiceUnavailableError} from '@fluxer/errors/src/domains/core/ServiceUnavailableError';
import {Logger} from '@fluxer/logger/src/Logger';
import {Hono} from 'hono';
import {describe, expect, test} from 'vitest';
import {describe, expect, test, vi} from 'vitest';
import type {HonoEnv} from '../../types/HonoEnv';
import {ConcurrencyLimitMiddleware} from '../ConcurrencyLimitMiddleware';
@@ -22,6 +24,7 @@ function createDeferred(): Deferred {
function buildApp(maxInflightRequests: number) {
const release = createDeferred();
const entered = createDeferred();
const boomError = new Error('boom');
const routes = new Hono<HonoEnv>({strict: true});
routes.use(ConcurrencyLimitMiddleware({maxInflightRequests}));
routes.get('/_health', (ctx) => ctx.text('OK'));
@@ -33,65 +36,132 @@ function buildApp(maxInflightRequests: number) {
return ctx.text('slow');
});
routes.get('/boom', () => {
throw new Error('boom');
throw boomError;
});
routes.onError(AppErrorHandler);
const app = new Hono<HonoEnv>({strict: true});
app.route('/v1', routes);
app.route('/', routes);
app.onError(AppErrorHandler);
return {app, release, entered};
return {app, release, entered, boomError};
}
describe('ConcurrencyLimitMiddleware', () => {
test('sheds with 503 once the in-flight ceiling is reached', async () => {
const {app, release, entered} = buildApp(1);
const errorLoggerSpy = vi.spyOn(Logger.prototype, 'error').mockImplementation(() => {});
const inflight = app.request('/slow');
await entered.promise;
try {
await entered.promise;
const shed = await app.request('/fast');
const shed = await app.request('/fast');
expect(shed.status).toBe(503);
expect(shed.headers.get('Retry-After')).toBe('1');
expect(await shed.json()).toMatchObject({code: 'SERVICE_UNAVAILABLE'});
expect(shed.status).toBe(503);
expect(shed.headers.get('Retry-After')).toBe('1');
expect(await shed.json()).toMatchObject({code: 'SERVICE_UNAVAILABLE'});
expect(errorLoggerSpy).toHaveBeenCalledTimes(1);
expect(errorLoggerSpy).toHaveBeenCalledWith(
{
err: expect.any(ServiceUnavailableError),
method: 'GET',
path: '/fast',
requestId: undefined,
status: 503,
},
'Request failed',
);
release.resolve();
expect((await inflight).status).toBe(200);
release.resolve();
expect((await inflight).status).toBe(200);
} finally {
release.resolve();
errorLoggerSpy.mockRestore();
}
});
test('admits probe paths while shedding everything else', async () => {
const {app, release, entered} = buildApp(1);
const errorLoggerSpy = vi.spyOn(Logger.prototype, 'error').mockImplementation(() => {});
const inflight = app.request('/slow');
await entered.promise;
try {
await entered.promise;
expect((await app.request('/_health')).status).toBe(200);
expect((await app.request('/_metrics')).status).toBe(200);
expect((await app.request('/v1/_health')).status).toBe(200);
expect((await app.request('/fast')).status).toBe(503);
expect((await app.request('/_health')).status).toBe(200);
expect((await app.request('/_metrics')).status).toBe(200);
expect((await app.request('/v1/_health')).status).toBe(200);
expect((await app.request('/fast')).status).toBe(503);
expect(errorLoggerSpy).toHaveBeenCalledTimes(1);
expect(errorLoggerSpy).toHaveBeenCalledWith(
{
err: expect.any(ServiceUnavailableError),
method: 'GET',
path: '/fast',
requestId: undefined,
status: 503,
},
'Request failed',
);
release.resolve();
await inflight;
release.resolve();
await inflight;
} finally {
release.resolve();
errorLoggerSpy.mockRestore();
}
});
test('releases the slot when the handler throws', async () => {
const {app} = buildApp(1);
for (let attempt = 0; attempt < 3; attempt += 1) {
expect((await app.request('/boom')).status).toBe(500);
const {app, boomError} = buildApp(1);
const errorLoggerSpy = vi.spyOn(Logger.prototype, 'error').mockImplementation(() => {});
try {
for (let attempt = 0; attempt < 3; attempt += 1) {
expect((await app.request('/boom')).status).toBe(500);
}
expect((await app.request('/fast')).status).toBe(200);
expect(errorLoggerSpy).toHaveBeenCalledTimes(3);
for (let callIndex = 1; callIndex <= 3; callIndex += 1) {
expect(errorLoggerSpy).toHaveBeenNthCalledWith(
callIndex,
{err: boomError, method: 'GET', path: '/boom', requestId: undefined, status: 500},
'Unhandled error occurred',
);
}
} finally {
errorLoggerSpy.mockRestore();
}
expect((await app.request('/fast')).status).toBe(200);
});
test('releases the slot taken by a shed request', async () => {
const {app, release, entered} = buildApp(1);
const errorLoggerSpy = vi.spyOn(Logger.prototype, 'error').mockImplementation(() => {});
const inflight = app.request('/slow');
await entered.promise;
try {
await entered.promise;
expect((await app.request('/fast')).status).toBe(503);
expect((await app.request('/fast')).status).toBe(503);
expect((await app.request('/fast')).status).toBe(503);
expect((await app.request('/fast')).status).toBe(503);
expect(errorLoggerSpy).toHaveBeenCalledTimes(2);
for (let callIndex = 1; callIndex <= 2; callIndex += 1) {
expect(errorLoggerSpy).toHaveBeenNthCalledWith(
callIndex,
{
err: expect.any(ServiceUnavailableError),
method: 'GET',
path: '/fast',
requestId: undefined,
status: 503,
},
'Request failed',
);
}
release.resolve();
await inflight;
release.resolve();
await inflight;
expect((await app.request('/fast')).status).toBe(200);
expect((await app.request('/fast')).status).toBe(200);
} finally {
release.resolve();
errorLoggerSpy.mockRestore();
}
});
});
@@ -1,8 +1,9 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {Logger} from '@fluxer/logger/src/Logger';
import {SnowflakeType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
import {Hono} from 'hono';
import {afterAll, beforeEach, describe, expect, test} from 'vitest';
import {afterAll, beforeEach, describe, expect, test, vi} from 'vitest';
import {z} from 'zod';
import {Config} from '../../Config';
import type {HonoEnv} from '../../types/HonoEnv';
@@ -52,10 +53,26 @@ describe('ResponseTypeMiddleware', () => {
test('rejects mismatching responses while validation is enabled', async () => {
const app = new Hono<HonoEnv>();
app.get('/snowflake', ResponseType(SnowflakeResponse), (ctx) => ctx.json({id: 'not-a-snowflake'}));
const errorLoggerSpy = vi.spyOn(Logger.prototype, 'error').mockImplementation(() => {});
const response = await app.request('/snowflake');
try {
const response = await app.request('/snowflake');
expect(response.status).toBe(500);
expect(response.status).toBe(500);
expect(errorLoggerSpy).toHaveBeenCalledTimes(1);
expect(errorLoggerSpy).toHaveBeenCalledWith(
{
body: {id: 'not-a-snowflake'},
method: 'GET',
path: '/snowflake',
status: 200,
validationErrors: [{message: 'INVALID_SNOWFLAKE_FORMAT', path: 'id'}],
},
'Response validation failed',
);
} finally {
errorLoggerSpy.mockRestore();
}
});
test('passes the response through untouched while validation is disabled', async () => {
+1 -2
View File
@@ -9,8 +9,7 @@ export function channelRequiresAgeVerification(
channelsById: ReadonlyMap<string, Channel>,
guildNsfw: boolean,
): boolean {
const parentCategory =
channel.parentId != null ? (channelsById.get(channel.parentId.toString()) ?? null) : null;
const parentCategory = channel.parentId != null ? (channelsById.get(channel.parentId.toString()) ?? null) : null;
return computeEffectiveChannelNsfw(
channelToContentWarningView(channel),
parentCategory ? channelToContentWarningView(parentCategory) : null,
@@ -49,6 +49,20 @@ describe('Stripe Webhook Refund', () => {
.body(payload)
.execute();
}
function useRefundListHandler(chargeId: string): void {
server.use(
http.get(
({request}) => request.url === `https://api.stripe.com/v1/refunds?charge=${chargeId}&limit=100`,
() =>
HttpResponse.json({
object: 'list',
data: [],
has_more: false,
url: '/v1/refunds',
}),
),
);
}
describe('charge.refunded', () => {
test('revokes premium and records first refund', async () => {
const account = await createTestAccount(harness);
@@ -73,6 +87,7 @@ describe('Stripe Webhook Refund', () => {
payment_intent_id: paymentIntentId,
completed_at: new Date(),
});
useRefundListHandler('ch_test_refund_123');
await sendWebhook({
type: 'charge.refunded',
data: {
@@ -121,6 +136,7 @@ describe('Stripe Webhook Refund', () => {
payment_intent_id: paymentIntentId,
completed_at: new Date(),
});
useRefundListHandler('ch_test_refund_456');
await sendWebhook({
type: 'charge.refunded',
data: {
@@ -150,6 +166,7 @@ describe('Stripe Webhook Refund', () => {
{stripe_customer_id: stripeCustomerId},
(await userRepository.findUnique(userId))!.toRow(),
);
useRefundListHandler('ch_test_refund_sub_789');
await sendWebhook({
type: 'charge.refunded',
data: {
@@ -182,6 +199,7 @@ describe('Stripe Webhook Refund', () => {
subscriptionCurrentPeriodEnd: null,
subscriptionCancelAt: null,
});
useRefundListHandler('ch_test_refund_donation_101');
const result = await sendWebhook({
type: 'charge.refunded',
data: {
@@ -234,6 +252,7 @@ describe('Stripe Webhook Refund', () => {
premium_until: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000).toISOString(),
})
.execute();
useRefundListHandler('ch_test_refund_gift_multi_123');
await sendWebhook({
type: 'charge.refunded',
data: {
@@ -287,6 +306,7 @@ describe('Stripe Webhook Refund', () => {
.get('/users/@me')
.execute();
expect(redeemerBefore.premium_type).toBe(UserPremiumTypes.SUBSCRIPTION);
useRefundListHandler('ch_test_refund_stripe_cus_only');
await sendWebhook({
type: 'charge.refunded',
data: {
@@ -325,6 +345,7 @@ describe('Stripe Webhook Refund', () => {
premium_type: UserPremiumTypes.LIFETIME,
})
.execute();
useRefundListHandler('ch_test_refund_gift_override_123');
await sendWebhook({
type: 'charge.refunded',
data: {
@@ -119,12 +119,8 @@ export class UserRelationshipService {
requestCache: RequestCache;
}): Promise<Relationship> {
const requesterUser = await this.userRepository.findUnique(userId);
const requesterIsStaff =
requesterUser != null && (requesterUser.flags & UserFlags.STAFF) === UserFlags.STAFF;
if (
!requesterIsStaff &&
(await getInstanceConfigRepository().getInstancePolicyConfig()).direct_messages_disabled
) {
const requesterIsStaff = requesterUser != null && (requesterUser.flags & UserFlags.STAFF) === UserFlags.STAFF;
if (!requesterIsStaff && (await getInstanceConfigRepository().getInstancePolicyConfig()).direct_messages_disabled) {
throw new DirectMessagesDisabledError();
}
if (staffForceAccept && requesterIsStaff) {
+1 -1
View File
@@ -267,7 +267,7 @@
"@typescript/native-preview": "catalog:",
"autoprefixer": "catalog:",
"esbuild": "catalog:",
"happy-dom": "20.7.0",
"happy-dom": "20.12.0",
"postcss": "catalog:",
"postcss-discard-comments": "catalog:",
"postcss-loader": "catalog:",
+2 -2
View File
@@ -4,9 +4,9 @@ version = 4
[[package]]
name = "anyhow"
version = "1.0.102"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "autocfg"
+2 -2
View File
@@ -2,7 +2,7 @@
import {mkdirSync, readFileSync, writeFileSync} from 'node:fs';
import {join} from 'node:path';
import sharp from 'sharp';
import sharp, {type OverlayOptions} from 'sharp';
import {convertToCodePoints} from '../src/features/expressions/utils/EmojiCodepointUtils';
const EMOJI_SPRITES = {
@@ -131,7 +131,7 @@ async function renderSpriteSheet(
const size = EMOJI_SIZE * scale;
const dstW = perRow * size;
const dstH = rows * size;
const compositeOps: Array<sharp.OverlayOptions> = [];
const compositeOps: Array<OverlayOptions> = [];
for (let i = 0; i < emojiEntries.length; i++) {
const item = emojiEntries[i];
const emojiBuffer = await loadEmojiImage(item.surrogates, size);
@@ -9,6 +9,7 @@ const APP_DIR = fileURLToPath(new URL('..', import.meta.url));
const SOURCE_DIR = join(APP_DIR, 'src');
const JSX_EXTENSIONS = new Set(['.tsx', '.jsx']);
const SKIP_DIRS = new Set(['node_modules', 'dist', 'coverage', '.cache', '.swc']);
const GENERATED_FILES = new Set([join(SOURCE_DIR, 'features', 'ui', 'components', 'SVGMasks.tsx')]);
const NON_THEMEABLE_IDENTIFIERS = new Set([
'Fragment',
'I18nProvider',
@@ -153,7 +154,9 @@ function collectFiles(paths) {
files.push(absolute);
}
}
return Array.from(new Set(files)).sort();
return Array.from(new Set(files))
.filter((file) => !GENERATED_FILES.has(file))
.sort();
}
function parseSource(source, filePath) {
+1 -3
View File
@@ -15,9 +15,7 @@
'Segoe UI Symbol', 'Segoe UI Historic', 'Apple Symbols',
'Noto Sans Symbols', 'Noto Sans Symbols 2', 'Noto Sans Math', 'Cambria Math';
--font-fallback-mono:
'Noto Sans Mono',
'Fluxer Sans SC', 'Fluxer Sans TC', 'Fluxer Sans JP', 'Fluxer Sans KR',
var(--font-fallback-sans);
'Noto Sans Mono', 'Fluxer Sans SC', 'Fluxer Sans TC', 'Fluxer Sans JP', 'Fluxer Sans KR', var(--font-fallback-sans);
--font-sans:
'Fluxer Sans', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari', 'Fluxer Sans Thai Looped',
@@ -6,7 +6,8 @@
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0980-0983, U+0985-098C, U+098F-0990, U+0993-09A8, U+09AA-09B0, U+09B2, U+09B6-09B9, U+09BC-09C4, U+09C7-09C8, U+09CB-09CE, U+09D7, U+09DC-09DD, U+09DF-09E3, U+09E6-09FE;
unicode-range:
U+0980-0983, U+0985-098C, U+098F-0990, U+0993-09A8, U+09AA-09B0, U+09B2, U+09B6-09B9, U+09BC-09C4, U+09C7-09C8, U+09CB-09CE, U+09D7, U+09DC-09DD, U+09DF-09E3, U+09E6-09FE;
}
@font-face {
font-family: 'Fluxer Fallback';
@@ -14,7 +15,8 @@
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0B82-0B83, U+0B85-0B8A, U+0B8E-0B90, U+0B92-0B95, U+0B99-0B9A, U+0B9C, U+0B9E-0B9F, U+0BA3-0BA4, U+0BA8-0BAA, U+0BAE-0BB9, U+0BBE-0BC2, U+0BC6-0BC8, U+0BCA-0BCD, U+0BD0, U+0BD7, U+0BE6-0BFA;
unicode-range:
U+0B82-0B83, U+0B85-0B8A, U+0B8E-0B90, U+0B92-0B95, U+0B99-0B9A, U+0B9C, U+0B9E-0B9F, U+0BA3-0BA4, U+0BA8-0BAA, U+0BAE-0BB9, U+0BBE-0BC2, U+0BC6-0BC8, U+0BCA-0BCD, U+0BD0, U+0BD7, U+0BE6-0BFA;
}
@font-face {
font-family: 'Fluxer Fallback';
@@ -22,7 +24,8 @@
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0C00-0C0C, U+0C0E-0C10, U+0C12-0C28, U+0C2A-0C39, U+0C3C-0C44, U+0C46-0C48, U+0C4A-0C4D, U+0C55-0C56, U+0C58-0C5A, U+0C5D, U+0C60-0C63, U+0C66-0C6F, U+0C77-0C7F;
unicode-range:
U+0C00-0C0C, U+0C0E-0C10, U+0C12-0C28, U+0C2A-0C39, U+0C3C-0C44, U+0C46-0C48, U+0C4A-0C4D, U+0C55-0C56, U+0C58-0C5A, U+0C5D, U+0C60-0C63, U+0C66-0C6F, U+0C77-0C7F;
}
@font-face {
font-family: 'Fluxer Fallback';
@@ -30,7 +33,8 @@
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0C80-0C8C, U+0C8E-0C90, U+0C92-0CA8, U+0CAA-0CB3, U+0CB5-0CB9, U+0CBC-0CC4, U+0CC6-0CC8, U+0CCA-0CCD, U+0CD5-0CD6, U+0CDD-0CDE, U+0CE0-0CE3, U+0CE6-0CEF, U+0CF1-0CF3;
unicode-range:
U+0C80-0C8C, U+0C8E-0C90, U+0C92-0CA8, U+0CAA-0CB3, U+0CB5-0CB9, U+0CBC-0CC4, U+0CC6-0CC8, U+0CCA-0CCD, U+0CD5-0CD6, U+0CDD-0CDE, U+0CE0-0CE3, U+0CE6-0CEF, U+0CF1-0CF3;
}
@font-face {
font-family: 'Fluxer Fallback';
@@ -46,7 +50,8 @@
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0A81-0A83, U+0A85-0A8D, U+0A8F-0A91, U+0A93-0AA8, U+0AAA-0AB0, U+0AB2-0AB3, U+0AB5-0AB9, U+0ABC-0AC5, U+0AC7-0AC9, U+0ACB-0ACD, U+0AD0, U+0AE0-0AE3, U+0AE6-0AF1, U+0AF9-0AFF;
unicode-range:
U+0A81-0A83, U+0A85-0A8D, U+0A8F-0A91, U+0A93-0AA8, U+0AAA-0AB0, U+0AB2-0AB3, U+0AB5-0AB9, U+0ABC-0AC5, U+0AC7-0AC9, U+0ACB-0ACD, U+0AD0, U+0AE0-0AE3, U+0AE6-0AF1, U+0AF9-0AFF;
}
@font-face {
font-family: 'Fluxer Fallback';
@@ -54,7 +59,8 @@
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0A01-0A03, U+0A05-0A0A, U+0A0F-0A10, U+0A13-0A28, U+0A2A-0A30, U+0A32-0A33, U+0A35-0A36, U+0A38-0A39, U+0A3C, U+0A3E-0A42, U+0A47-0A48, U+0A4B-0A4D, U+0A51, U+0A59-0A5C, U+0A5E, U+0A66-0A76;
unicode-range:
U+0A01-0A03, U+0A05-0A0A, U+0A0F-0A10, U+0A13-0A28, U+0A2A-0A30, U+0A32-0A33, U+0A35-0A36, U+0A38-0A39, U+0A3C, U+0A3E-0A42, U+0A47-0A48, U+0A4B-0A4D, U+0A51, U+0A59-0A5C, U+0A5E, U+0A66-0A76;
}
@font-face {
font-family: 'Fluxer Fallback';
@@ -62,7 +68,8 @@
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0B01-0B03, U+0B05-0B0C, U+0B0F-0B10, U+0B13-0B28, U+0B2A-0B30, U+0B32-0B33, U+0B35-0B39, U+0B3C-0B44, U+0B47-0B48, U+0B4B-0B4D, U+0B55-0B57, U+0B5C-0B5D, U+0B5F-0B63, U+0B66-0B77;
unicode-range:
U+0B01-0B03, U+0B05-0B0C, U+0B0F-0B10, U+0B13-0B28, U+0B2A-0B30, U+0B32-0B33, U+0B35-0B39, U+0B3C-0B44, U+0B47-0B48, U+0B4B-0B4D, U+0B55-0B57, U+0B5C-0B5D, U+0B5F-0B63, U+0B66-0B77;
}
@font-face {
font-family: 'Fluxer Fallback';
@@ -70,7 +77,8 @@
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0D81-0D83, U+0D85-0D96, U+0D9A-0DB1, U+0DB3-0DBB, U+0DBD, U+0DC0-0DC6, U+0DCA, U+0DCF-0DD4, U+0DD6, U+0DD8-0DDF, U+0DE6-0DEF, U+0DF2-0DF4, U+111E1-111F4;
unicode-range:
U+0D81-0D83, U+0D85-0D96, U+0D9A-0DB1, U+0DB3-0DBB, U+0DBD, U+0DC0-0DC6, U+0DCA, U+0DCF-0DD4, U+0DD6, U+0DD8-0DDF, U+0DE6-0DEF, U+0DF2-0DF4, U+111E1-111F4;
}
@font-face {
font-family: 'Fluxer Fallback';
@@ -86,7 +94,9 @@
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0E81-0E82, U+0E84, U+0E86-0E8A, U+0E8C-0EA3, U+0EA5, U+0EA7-0EBD, U+0EC0-0EC4, U+0EC6, U+0EC8-0ECE, U+0ED0-0ED9, U+0EDC-0EDF;
unicode-range:
U+0E81-0E82, U+0E84, U+0E86-0E8A, U+0E8C-0EA3, U+0EA5, U+0EA7-0EBD, U+0EC0-0EC4, U+0EC6, U+0EC8-0ECE, U+0ED0-0ED9,
U+0EDC-0EDF;
}
@font-face {
font-family: 'Fluxer Fallback';
@@ -118,7 +128,8 @@
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+1200-1248, U+124A-124D, U+1250-1256, U+1258, U+125A-125D, U+1260-1288, U+128A-128D, U+1290-12B0, U+12B2-12B5, U+12B8-12BE, U+12C0, U+12C2-12C5, U+12C8-12D6, U+12D8-1310, U+1312-1315, U+1318-135A, U+135D-137C, U+1380-1399, U+2D80-2D96, U+2DA0-2DA6, U+2DA8-2DAE, U+2DB0-2DB6, U+2DB8-2DBE, U+2DC0-2DC6, U+2DC8-2DCE, U+2DD0-2DD6, U+2DD8-2DDE, U+AB01-AB06, U+AB09-AB0E, U+AB11-AB16, U+AB20-AB26, U+AB28-AB2E, U+1E7E0-1E7E6, U+1E7E8-1E7EB, U+1E7ED-1E7EE, U+1E7F0-1E7FE;
unicode-range:
U+1200-1248, U+124A-124D, U+1250-1256, U+1258, U+125A-125D, U+1260-1288, U+128A-128D, U+1290-12B0, U+12B2-12B5, U+12B8-12BE, U+12C0, U+12C2-12C5, U+12C8-12D6, U+12D8-1310, U+1312-1315, U+1318-135A, U+135D-137C, U+1380-1399, U+2D80-2D96, U+2DA0-2DA6, U+2DA8-2DAE, U+2DB0-2DB6, U+2DB8-2DBE, U+2DC0-2DC6, U+2DC8-2DCE, U+2DD0-2DD6, U+2DD8-2DDE, U+AB01-AB06, U+AB09-AB0E, U+AB11-AB16, U+AB20-AB26, U+AB28-AB2E, U+1E7E0-1E7E6, U+1E7E8-1E7EB, U+1E7ED-1E7EE, U+1E7F0-1E7FE;
}
@font-face {
font-family: 'Fluxer Fallback';
@@ -198,7 +209,8 @@
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+2190-2199, U+2300-230F, U+2311-2315, U+2317, U+231C-231F, U+2322-2323, U+2329-232A, U+232C-2335, U+237C, U+2380-2394, U+2396-239A, U+23AF, U+23BE-23CD, U+23D0-23DB, U+23E2-23E8, U+2460-24FF, U+25CC, U+260A-260D, U+2613, U+2624-262F, U+2638-263B, U+263D-2653, U+2669-267E, U+2690-269D, U+26A2-26A9, U+26AD-26BC, U+26CE, U+26E2-26FF, U+271D-2721, U+2776-2793, U+1F100-1F10C, U+1F110-1F16C, U+1F170-1F190, U+1F19B-1F1AC, U+1F546-1F549, U+1F54F, U+1F610, U+1F700-1F773;
unicode-range:
U+2190-2199, U+2300-230F, U+2311-2315, U+2317, U+231C-231F, U+2322-2323, U+2329-232A, U+232C-2335, U+237C, U+2380-2394, U+2396-239A, U+23AF, U+23BE-23CD, U+23D0-23DB, U+23E2-23E8, U+2460-24FF, U+25CC, U+260A-260D, U+2613, U+2624-262F, U+2638-263B, U+263D-2653, U+2669-267E, U+2690-269D, U+26A2-26A9, U+26AD-26BC, U+26CE, U+26E2-26FF, U+271D-2721, U+2776-2793, U+1F100-1F10C, U+1F110-1F16C, U+1F170-1F190, U+1F19B-1F1AC, U+1F546-1F549, U+1F54F, U+1F610, U+1F700-1F773;
}
@font-face {
font-family: 'Fluxer Symbols';
@@ -206,7 +218,8 @@
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+21AF, U+21E6-21F0, U+21F3, U+2316, U+2318, U+231A-231B, U+2324-2328, U+232B, U+237B, U+237D-237F, U+23CE-23CF, U+23E9-23EA, U+23ED-23EF, U+23F1-2426, U+2440-244A, U+25A0-25CB, U+25CD-2609, U+260E-2612, U+2614-2623, U+2630-2637, U+263C, U+2654-2668, U+267F-268F, U+269E-26A1, U+26AA-26AC, U+26BD-26CD, U+26CF-26E1, U+2700-2704, U+2706-2709, U+270B-271C, U+2722-2727, U+2729-274B, U+274D, U+274F-2753, U+2756-2775, U+2794, U+2798-27AF, U+27B1-27BE, U+2800-28FF, U+2B00-2B0D, U+2B12-2B2F, U+2B4D-2B73, U+2B76-2B95, U+2B97-2BFD, U+2BFF, U+1F000-1F02B, U+1F030-1F093, U+1F0A0-1F0AE, U+1F0B1-1F0BF, U+1F0C1-1F0CF, U+1F0D1-1F0F5, U+1F10D-1F10F, U+1F16D-1F16F, U+1F1AD, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F321-1F32C, U+1F336, U+1F378, U+1F37D, U+1F393-1F39F, U+1F3A7, U+1F3AC-1F3AE, U+1F3C2, U+1F3C4, U+1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4DA, U+1F4DF, U+1F4E4-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50A, U+1F50D, U+1F512-1F513, U+1F53E-1F545, U+1F54A, U+1F550-1F579, U+1F57B-1F594, U+1F597-1F5A3, U+1F5A5-1F5FA, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CB, U+1F6CD-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F774-1F776, U+1F77B-1F7D9, U+1F7E0-1F7EB, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8B1, U+1F900-1F90B, U+1F93B, U+1F946, U+1FA00-1FA53, U+1FA60-1FA6D, U+1FA70-1FA74, U+1FA78-1FA7A, U+1FA80-1FA86, U+1FA90-1FAA8, U+1FAB0-1FAB6, U+1FAC0-1FAC2, U+1FAD0-1FAD6, U+1FB00-1FB92, U+1FB94-1FBCA, U+1FBF0-1FBF9;
unicode-range:
U+21AF, U+21E6-21F0, U+21F3, U+2316, U+2318, U+231A-231B, U+2324-2328, U+232B, U+237B, U+237D-237F, U+23CE-23CF, U+23E9-23EA, U+23ED-23EF, U+23F1-2426, U+2440-244A, U+25A0-25CB, U+25CD-2609, U+260E-2612, U+2614-2623, U+2630-2637, U+263C, U+2654-2668, U+267F-268F, U+269E-26A1, U+26AA-26AC, U+26BD-26CD, U+26CF-26E1, U+2700-2704, U+2706-2709, U+270B-271C, U+2722-2727, U+2729-274B, U+274D, U+274F-2753, U+2756-2775, U+2794, U+2798-27AF, U+27B1-27BE, U+2800-28FF, U+2B00-2B0D, U+2B12-2B2F, U+2B4D-2B73, U+2B76-2B95, U+2B97-2BFD, U+2BFF, U+1F000-1F02B, U+1F030-1F093, U+1F0A0-1F0AE, U+1F0B1-1F0BF, U+1F0C1-1F0CF, U+1F0D1-1F0F5, U+1F10D-1F10F, U+1F16D-1F16F, U+1F1AD, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F321-1F32C, U+1F336, U+1F378, U+1F37D, U+1F393-1F39F, U+1F3A7, U+1F3AC-1F3AE, U+1F3C2, U+1F3C4, U+1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4DA, U+1F4DF, U+1F4E4-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50A, U+1F50D, U+1F512-1F513, U+1F53E-1F545, U+1F54A, U+1F550-1F579, U+1F57B-1F594, U+1F597-1F5A3, U+1F5A5-1F5FA, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CB, U+1F6CD-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F774-1F776, U+1F77B-1F7D9, U+1F7E0-1F7EB, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8B1, U+1F900-1F90B, U+1F93B, U+1F946, U+1FA00-1FA53, U+1FA60-1FA6D, U+1FA70-1FA74, U+1FA78-1FA7A, U+1FA80-1FA86, U+1FA90-1FAA8, U+1FAB0-1FAB6, U+1FAC0-1FAC2, U+1FAD0-1FAD6, U+1FB00-1FB92, U+1FB94-1FBCA, U+1FBF0-1FBF9;
}
@font-face {
font-family: 'Fluxer Math';
@@ -214,5 +227,6 @@
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+2102, U+210A-2113, U+2115, U+2119-211D, U+2124, U+2128, U+212C-212D, U+212F-2131, U+2133-2138, U+213C-2140, U+2145-2149, U+2200-22FF, U+27C0-27FF, U+2900-2AFF, U+1D400-1D454, U+1D456-1D49C, U+1D49E-1D49F, U+1D4A2, U+1D4A5-1D4A6, U+1D4A9-1D4AC, U+1D4AE-1D4B9, U+1D4BB, U+1D4BD-1D4C3, U+1D4C5-1D505, U+1D507-1D50A, U+1D50D-1D514, U+1D516-1D51C, U+1D51E-1D539, U+1D53B-1D53E, U+1D540-1D544, U+1D546, U+1D54A-1D550, U+1D552-1D6A5, U+1D6A8-1D7CB, U+1D7CE-1D7FF;
unicode-range:
U+2102, U+210A-2113, U+2115, U+2119-211D, U+2124, U+2128, U+212C-212D, U+212F-2131, U+2133-2138, U+213C-2140, U+2145-2149, U+2200-22FF, U+27C0-27FF, U+2900-2AFF, U+1D400-1D454, U+1D456-1D49C, U+1D49E-1D49F, U+1D4A2, U+1D4A5-1D4A6, U+1D4A9-1D4AC, U+1D4AE-1D4B9, U+1D4BB, U+1D4BD-1D4C3, U+1D4C5-1D505, U+1D507-1D50A, U+1D50D-1D514, U+1D516-1D51C, U+1D51E-1D539, U+1D53B-1D53E, U+1D540-1D544, U+1D546, U+1D54A-1D550, U+1D552-1D6A5, U+1D6A8-1D7CB, U+1D7CE-1D7FF;
}
@@ -2,10 +2,44 @@
import {resolveRetryAfterMs} from '@app/features/messaging/utils/RetryAfterUtils';
import {HttpError} from '@app/features/platform/types/EndpointError';
import * as SlowmodeCommands from '@app/features/slowmode/commands/SlowmodeCommands';
import Slowmode from '@app/features/slowmode/state/Slowmode';
import {Logger} from '@app/features/platform/utils/AppLogger';
import type {PendingMessageSend} from '@app/features/slowmode/commands/SlowmodeCommands';
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest';
interface SlowmodeStore {
clearChannel(channelId: string): void;
getLastSendTimestamp(channelId: string): number | null;
getSlowmodeRemaining(channelId: string, rateLimitPerUser: number): number;
}
interface SlowmodeCommandModule {
clampSlowmodeRetryAfterMs(retryAfterMs: number | null): number;
confirmMessageSend(channelId: string, sentAt: string, pending?: PendingMessageSend): void;
discardPendingMessageSend(channelId: string, pending: PendingMessageSend): void;
recordPendingMessageSend(channelId: string): PendingMessageSend;
updateSlowmodeRemaining(channelId: string, retryAfterMs: number): void;
}
let Slowmode: SlowmodeStore;
let SlowmodeCommands: SlowmodeCommandModule;
beforeAll(async () => {
const debug = vi.spyOn(Logger.prototype, 'debug').mockImplementation(() => undefined);
try {
const [slowmodeModule, commandModule, persistenceModule] = await Promise.all([
import('@app/features/slowmode/state/Slowmode'),
import('@app/features/slowmode/commands/SlowmodeCommands'),
import('@app/features/platform/utils/MobXPersistence'),
]);
Slowmode = slowmodeModule.default;
SlowmodeCommands = commandModule;
await persistenceModule.awaitHydration('Slowmode');
expect(debug).toHaveBeenCalledExactlyOnceWith('Store Slowmode hydrated from AppStorage and is now persisting.');
} finally {
debug.mockRestore();
}
});
const CHANNEL_ID = '1234567890123456789';
const RATE_LIMIT_PER_USER = 5;
@@ -1,167 +0,0 @@
// @vitest-environment happy-dom
// SPDX-License-Identifier: AGPL-3.0-or-later
import {installVoiceMenuTestBootstrap} from '@app/features/ui/action_menu/items/__fixtures__/VoiceMenuTestBootstrap';
import type {VoiceParticipantMenuSource} from '@app/features/ui/action_menu/items/VoiceParticipantMenuTypes';
import type {User} from '@app/features/user/models/User';
import {act, createElement} from 'react';
import {createRoot, type Root} from 'react-dom/client';
import {afterEach, expect, test, vi} from 'vitest';
vi.mock('@lingui/core/macro', () => {
const descriptor = (value: unknown): unknown => (typeof value === 'string' ? {message: value} : value);
return {msg: descriptor, t: descriptor, plural: () => '', select: () => '', selectOrdinal: () => ''};
});
vi.mock('@lingui/react/macro', () => ({
Trans: () => null,
useLingui: () => ({i18n: {_: (descriptor: {message?: string}) => descriptor.message ?? '', locale: 'en'}}),
}));
installVoiceMenuTestBootstrap();
const {useVoiceParticipantMenuData} = await import('@app/features/ui/action_menu/items/VoiceParticipantMenuData');
const {default: EntranceSoundListenerPrefs} = await import('@app/features/voice/state/EntranceSoundListenerPrefs');
const {default: MediaEngine} = await import('@app/features/voice/engine/MediaEngineFacade');
const {observer} = await import('mobx-react-lite');
(globalThis as {IS_REACT_ACT_ENVIRONMENT?: boolean}).IS_REACT_ACT_ENVIRONMENT = true;
const TARGET_USER = {id: '111', username: 'target'} as unknown as User;
const PARTICIPANT_SOURCE: VoiceParticipantMenuSource = {kind: 'participant'};
const CAMERA_SOURCE: VoiceParticipantMenuSource = {kind: 'camera'};
const noop = (): void => undefined;
interface MenuLeaf {
label?: string;
checked?: boolean;
value?: number;
items?: Array<MenuLeaf>;
}
interface MenuGroup {
items: Array<MenuLeaf>;
}
const shadowedMediaEngineKeys: Array<string> = [];
let root: Root | null = null;
let container: HTMLDivElement | null = null;
function shadowMediaEngine(key: string, value: unknown): void {
Object.defineProperty(MediaEngine, key, {value, configurable: true, writable: true});
shadowedMediaEngineKeys.push(key);
}
function findLeaf(groups: Array<MenuGroup>, label: string): MenuLeaf | null {
for (const group of groups) {
for (const item of group.items) {
if (item.label === label) return item;
for (const child of item.items ?? []) {
if (child.label === label) return child;
}
}
}
return null;
}
async function renderMenu(
options: Parameters<typeof useVoiceParticipantMenuData>[0],
): Promise<{groups: () => Array<MenuGroup>}> {
const latest: {groups: Array<MenuGroup>} = {groups: []};
const Probe = observer(function Probe() {
latest.groups = useVoiceParticipantMenuData(options).groups as unknown as Array<MenuGroup>;
return null;
});
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
await act(async () => {
root?.render(createElement(Probe));
});
return {groups: () => latest.groups};
}
afterEach(async () => {
await act(async () => {
root?.unmount();
});
root = null;
container?.remove();
container = null;
for (const key of shadowedMediaEngineKeys.splice(0)) {
delete (MediaEngine as unknown as Record<string, unknown>)[key];
}
EntranceSoundListenerPrefs.reset(TARGET_USER.id);
});
test('entrance-sound checkbox tracks the pref while the menu stays open', async () => {
const menu = await renderMenu({
user: TARGET_USER,
surface: 'call-tile',
source: PARTICIPANT_SOURCE,
onClose: noop,
});
expect(findLeaf(menu.groups(), 'Mute entrance sound')?.checked).toBe(false);
await act(async () => {
EntranceSoundListenerPrefs.setMuted(TARGET_USER.id, true);
});
expect(findLeaf(menu.groups(), 'Mute entrance sound')?.checked).toBe(true);
await act(async () => {
EntranceSoundListenerPrefs.setVolume(TARGET_USER.id, 42);
});
expect(findLeaf(menu.groups(), 'Entrance sound volume')?.value).toBe(42);
});
test('disable video locally is offered for another user camera tile in the local room', async () => {
shadowMediaEngine('connected', true);
shadowMediaEngine('connectionId', 'self-connection');
shadowMediaEngine('channelId', 'channel-1');
shadowMediaEngine('getVoiceStateByConnectionId', (connectionId: string) =>
connectionId === 'target-connection' ? {channel_id: 'channel-1', user_id: TARGET_USER.id} : null,
);
const menu = await renderMenu({
user: TARGET_USER,
connectionId: 'target-connection',
surface: 'call-tile',
source: CAMERA_SOURCE,
onClose: noop,
});
expect(findLeaf(menu.groups(), 'Disable video locally')).not.toBeNull();
});
test('offers a single inline user-volume slider and no nested secondary submenu wrapper', async () => {
const REMOVED_SUBMENU_LABELS = [
'Media controls',
'Device controls',
'Display options',
'User actions',
'Relationship',
'Relationship actions',
'Moderation',
'Moderation actions',
'Advanced',
'Advanced actions',
];
shadowMediaEngine('connected', true);
shadowMediaEngine('connectionId', 'self-connection');
shadowMediaEngine('channelId', 'channel-1');
shadowMediaEngine('getVoiceStateByConnectionId', (connectionId: string) =>
connectionId === 'target-connection' ? {channel_id: 'channel-1', user_id: TARGET_USER.id} : null,
);
const menu = await renderMenu({
user: TARGET_USER,
connectionId: 'target-connection',
surface: 'participant-list',
source: PARTICIPANT_SOURCE,
onClose: noop,
});
const groups = menu.groups();
const topLevelItems = groups.flatMap((group) => group.items);
const topLevelSliders = topLevelItems.filter((item) => typeof item.value === 'number');
expect(topLevelSliders).toHaveLength(1);
expect(topLevelSliders[0]?.label).toBe('User volume');
const submenuLabels = topLevelItems.filter((item) => Array.isArray(item.items)).map((item) => item.label);
expect(submenuLabels.every((label) => label === 'Entrance sound')).toBe(true);
for (const label of REMOVED_SUBMENU_LABELS) {
expect(findLeaf(groups, label)).toBeNull();
}
});
@@ -1,116 +0,0 @@
// @vitest-environment happy-dom
// SPDX-License-Identifier: AGPL-3.0-or-later
import {installVoiceMenuTestBootstrap} from '@app/features/ui/action_menu/items/__fixtures__/VoiceMenuTestBootstrap';
import type {VoiceParticipantMenuScreenShareSource} from '@app/features/ui/action_menu/items/VoiceParticipantMenuTypes';
import type {I18n} from '@lingui/core';
import {expect, test, vi} from 'vitest';
vi.mock('@lingui/core/macro', () => {
const descriptor = (value: unknown): unknown => (typeof value === 'string' ? {message: value} : value);
return {msg: descriptor, t: descriptor, plural: () => '', select: () => '', selectOrdinal: () => ''};
});
vi.mock('@lingui/react/macro', () => ({
Trans: () => null,
useLingui: () => ({i18n: {_: (descriptor: {message?: string}) => descriptor.message ?? '', locale: 'en'}}),
}));
vi.mock('@app/features/voice/components/ActiveScreenShareMenu', () => ({
changeActiveScreenShare: vi.fn(async () => undefined),
stopActiveScreenShare: vi.fn(async () => undefined),
}));
vi.mock('@app/features/voice/components/modals/ScreenSharePickerModal', () => ({
openScreenSharePreviewPrivacyModal: vi.fn(),
}));
vi.mock('@app/features/voice/engine/MediaEngineFacade', () => ({
default: {
applyLocalAudioPreferencesForUser: vi.fn(),
getVoiceStateByConnectionId: () => null,
connectionId: null,
},
}));
vi.mock('@app/features/voice/state/PopoutWindowManager', () => ({
default: {openTilePopout: vi.fn()},
isVoicePopoutSupported: () => false,
}));
vi.mock('@app/features/voice/state/StreamAudioPrefs', () => ({
default: {setMuted: vi.fn(), setVolume: vi.fn()},
}));
vi.mock('@app/features/voice/state/VoiceSettings', () => ({
default: {showMyOwnScreenShare: false, pauseOwnScreenSharePreviewOnUnfocus: false},
}));
vi.mock('@app/features/voice/commands/VoiceSettingsCommands', () => ({
update: vi.fn(),
}));
installVoiceMenuTestBootstrap();
const {buildVoiceParticipantStreamMenu} = await import(
'@app/features/ui/action_menu/items/VoiceParticipantStreamMenuBuilder'
);
const i18n = {
locale: 'en',
_: (descriptor: {message?: string}) => descriptor.message ?? '',
} as unknown as I18n;
interface MenuLeaf {
label?: string;
items?: Array<MenuLeaf>;
}
function streamMenu(source: VoiceParticipantMenuScreenShareSource): Array<{items: Array<MenuLeaf>}> {
return buildVoiceParticipantStreamMenu({
i18n,
userId: '111',
channelId: null,
participantIdentity: 'user_111_conn',
displayName: 'Alice',
isCurrentUserConnectedToVoice: false,
source,
streamVolume: 100,
isStreamMuted: false,
showMyOwnScreenShare: false,
pauseOwnScreenSharePreviewOnUnfocus: false,
onClose: () => undefined,
}) as unknown as Array<{items: Array<MenuLeaf>}>;
}
function findLeaf(groups: Array<{items: Array<MenuLeaf>}>, label: string): MenuLeaf | null {
for (const group of groups) {
for (const item of group.items) {
if (item.label === label) return item;
for (const child of item.items ?? []) {
if (child.label === label) return child;
}
}
}
return null;
}
const OWN_STREAM_SOURCE: VoiceParticipantMenuScreenShareSource = {
kind: 'screen-share',
streamKey: 'stream-key',
state: {kind: 'own'},
};
const WATCHED_REMOTE_STREAM_SOURCE: VoiceParticipantMenuScreenShareSource = {
kind: 'screen-share',
streamKey: 'stream-key',
state: {kind: 'remote-watched', hasAudio: true, onStopWatching: () => undefined},
};
test('own stream keeps a More options submenu with the screen-share preferences', () => {
const groups = streamMenu(OWN_STREAM_SOURCE);
const moreOptions = findLeaf(groups, 'More options');
expect(moreOptions).not.toBeNull();
expect(findLeaf(groups, 'Show My Screen Share')).not.toBeNull();
expect(findLeaf(groups, 'Report Problem')).toBeNull();
});
test('remote watched stream omits the now-empty More options submenu and keeps audio controls', () => {
const groups = streamMenu(WATCHED_REMOTE_STREAM_SOURCE);
expect(findLeaf(groups, 'More options')).toBeNull();
expect(findLeaf(groups, 'Mute')).not.toBeNull();
expect(findLeaf(groups, 'Stream volume')).not.toBeNull();
expect(findLeaf(groups, 'Report Problem')).toBeNull();
});
@@ -1,71 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
const HARNESS_ENDPOINT = 'https://primary.test/api';
export function installVoiceMenuTestBootstrap(): void {
const host = globalThis as unknown as {window?: Record<string, unknown>};
if (typeof host.window === 'undefined') {
host.window = host as unknown as Record<string, unknown>;
}
host.window.__FLUXER_BOOTSTRAP__ = {
config: {
releaseChannel: 'stable',
bootstrapApiEndpoint: HARNESS_ENDPOINT,
bootstrapApiPublicEndpoint: HARNESS_ENDPOINT,
},
instance: {
api_code_version: Number.MAX_SAFE_INTEGER,
endpoints: {
api: HARNESS_ENDPOINT,
api_client: HARNESS_ENDPOINT,
api_public: HARNESS_ENDPOINT,
gateway: 'wss://gateway.primary.test',
media: 'https://media.primary.test',
static_cdn: 'https://cdn.primary.test',
marketing: 'https://primary.test',
admin: 'https://admin.primary.test',
invite: 'https://primary.test/invite',
gift: 'https://primary.test/gift',
webapp: 'https://app.primary.test',
upload_relay: 'https://upload.primary.test',
},
captcha: {provider: 'none', hcaptcha_site_key: null, turnstile_site_key: null},
features: {
voice_enabled: false,
stripe_enabled: false,
self_hosted: false,
presigned_attachment_uploads: false,
emails_enabled: false,
},
gif: {provider: 'klipy', display_name: 'Klipy', attribution_required: false},
sso: {enabled: false, enforced: false, display_name: null, redirect_uri: ''},
registration: {mode: 'open', admin_registration_urls_enabled: true},
community: {single_community: false, single_community_guild_id: null, direct_messages_disabled: false},
services: {gif_enabled: true, youtube_enabled: false, bluesky_enabled: false},
limits: undefined,
push: {public_vapid_key: null},
app_public: {
branding: {
product_name: 'Fluxer',
icon_url: null,
symbol_url: null,
logo_url: null,
wordmark_url: null,
favicon_url: null,
theme_color: null,
},
setup: {configured: true, admin_url: null},
legal: {terms_url: null, privacy_url: null},
registration: {collect_date_of_birth: true},
},
},
geoip: {
countryCode: null,
regionCode: null,
latitude: null,
longitude: null,
ageRestrictedGeos: [],
ageBlockedGeos: [],
},
};
}
+14 -5
View File
@@ -6,9 +6,11 @@ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
async function loadTick() {
vi.resetModules();
const {Logger} = await import('@app/features/platform/utils/AppLogger');
const debug = vi.spyOn(Logger.prototype, 'debug').mockImplementation(() => undefined);
const {default: Window} = await import('@app/features/window/state/Window');
const {default: Tick} = await import('@app/features/ui/state/Tick');
return {Tick, Window};
return {debug, Tick, Window};
}
describe('Tick', () => {
@@ -17,14 +19,16 @@ describe('Tick', () => {
});
afterEach(() => {
vi.restoreAllMocks();
vi.useRealTimers();
});
it('keeps counting while the window is visible but not focused', async () => {
const {Tick, Window} = await loadTick();
const {debug, Tick, Window} = await loadTick();
const dispose = autorun(() => Tick.nowSecond);
const started = Tick.nowSecond;
Window.setFocused(false);
expect(debug).toHaveBeenCalledExactlyOnceWith('Window focus changed: false');
vi.advanceTimersByTime(3000);
expect(Window.visible).toBe(true);
expect(Tick.nowSecond).toBe(started + 3);
@@ -32,9 +36,10 @@ describe('Tick', () => {
});
it('stops counting while the window is hidden', async () => {
const {Tick, Window} = await loadTick();
const {debug, Tick, Window} = await loadTick();
const dispose = autorun(() => Tick.nowSecond);
Window.setVisible(false);
expect(debug).toHaveBeenCalledExactlyOnceWith('Window visibility changed: false');
const stopped = Tick.nowSecond;
vi.advanceTimersByTime(5000);
expect(Tick.nowSecond).toBe(stopped);
@@ -42,24 +47,28 @@ describe('Tick', () => {
});
it('resynchronises to wall clock as soon as the window becomes visible again', async () => {
const {Tick, Window} = await loadTick();
const {debug, Tick, Window} = await loadTick();
const dispose = autorun(() => Tick.nowSecond);
Window.setVisible(false);
const stopped = Tick.nowSecond;
vi.advanceTimersByTime(60_000);
expect(Tick.nowSecond).toBe(stopped);
Window.setVisible(true);
expect(debug).toHaveBeenNthCalledWith(1, 'Window visibility changed: false');
expect(debug).toHaveBeenNthCalledWith(2, 'Window visibility changed: true');
expect(debug).toHaveBeenCalledTimes(2);
expect(Tick.nowSecond).toBe(stopped + 60);
dispose();
});
it('does not run an interval while nothing observes the clock', async () => {
const {Tick} = await loadTick();
const {debug, Tick} = await loadTick();
const idle = Tick.nowSecond;
vi.advanceTimersByTime(5000);
expect(Tick.nowSecond).toBe(idle);
const dispose = autorun(() => Tick.nowSecond);
expect(Tick.nowSecond).toBe(idle + 5);
expect(debug).not.toHaveBeenCalled();
dispose();
});
});
@@ -40,6 +40,7 @@ interface FakeGraphHarness {
interface FakeWindowHarness {
requestedOptions: Array<AudioContextOptions | undefined>;
contexts: Array<FakeAudioContext>;
constructionErrors: Array<DOMException>;
bodyListeners: Array<string>;
}
@@ -52,7 +53,7 @@ function installFakeWindow(
honourSampleRate?: boolean;
} = {},
): FakeWindowHarness {
const harness: FakeWindowHarness = {requestedOptions: [], contexts: [], bodyListeners: []};
const harness: FakeWindowHarness = {requestedOptions: [], contexts: [], constructionErrors: [], bodyListeners: []};
const throwOn = config.throwOn ?? ((): boolean => false);
const honourSampleRate = config.honourSampleRate ?? true;
@@ -64,7 +65,9 @@ function installFakeWindow(
constructor(options?: AudioContextOptions) {
harness.requestedOptions.push(options);
if (throwOn(options)) {
throw new DOMException('sample rate not supported', 'NotSupportedError');
const error = new DOMException('sample rate not supported', 'NotSupportedError');
harness.constructionErrors.push(error);
throw error;
}
this.sampleRate = (honourSampleRate ? options?.sampleRate : undefined) ?? HARDWARE_SAMPLE_RATE;
harness.contexts.push(this as unknown as FakeAudioContext);
@@ -219,15 +222,30 @@ describe('VoiceSharedAudioContext', () => {
it('returns null without throwing when the construction fails', async () => {
const harness = installFakeWindow({throwOn: () => true});
const {getSharedVoiceAudioContext} = await loadModule();
const warning = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
try {
let context: AudioContext | null | undefined;
expect(() => {
context = getSharedVoiceAudioContext();
}).not.toThrow();
let context: AudioContext | null | undefined;
expect(() => {
context = getSharedVoiceAudioContext();
}).not.toThrow();
expect(context).toBeNull();
expect(harness.contexts).toHaveLength(0);
expect(harness.requestedOptions).toHaveLength(1);
expect(context).toBeNull();
expect(harness.contexts).toHaveLength(0);
expect(harness.requestedOptions).toEqual([{latencyHint: 'interactive'}]);
expect(harness.constructionErrors).toHaveLength(1);
expect(warning).toHaveBeenCalledTimes(1);
expect(warning).toHaveBeenCalledWith(
expect.stringMatching(/^%c\[\d{2}:\d{2}:\d{2}\] \[VoiceSharedAudioContext\] \[Warn\]$/),
'color:#ffc107;font-weight:normal',
'Failed to construct voice AudioContext',
{
options: {latencyHint: 'interactive'},
error: harness.constructionErrors[0],
},
);
} finally {
warning.mockRestore();
}
});
it('caches the context across calls and rebuilds it once it has closed', async () => {
@@ -267,10 +285,25 @@ describe('VoiceSharedAudioContext', () => {
});
it('createVoiceAudioContext returns null instead of throwing when the rate is rejected', async () => {
installFakeWindow({throwOn: (options) => options?.sampleRate !== undefined});
const harness = installFakeWindow({throwOn: (options) => options?.sampleRate !== undefined});
const {createVoiceAudioContext} = await loadModule();
expect(createVoiceAudioContext({latencyHint: 'interactive', sampleRate: 48000})).toBeNull();
const warning = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
try {
expect(createVoiceAudioContext({latencyHint: 'interactive', sampleRate: 48000})).toBeNull();
expect(harness.constructionErrors).toHaveLength(1);
expect(warning).toHaveBeenCalledTimes(1);
expect(warning).toHaveBeenCalledWith(
expect.stringMatching(/^%c\[\d{2}:\d{2}:\d{2}\] \[VoiceSharedAudioContext\] \[Warn\]$/),
'color:#ffc107;font-weight:normal',
'Failed to construct voice AudioContext',
{
options: {latencyHint: 'interactive', sampleRate: 48000},
error: harness.constructionErrors[0],
},
);
} finally {
warning.mockRestore();
}
});
it('leaves the context untouched when the browser has no WaveShaper', async () => {
@@ -2,8 +2,9 @@
import type {GuildReadyData} from '@app/features/gateway/types/GatewayGuildTypes';
import type {VoiceState} from '@app/features/gateway/types/GatewayVoiceTypes';
import {Logger} from '@app/features/platform/utils/AppLogger';
import {autorun} from 'mobx';
import {describe, expect, it} from 'vitest';
import {describe, expect, it, vi} from 'vitest';
import {VoiceEngineV2AppVoiceStateAdapter} from './VoiceEngineV2AppVoiceStateAdapter';
function voiceState(overrides: Partial<VoiceState> = {}): VoiceState {
@@ -59,16 +60,27 @@ describe('VoiceEngineV2AppVoiceStateAdapter', () => {
});
it('does not reinsert locally removed stale connections', () => {
const adapter = new VoiceEngineV2AppVoiceStateAdapter();
adapter.handleGatewayVoiceStateUpdate('guild-1', voiceState({connection_id: 'stale-connection'}));
expect(adapter.getVoiceStateByConnectionId('stale-connection')).toBeDefined();
const debug = vi.spyOn(Logger.prototype, 'debug').mockImplementation(() => undefined);
try {
const adapter = new VoiceEngineV2AppVoiceStateAdapter();
adapter.handleGatewayVoiceStateUpdate('guild-1', voiceState({connection_id: 'stale-connection'}));
expect(adapter.getVoiceStateByConnectionId('stale-connection')).toBeDefined();
adapter.removeVoiceStateConnection('stale-connection');
adapter.handleGatewayVoiceStateUpdate('guild-1', voiceState({connection_id: 'stale-connection'}));
adapter.removeVoiceStateConnection('stale-connection');
adapter.handleGatewayVoiceStateUpdate('guild-1', voiceState({connection_id: 'stale-connection'}));
expect(adapter.isConnectionIgnored('stale-connection')).toBe(true);
expect(adapter.getVoiceStateByConnectionId('stale-connection')).toBeNull();
expect(adapter.getAllVoiceStatesInChannel('guild-1', 'channel-1')['stale-connection']).toBeUndefined();
expect(adapter.isConnectionIgnored('stale-connection')).toBe(true);
expect(adapter.getVoiceStateByConnectionId('stale-connection')).toBeNull();
expect(adapter.getAllVoiceStatesInChannel('guild-1', 'channel-1')['stale-connection']).toBeUndefined();
expect(debug).toHaveBeenCalledExactlyOnceWith('Ignored voice state update for locally removed connection', {
guildId: 'guild-1',
channelId: 'channel-1',
userId: 'user-1',
connectionId: 'stale-connection',
});
} finally {
debug.mockRestore();
}
});
it('publishes self-video true-to-false changes through MobX observation', () => {
@@ -28,7 +28,38 @@ AppStorage.subscribe(
{key: 'VoiceSettings'},
);
const {default: VoiceSettings} = await import('./VoiceSettings');
async function loadVoiceSettings() {
const [{Logger}, persistenceModule] = await Promise.all([
import('@app/features/platform/utils/AppLogger'),
import('@app/features/platform/utils/MobXPersistence'),
]);
const debug = vi.spyOn(Logger.prototype, 'debug').mockImplementation(() => undefined);
try {
const [{default: VoiceSettings}, {default: MediaPermission}] = await Promise.all([
import('./VoiceSettings'),
import('@app/features/permissions/system/state/MediaPermission'),
]);
await Promise.all([
persistenceModule.awaitHydration('MacPermissions'),
persistenceModule.awaitHydration('VoiceSettings'),
vi.waitFor(() => expect(MediaPermission.isInitialized()).toBe(true)),
]);
expect(debug).toHaveBeenCalledTimes(3);
expect(debug).toHaveBeenCalledWith('Store MacPermissions hydrated from AppStorage and is now persisting.');
expect(debug).toHaveBeenCalledWith('Initial permission state', {
microphone: 'granted',
camera: 'granted',
micDenied: false,
cameraDenied: false,
});
expect(debug).toHaveBeenCalledWith('Store VoiceSettings hydrated from AppStorage and is now persisting.');
return VoiceSettings;
} finally {
debug.mockRestore();
}
}
const VoiceSettings = await loadVoiceSettings();
describe('AV1/HEVC screen-share opt-in', () => {
it('rewrites a stored AV1 screen-share preference back to automatic on first launch', () => {
@@ -1,12 +1,14 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {Logger} from '@app/features/platform/utils/AppLogger';
import type {HardwareEncodeReport} from '@app/features/voice/utils/GpuEncoderCapabilities';
import {beforeEach, describe, expect, it, vi} from 'vitest';
import {afterEach, beforeEach, describe, expect, it, type MockInstance, vi} from 'vitest';
let av1OptIn = false;
let hevcOptIn = false;
let desktop = true;
let gpuReport: HardwareEncodeReport | null = null;
let codecCapabilityInfo: MockInstance<(...args: Array<unknown>) => void>;
vi.mock('@app/features/voice/state/VoiceSettings', () => ({
default: {
@@ -70,7 +72,22 @@ const ALL_SOFTWARE: HardwareEncodeReport = {
};
describe('screen-share AV1/HEVC opt-in gate', () => {
afterEach(() => {
try {
expect(codecCapabilityInfo.mock.calls.length).toBeGreaterThan(0);
for (const call of codecCapabilityInfo.mock.calls) {
expect(call).toEqual([
'Codec capabilities probed',
{capabilities: {vp8: true, vp9: true, h264: true, h265: true, av1: true}},
]);
}
} finally {
codecCapabilityInfo.mockRestore();
}
});
beforeEach(() => {
codecCapabilityInfo = vi.spyOn(Logger.prototype, 'info').mockImplementation(() => undefined);
av1OptIn = false;
hevcOptIn = false;
desktop = true;
+2 -1
View File
@@ -3,9 +3,10 @@ name = "fluxer_app_proxy"
version = "0.1.0"
edition.workspace = true
license.workspace = true
publish = false
[dependencies]
anyhow = "1.0.102"
anyhow = "1.0.104"
axum = { version = "0.8.9", features = ["macros"] }
base64 = "0.22"
fluxer_common = { path = "../fluxer_common" }
+3 -3
View File
@@ -45,7 +45,7 @@ pub struct InviteMetaResolver {
enum InviteMetaStorage {
Postgres(PostgresInviteMetaStorage),
#[cfg(feature = "scylla")]
Scylla(ScyllaInviteMetaStorage),
Scylla(Box<ScyllaInviteMetaStorage>),
}
struct PostgresInviteMetaStorage {
@@ -183,13 +183,13 @@ impl InviteMetaResolver {
let cache = invite_meta_cache(config);
Ok(Self {
storage: InviteMetaStorage::Scylla(ScyllaInviteMetaStorage {
storage: InviteMetaStorage::Scylla(Box::new(ScyllaInviteMetaStorage {
db,
stmt_invite,
stmt_guild,
stmt_channel,
stmt_user,
}),
})),
cache,
})
}
+1 -1
View File
@@ -5,7 +5,7 @@ edition.workspace = true
license.workspace = true
[dependencies]
anyhow = "1.0.102"
anyhow = "1.0.104"
aws-credential-types = "1.2.14"
aws-sigv4 = "1.4.5"
axum = { version = "0.8.9", default-features = false }
+2 -2
View File
@@ -13,9 +13,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.102"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "autotools"
@@ -7,61 +7,61 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CRATE_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
require_pkg_config() {
if ! command -v pkg-config >/dev/null 2>&1; then
echo "error: pkg-config is required but was not found in PATH." >&2
echo " Install via: brew install pkg-config (macOS)" >&2
echo " apt-get install pkg-config (Debian/Ubuntu)" >&2
exit 1
fi
if ! command -v pkg-config >/dev/null 2>&1; then
echo "error: pkg-config is required but was not found in PATH." >&2
echo " Install via: brew install pkg-config (macOS)" >&2
echo " apt-get install pkg-config (Debian/Ubuntu)" >&2
exit 1
fi
}
assert_webrtc_pkgconfig() {
if ! pkg-config --exists webrtc-audio-processing-2; then
echo "error: pkg-config cannot locate webrtc-audio-processing-2." >&2
echo " See fluxer_desktop/native/audio-apm/README.md for install steps." >&2
exit 1
fi
if ! pkg-config --exists webrtc-audio-processing-2; then
echo "error: pkg-config cannot locate webrtc-audio-processing-2." >&2
echo " See fluxer_desktop/native/audio-apm/README.md for install steps." >&2
exit 1
fi
}
assert_absl_pkgconfig() {
if ! pkg-config --exists absl_base; then
echo "error: pkg-config cannot locate absl_base." >&2
echo " Install via: brew install abseil (macOS)" >&2
echo " apt-get install libabsl-dev (Debian/Ubuntu)" >&2
exit 1
fi
if ! pkg-config --exists absl_base; then
echo "error: pkg-config cannot locate absl_base." >&2
echo " Install via: brew install abseil (macOS)" >&2
echo " apt-get install libabsl-dev (Debian/Ubuntu)" >&2
exit 1
fi
}
inject_absl_include_path() {
local absl_includes
absl_includes="$(pkg-config --cflags-only-I absl_base)"
if [[ -z "${absl_includes}" ]]; then
echo "error: pkg-config returned no include path for absl_base." >&2
exit 1
fi
local absl_includes
absl_includes="$(pkg-config --cflags-only-I absl_base)"
if [[ -z "${absl_includes}" ]]; then
echo "error: pkg-config returned no include path for absl_base." >&2
exit 1
fi
local prev_cxxflags="${CXXFLAGS-}"
local prev_bindgen="${BINDGEN_EXTRA_CLANG_ARGS-}"
local prev_cxxflags="${CXXFLAGS-}"
local prev_bindgen="${BINDGEN_EXTRA_CLANG_ARGS-}"
export CXXFLAGS="${absl_includes} ${prev_cxxflags}"
export BINDGEN_EXTRA_CLANG_ARGS="${absl_includes} ${prev_bindgen}"
export CXXFLAGS="${absl_includes} ${prev_cxxflags}"
export BINDGEN_EXTRA_CLANG_ARGS="${absl_includes} ${prev_bindgen}"
}
main() {
cd "${CRATE_DIR}"
cd "${CRATE_DIR}"
local cargo_subcmd="build"
if [[ $# -gt 0 && "$1" != -* && "$1" != "--" ]]; then
cargo_subcmd="$1"
shift
fi
local cargo_subcmd="build"
if [[ $# -gt 0 && "$1" != -* && "$1" != "--" ]]; then
cargo_subcmd="$1"
shift
fi
require_pkg_config
assert_webrtc_pkgconfig
assert_absl_pkgconfig
inject_absl_include_path
require_pkg_config
assert_webrtc_pkgconfig
assert_absl_pkgconfig
inject_absl_include_path
exec cargo "${cargo_subcmd}" --features real-apm "$@"
exec cargo "${cargo_subcmd}" --features real-apm "$@"
}
main "$@"
+2 -2
View File
@@ -165,9 +165,9 @@ dependencies = [
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
version = "0.9.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
dependencies = [
"crossbeam-utils",
]
+2 -2
View File
@@ -165,9 +165,9 @@ dependencies = [
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
version = "0.9.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
dependencies = [
"crossbeam-utils",
]
+107
View File
@@ -0,0 +1,107 @@
[graph]
all-features = true
no-default-features = false
[output]
feature-depth = 1
[advisories]
version = 2
yanked = "deny"
unmaintained = "all"
ignore = []
[licenses]
version = 2
confidence-threshold = 0.93
unused-allowed-license = "allow"
allow = [
"MIT",
"MIT-0",
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"BSD-2-Clause",
"BSD-3-Clause",
"ISC",
"MPL-2.0",
"NCSA",
"Unicode-3.0",
"Zlib",
"CC0-1.0",
"AGPL-3.0-or-later",
"BSL-1.0",
"CDLA-Permissive-2.0",
]
exceptions = []
[licenses.private]
ignore = true
registries = []
[[licenses.clarify]]
crate = "ring"
expression = "ISC AND MIT AND OpenSSL"
license-files = [
{ path = "LICENSE", hash = 0xbd0eed23 },
]
[[licenses.clarify]]
crate = "webrtc-audio-processing@2.1.0"
expression = "BSD-3-Clause"
license-files = [
{ path = "COPYING", hash = 0x3d3ea952 },
]
[[licenses.clarify]]
crate = "webrtc-audio-processing-config@2.1.0"
expression = "BSD-3-Clause"
license-files = [
{ path = "COPYING", hash = 0x7e861f0d },
]
[[licenses.clarify]]
crate = "webrtc-audio-processing-sys@2.1.0"
expression = "BSD-3-Clause"
license-files = [
{ path = "COPYING", hash = 0x7e861f0d },
]
[bans]
multiple-versions = "warn"
wildcards = "deny"
allow-wildcard-paths = true
highlight = "all"
workspace-default-features = "allow"
external-default-features = "allow"
deny = [
{ crate = "fuse", reason = "libfuse2-based Rust wrapper; use a maintained FUSE3-native crate only if Fluxer ever needs FUSE directly" },
{ crate = "fuse-sys", reason = "libfuse2 FFI crate; Fluxer AppImages must not reintroduce libfuse2 through native Rust dependencies" },
]
skip = [
{ crate = "bitflags@1.3.2", reason = "hunspell bindgen requires the legacy bitflags API" },
{ crate = "foldhash@0.1.5", reason = "transitive dependencies require the prior hash table API" },
{ crate = "hashbrown@0.15.5", reason = "transitive dependencies require the older hash table API" },
{ crate = "hashbrown@0.16.1", reason = "transitive dependencies require the prior hash table API" },
{ crate = "hermit-abi@0.3.9", reason = "udev requires the prior Hermit ABI" },
{ crate = "itertools@0.13.0", reason = "bindgen and criterion require the prior iterator API" },
{ crate = "libloading@0.8.9", reason = "bindgen requires the prior dynamic loading API" },
{ crate = "nix@0.29.0", reason = "evdev requires the prior Unix API" },
{ crate = "nom@7.1.3", reason = "bindgen requires the prior parser API" },
{ crate = "rustc-hash@1.1.0", reason = "hunspell bindgen requires the legacy hash API" },
{ crate = "shlex@1.3.0", reason = "bindgen requires the prior shell parsing API" },
{ crate = "syn@1.0.109", reason = "hunspell bindgen requires the legacy macro API" },
{ crate = "syn@2.0.119", reason = "napi derive requires the prior macro API" },
{ crate = "windows-sys@0.48.0", reason = "udev requires the legacy Windows API" },
{ crate = "windows-sys@0.59.0", reason = "hunspell build tooling requires the prior Windows API" },
{ crate = "wit-bindgen@0.51.0", reason = "the WASI preview 3 compatibility layer requires the prior binding API" },
]
skip-tree = []
[sources]
unknown-registry = "deny"
unknown-git = "deny"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
allow-git = []
[sources.allow-org]
github = []
+2 -2
View File
@@ -211,9 +211,9 @@ dependencies = [
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
version = "0.9.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
dependencies = [
"crossbeam-utils",
]
@@ -52,12 +52,11 @@ impl EncoderFrameRate {
let denominator = u64::from(self.denominator);
assert!(numerator > 0, "fps numerator positive");
assert!(denominator > 0, "fps denominator positive");
((1_000_000u64 * denominator) + numerator - 1) / numerator
(1_000_000u64 * denominator).div_ceil(numerator)
}
pub fn gop_pic_size(self) -> u16 {
let rounded = (u64::from(self.numerator) + u64::from(self.denominator) - 1)
/ u64::from(self.denominator);
let rounded = u64::from(self.numerator).div_ceil(u64::from(self.denominator));
let bounded = rounded.clamp(1, u64::from(ENCODER_FRAME_RATE_MAX));
bounded as u16
}
+2 -2
View File
@@ -245,9 +245,9 @@ dependencies = [
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
version = "0.9.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
dependencies = [
"crossbeam-utils",
]
@@ -228,12 +228,11 @@ fn retain_screen_audio_sink_handle(
"DirectAudioCapture.setScreenAudioSink received an empty native external sink handle",
));
}
let handle = unsafe {
NativeScreenFrameSinkHandle::retain_from_raw(data.cast::<NativeScreenFrameSinkHandle>())
}
.ok_or_else(|| {
generic_error("DirectAudioCapture.setScreenAudioSink received an invalid handle")
})?;
let handle = unsafe { data.cast::<NativeScreenFrameSinkHandle>().as_ref() }
.and_then(NativeScreenFrameSinkHandle::retain_ref)
.ok_or_else(|| {
generic_error("DirectAudioCapture.setScreenAudioSink received an invalid handle")
})?;
Ok(Arc::new(handle))
}
@@ -740,8 +740,8 @@ mod tests {
.expect("apm");
let frame_len = DIRECT_CAPTURE_APM_FRAME_SAMPLES;
let mut samples = vec![0.0_f32; frame_len];
for n in 0..frame_len {
samples[n] = ((n as f32) / (frame_len as f32) - 0.5) * 0.5;
for (n, sample) in samples.iter_mut().enumerate() {
*sample = ((n as f32) / (frame_len as f32) - 0.5) * 0.5;
}
let original = samples.clone();
let _ = apm.process_in_place(&mut samples).expect("process");
+2 -2
View File
@@ -4,9 +4,9 @@ version = 4
[[package]]
name = "anyhow"
version = "1.0.102"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "async-broadcast"
+2 -2
View File
@@ -4,9 +4,9 @@ version = 4
[[package]]
name = "anyhow"
version = "1.0.102"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "async-broadcast"
+4 -4
View File
@@ -44,9 +44,9 @@ checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anyhow"
version = "1.0.102"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "arrayvec"
@@ -1479,9 +1479,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "spin"
version = "0.10.0"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3"
dependencies = [
"lock_api",
]
@@ -952,12 +952,13 @@ fn retain_native_frame_sink_handle(
));
}
let handle = unsafe {
NativeScreenFrameSinkHandle::retain_from_raw(data.cast::<NativeScreenFrameSinkHandle>())
}
.ok_or_else(|| {
invalid_arg("ScreenCapture.setFrameSinkHandle received an invalid native frame sink handle")
})?;
let handle = unsafe { data.cast::<NativeScreenFrameSinkHandle>().as_ref() }
.and_then(NativeScreenFrameSinkHandle::retain_ref)
.ok_or_else(|| {
invalid_arg(
"ScreenCapture.setFrameSinkHandle received an invalid native frame sink handle",
)
})?;
Ok(Arc::new(handle))
}
@@ -967,7 +968,7 @@ fn enqueue_native_bus_video_frame(
frame: &VideoFrame,
) -> EnqueueOutcome {
if let Some(dmabuf) = frame.dmabuf.as_ref() {
if sink.handle().enqueue_dmabuf.is_none() {
if !sink.supports_dmabuf() {
return EnqueueOutcome::Rejected;
}
let plane_count = dmabuf.plane_count as usize;
@@ -975,18 +976,13 @@ fn enqueue_native_bus_video_frame(
return EnqueueOutcome::Rejected;
}
let mut duped_fds: Vec<i32> = Vec::with_capacity(plane_count);
let mut duped_fds: Vec<OwnedFd> = Vec::with_capacity(plane_count);
for raw in dmabuf.fds.iter().take(plane_count) {
let duped = unsafe { libc::dup(*raw) };
if duped < 0 {
for fd in duped_fds {
unsafe {
libc::close(fd);
}
}
return EnqueueOutcome::Rejected;
}
duped_fds.push(duped);
duped_fds.push(unsafe { OwnedFd::from_raw_fd(duped) });
}
let desc = BusDmabufDesc {
@@ -1001,7 +997,7 @@ fn enqueue_native_bus_video_frame(
timestamp_us: frame.timestamp_us,
};
return unsafe { sink.enqueue_dmabuf_take_fds(desc, &duped_fds) };
return sink.enqueue_dmabuf_take_fds(desc, duped_fds);
}
sink.enqueue_nv12_copy(
@@ -138,15 +138,14 @@ fn retain_screen_audio_sink_handle(
"ProcessLoopback.setScreenAudioSink received an empty native external sink handle",
));
}
let handle = unsafe {
NativeScreenFrameSinkHandle::retain_from_raw(data.cast::<NativeScreenFrameSinkHandle>())
}
.ok_or_else(|| {
napi::Error::new(
Status::InvalidArg,
"ProcessLoopback.setScreenAudioSink received an invalid native sink handle",
)
})?;
let handle = unsafe { data.cast::<NativeScreenFrameSinkHandle>().as_ref() }
.and_then(NativeScreenFrameSinkHandle::retain_ref)
.ok_or_else(|| {
napi::Error::new(
Status::InvalidArg,
"ProcessLoopback.setScreenAudioSink received an invalid native sink handle",
)
})?;
Ok(Arc::new(handle))
}
@@ -960,15 +960,14 @@ fn retain_native_frame_sink_handle(
));
}
let handle = unsafe {
NativeScreenFrameSinkHandle::retain_from_raw(data.cast::<NativeScreenFrameSinkHandle>())
}
.ok_or_else(|| {
napi::Error::new(
Status::InvalidArg,
"ScreenCapture.setFrameSinkHandle received an invalid native frame sink handle",
)
})?;
let handle = unsafe { data.cast::<NativeScreenFrameSinkHandle>().as_ref() }
.and_then(NativeScreenFrameSinkHandle::retain_ref)
.ok_or_else(|| {
napi::Error::new(
Status::InvalidArg,
"ScreenCapture.setFrameSinkHandle received an invalid native frame sink handle",
)
})?;
Ok(Arc::new(handle))
}
@@ -980,7 +979,7 @@ fn try_enqueue_native_cv_pixel_buffer(
height: u32,
sample_buffer: &CMSampleBuffer,
) -> EnqueueOutcome {
if sink.handle().enqueue_mac_cv_pixel_buffer.is_none() {
if !sink.supports_mac_cv_pixel_buffer() {
return EnqueueOutcome::Rejected;
}
let retained =
+2 -2
View File
@@ -4,9 +4,9 @@ version = 4
[[package]]
name = "anyhow"
version = "1.0.102"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "bitflags"
+2 -2
View File
@@ -165,9 +165,9 @@ dependencies = [
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
version = "0.9.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
dependencies = [
"crossbeam-utils",
]
+4 -4
View File
@@ -34,9 +34,9 @@ checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anyhow"
version = "1.0.102"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "autocfg"
@@ -216,9 +216,9 @@ dependencies = [
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
version = "0.9.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
dependencies = [
"crossbeam-utils",
]
@@ -30,12 +30,6 @@ pub struct NSErrorBlock {
pub user_invoke: NsErrorUserInvoke,
}
pub unsafe extern "C" fn ns_error_trampoline(block: *mut NSErrorBlock, err: *mut c_void) {
if let Some(block) = unsafe { block.as_mut() } {
unsafe { (block.user_invoke)(block.ctx, err) };
}
}
pub type ContentErrorUserInvoke =
unsafe extern "C" fn(ctx: *mut c_void, content: *mut c_void, err: *mut c_void);
pub type ContentErrorInvoke =
@@ -52,16 +46,6 @@ pub struct ContentErrorBlock {
pub user_invoke: ContentErrorUserInvoke,
}
pub unsafe extern "C" fn content_error_trampoline(
block: *mut ContentErrorBlock,
content: *mut c_void,
err: *mut c_void,
) {
if let Some(block) = unsafe { block.as_mut() } {
unsafe { (block.user_invoke)(block.ctx, content, err) };
}
}
pub const NS_ERROR_BLOCK_DESCRIPTOR: BlockDescriptorSig = BlockDescriptorSig {
reserved: 0,
size: std::mem::size_of::<NSErrorBlock>() as u64,
@@ -78,6 +62,22 @@ pub const CONTENT_ERROR_BLOCK_DESCRIPTOR: BlockDescriptorSig = BlockDescriptorSi
mod tests {
use super::*;
unsafe extern "C" fn ns_error_trampoline(block: *mut NSErrorBlock, err: *mut c_void) {
if let Some(block) = unsafe { block.as_mut() } {
unsafe { (block.user_invoke)(block.ctx, err) };
}
}
unsafe extern "C" fn content_error_trampoline(
block: *mut ContentErrorBlock,
content: *mut c_void,
err: *mut c_void,
) {
if let Some(block) = unsafe { block.as_mut() } {
unsafe { (block.user_invoke)(block.ctx, content, err) };
}
}
#[repr(C)]
struct RoundTripCtx {
seen_err: *mut c_void,
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use criterion::{Criterion, black_box, criterion_group, criterion_main};
use std::hint::black_box;
use criterion::{Criterion, criterion_group, criterion_main};
use fluxer_screen_frame_bus::{CpuStagingBackend, StagingSurfacePair};
const BENCH_WIDTH: usize = 1920;
@@ -13,7 +13,7 @@ use std::sync::OnceLock;
use crate::frame_pool::PooledFrame;
#[cfg(target_os = "linux")]
use std::os::fd::OwnedFd;
use std::os::fd::{IntoRawFd, OwnedFd};
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -325,30 +325,30 @@ pub type NativeScreenFrameSinkEnqueueScreenAudioFn = unsafe extern "C" fn(
#[repr(C)]
#[derive(Clone, Copy)]
pub struct NativeScreenFrameSinkHandle {
pub magic: u32,
pub version: u32,
pub context: *const c_void,
pub retain: NativeScreenFrameSinkRetainFn,
pub release: NativeScreenFrameSinkReleaseFn,
pub enqueue_nv12: Option<NativeScreenFrameSinkEnqueueNv12Fn>,
pub enqueue_bgra: Option<NativeScreenFrameSinkEnqueueBgraFn>,
pub enqueue_mac_cv_pixel_buffer: Option<NativeScreenFrameSinkEnqueueMacCvPixelBufferFn>,
pub enqueue_dmabuf: Option<NativeScreenFrameSinkEnqueueDmabufFn>,
pub enqueue_shared_texture: Option<NativeScreenFrameSinkEnqueueSharedTextureFn>,
pub enqueue_screen_audio: Option<NativeScreenFrameSinkEnqueueScreenAudioFn>,
magic: u32,
version: u32,
context: *const c_void,
retain: NativeScreenFrameSinkRetainFn,
release: NativeScreenFrameSinkReleaseFn,
enqueue_nv12: Option<NativeScreenFrameSinkEnqueueNv12Fn>,
enqueue_bgra: Option<NativeScreenFrameSinkEnqueueBgraFn>,
enqueue_mac_cv_pixel_buffer: Option<NativeScreenFrameSinkEnqueueMacCvPixelBufferFn>,
enqueue_dmabuf: Option<NativeScreenFrameSinkEnqueueDmabufFn>,
enqueue_shared_texture: Option<NativeScreenFrameSinkEnqueueSharedTextureFn>,
enqueue_screen_audio: Option<NativeScreenFrameSinkEnqueueScreenAudioFn>,
}
unsafe impl Send for NativeScreenFrameSinkHandle {}
unsafe impl Sync for NativeScreenFrameSinkHandle {}
impl NativeScreenFrameSinkHandle {
pub fn is_valid(&self) -> bool {
fn is_valid(&self) -> bool {
self.magic == NATIVE_SCREEN_FRAME_SINK_HANDLE_MAGIC
&& self.version == NATIVE_SCREEN_FRAME_SINK_HANDLE_VERSION
&& !self.context.is_null()
}
pub unsafe fn retain_ref(&self) -> Option<NativeScreenFrameSinkHandleRef> {
pub fn retain_ref(&self) -> Option<NativeScreenFrameSinkHandleRef> {
if !self.is_valid() {
return None;
}
@@ -356,13 +356,6 @@ impl NativeScreenFrameSinkHandle {
Some(NativeScreenFrameSinkHandleRef { handle: *self })
}
pub unsafe fn retain_from_raw(raw: *const Self) -> Option<NativeScreenFrameSinkHandleRef> {
if raw.is_null() {
return None;
}
unsafe { (*raw).retain_ref() }
}
pub fn native_outcome(outcome: EnqueueOutcome) -> u32 {
match outcome {
EnqueueOutcome::Accepted => NATIVE_SCREEN_FRAME_SINK_ACCEPTED,
@@ -388,10 +381,6 @@ unsafe impl Send for NativeScreenFrameSinkHandleRef {}
unsafe impl Sync for NativeScreenFrameSinkHandleRef {}
impl NativeScreenFrameSinkHandleRef {
pub fn handle(&self) -> &NativeScreenFrameSinkHandle {
&self.handle
}
pub fn enqueue_nv12_copy(
&self,
data: &[u8],
@@ -442,6 +431,11 @@ impl NativeScreenFrameSinkHandleRef {
})
}
#[cfg(target_os = "macos")]
pub fn supports_mac_cv_pixel_buffer(&self) -> bool {
self.handle.enqueue_mac_cv_pixel_buffer.is_some()
}
#[cfg(target_os = "macos")]
pub unsafe fn enqueue_mac_cv_pixel_buffer(
&self,
@@ -467,12 +461,25 @@ impl NativeScreenFrameSinkHandleRef {
}
#[cfg(target_os = "linux")]
pub unsafe fn enqueue_dmabuf_take_fds(&self, desc: DmabufDesc, fds: &[i32]) -> EnqueueOutcome {
pub fn supports_dmabuf(&self) -> bool {
self.handle.enqueue_dmabuf.is_some()
}
#[cfg(target_os = "linux")]
pub fn enqueue_dmabuf_take_fds(&self, desc: DmabufDesc, fds: Vec<OwnedFd>) -> EnqueueOutcome {
let Some(enqueue) = self.handle.enqueue_dmabuf else {
return EnqueueOutcome::Rejected;
};
let fd_count = fds.len();
if fd_count == 0 || fd_count > 4 || usize::from(desc.plane_count) != fd_count {
return EnqueueOutcome::Rejected;
}
let mut raw_fds = [-1; 4];
for (raw_fd, fd) in raw_fds.iter_mut().zip(fds) {
*raw_fd = fd.into_raw_fd();
}
NativeScreenFrameSinkHandle::outcome_from_native(unsafe {
enqueue(self.handle.context, desc, fds.as_ptr(), fds.len())
enqueue(self.handle.context, desc, raw_fds.as_ptr(), fd_count)
})
}
+2 -2
View File
@@ -13,9 +13,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.102"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "bindgen"
+2 -2
View File
@@ -13,9 +13,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.102"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "bindgen"
@@ -792,10 +792,10 @@ fn retain_native_frame_sink_handle(
}
let handle = unsafe {
fluxer_screen_frame_bus::NativeScreenFrameSinkHandle::retain_from_raw(
data.cast::<fluxer_screen_frame_bus::NativeScreenFrameSinkHandle>(),
)
data.cast::<fluxer_screen_frame_bus::NativeScreenFrameSinkHandle>()
.as_ref()
}
.and_then(fluxer_screen_frame_bus::NativeScreenFrameSinkHandle::retain_ref)
.ok_or_else(|| {
napi::Error::new(
Status::InvalidArg,
@@ -37,15 +37,14 @@ fn retain_screen_audio_sink_handle(
"ProcessLoopback.setScreenAudioSink received an empty native external sink handle",
));
}
let handle = unsafe {
NativeScreenFrameSinkHandle::retain_from_raw(data.cast::<NativeScreenFrameSinkHandle>())
}
.ok_or_else(|| {
Error::new(
Status::InvalidArg,
"ProcessLoopback.setScreenAudioSink received an invalid native sink handle",
)
})?;
let handle = unsafe { data.cast::<NativeScreenFrameSinkHandle>().as_ref() }
.and_then(NativeScreenFrameSinkHandle::retain_ref)
.ok_or_else(|| {
Error::new(
Status::InvalidArg,
"ProcessLoopback.setScreenAudioSink received an invalid native sink handle",
)
})?;
Ok(Arc::new(handle))
}
@@ -341,7 +340,7 @@ impl Callbacks {
FrameSamples::Owned(owned) => owned.as_slice(),
};
let channels = u32::from(audio_contract::TARGET_CHANNELS);
if channels == 0 || samples.is_empty() || samples.len() % (channels as usize) != 0 {
if channels == 0 || samples.is_empty() || !samples.len().is_multiple_of(channels as usize) {
return true;
}
let frames = samples.len() as u32 / channels;
+2 -1
View File
@@ -26,7 +26,8 @@
"@types/node": "24.10.9",
"@typescript/native-preview": "7.0.0-dev.20260130.1",
"electron": "42.10.0",
"electron-builder": "26.9.0",
"electron-builder": "26.15.3",
"electron-builder-squirrel-windows": "26.15.3",
"esbuild": "0.27.2",
"typescript": "5.9.3"
},
@@ -109,7 +109,7 @@ impl Node {
}
impl MemberOSet {
fn from_sorted(&mut self, mut keys: Vec<MemberKey>) {
fn replace_from_keys(&mut self, mut keys: Vec<MemberKey>) {
keys.sort_unstable();
keys.dedup();
self.root = build_balanced(&keys);
@@ -192,7 +192,7 @@ fn from_sorted<'a>(
return Err(Error::BadArg);
}
let mut set = resource.inner.lock().map_err(|_| Error::BadArg)?;
set.from_sorted(keys);
set.replace_from_keys(keys);
Ok(atoms::ok().encode(env))
}
+3 -2
View File
@@ -1,7 +1,7 @@
% SPDX-License-Identifier: AGPL-3.0-or-later
{erl_opts, [debug_info, nowarn_deprecated_function]}.
{deps, [
{cowboy, "2.14.2"},
{cowboy, "2.18.0"},
{base64url, "1.0.1"},
{jose, "1.11.10"},
{ezstd, "1.1.0"},
@@ -13,7 +13,8 @@
{project_plugins, [
{erlfmt, {git, "https://github.com/WhatsApp/erlfmt.git", {tag, "v1.8.0"}}},
{rebar3_lint, {git, "https://github.com/project-fifo/rebar3_lint.git", {branch, "master"}}}
{rebar3_lint, {git, "https://github.com/project-fifo/rebar3_lint.git", {branch, "master"}}},
{rebar3_audit, "0.2.7"}
]}.
{pre_hooks, [{compile, "../tools/ci/run.sh build-gateway-nifs --gateway-dir ."}]}.
+9 -9
View File
@@ -1,7 +1,7 @@
{"1.2.0",
[{<<"base64url">>,{pkg,<<"base64url">>,<<"1.0.1">>},0},
{<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.14.2">>},0},
{<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.16.0">>},1},
{<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.18.0">>},0},
{<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.19.0">>},1},
{<<"enats">>,{pkg,<<"enats">>,<<"1.2.0">>},0},
{<<"enats_msg">>,{pkg,<<"enats_msg">>,<<"1.0.3">>},1},
{<<"eqwalizer_support">>,
@@ -12,26 +12,26 @@
{<<"ezstd">>,{pkg,<<"ezstd">>,<<"1.1.0">>},0},
{<<"jose">>,{pkg,<<"jose">>,<<"1.11.10">>},0},
{<<"opentelemetry_api">>,{pkg,<<"opentelemetry_api">>,<<"1.4.0">>},1},
{<<"ranch">>,{pkg,<<"ranch">>,<<"2.2.0">>},1}]}.
{<<"ranch">>,{pkg,<<"ranch">>,<<"2.2.1">>},1}]}.
[
{pkg_hash,[
{<<"base64url">>, <<"F8C7F2DA04CA9A5D0F5F50258F055E1D699F0E8BF4CFDB30B750865368403CF6">>},
{<<"cowboy">>, <<"4008BE1DF6ADE45E4F2A4E9E2D22B36D0B5ABA4E20B0A0D7049E28D124E34847">>},
{<<"cowlib">>, <<"54592074EBBBB92EE4746C8A8846E5605052F29309D3A873468D76CDF932076F">>},
{<<"cowboy">>, <<"BFF388EB4D6356CB3F88C26E65B515976BF04B401B805D31550C3E60EBA8FE18">>},
{<<"cowlib">>, <<"C9D11C9D035472E27A740C9F327786C61ED209269B4BE0260D59D3EC07B8949F">>},
{<<"enats">>, <<"D7459C804013CAFA4AF880B18D446C48890D28D372D62AD66C76187E5779248D">>},
{<<"enats_msg">>, <<"50631124F37D88BE76A91A5B96A6565C5981EBF917CD819F0175CA658A966F43">>},
{<<"ezstd">>, <<"D3B483D6ACFADFB65DBA4015371E6D54526DBF3D9EF0941B5ADD8BF5890731F4">>},
{<<"jose">>, <<"A903F5227417BD2A08C8A00A0CBCC458118BE84480955E8D251297A425723F83">>},
{<<"opentelemetry_api">>, <<"63CA1742F92F00059298F478048DFB826F4B20D49534493D6919A0DB39B6DB04">>},
{<<"ranch">>, <<"25528F82BC8D7C6152C57666CA99EC716510FE0925CB188172F41CE93117B1B0">>}]},
{<<"ranch">>, <<"FC2BB0E800EFBEAB8FF82EB325450B9C08653B14C3F69E8B09BBEA304973105D">>}]},
{pkg_hash_ext,[
{<<"base64url">>, <<"F9B3ADD4731A02A9B0410398B475B33E7566A695365237A6BDEE1BB447719F5C">>},
{<<"cowboy">>, <<"569081DA046E7B41B5DF36AA359BE71A0C8874E5B9CFF6F747073FC57BAF1AB9">>},
{<<"cowlib">>, <<"7F478D80D66B747344F0EA7708C187645CFCC08B11AA424632F78E25BF05DB51">>},
{<<"cowboy">>, <<"62D0B26ABCF455054972B0DA242389C69D5982CE5914AFB8C344517F667B9600">>},
{<<"cowlib">>, <<"6DC66E3135B229193EA4DCB14294E79520C923D391315C9C962EF0B4BEA72356">>},
{<<"enats">>, <<"20DEB3CB1D3E960194DF8B136C40D2DB085B485BBA5E493B340AB5F9FD2BED22">>},
{<<"enats_msg">>, <<"C4F2139E5144FABC99AFE01B8B016AD9DA278CDDC60857AC5D5AFB0AD1283534">>},
{<<"ezstd">>, <<"28CFA0ED6CC3922095AD5BA0F23392A1664273358B17184BAA909868361184E7">>},
{<<"jose">>, <<"0D6CD36FF8BA174DB29148FC112B5842186B68A90CE9FC2B3EC3AFE76593E614">>},
{<<"opentelemetry_api">>, <<"3DFBBFAA2C2ED3121C5C483162836C4F9027DEF469C41578AF5EF32589FCFC58">>},
{<<"ranch">>, <<"FA0B99A1780C80218A4197A59EA8D3BDAE32FBFF7E88527D7D8A4787EFF4F8E7">>}]}
{<<"ranch">>, <<"55F05CCE20EC2DA1D90DE5D5981AFB93DBFC01325FC7E933189AA5C62F037C24">>}]}
].
+29 -29
View File
@@ -5,36 +5,36 @@
set -eu
is_positive_int() {
case "${1:-}" in
'' | 0* | *[!0-9]*) return 1 ;;
*) [ "$1" -gt 0 ] ;;
esac
case "${1:-}" in
'' | 0* | *[!0-9]*) return 1 ;;
*) [ "$1" -gt 0 ] ;;
esac
}
available_cpu_count() {
cpu_quota=""
cpu_period=""
if [ -r /sys/fs/cgroup/cpu.max ]; then
read -r cpu_quota cpu_period < /sys/fs/cgroup/cpu.max || true
else
for cgroup_dir in /sys/fs/cgroup/cpu /sys/fs/cgroup/cpu,cpuacct; do
if [ -r "$cgroup_dir/cpu.cfs_quota_us" ] && [ -r "$cgroup_dir/cpu.cfs_period_us" ]; then
read -r cpu_quota < "$cgroup_dir/cpu.cfs_quota_us" || true
read -r cpu_period < "$cgroup_dir/cpu.cfs_period_us" || true
break
fi
done
fi
if is_positive_int "$cpu_quota" && is_positive_int "$cpu_period"; then
echo "$(((cpu_quota + cpu_period - 1) / cpu_period))"
return 0
fi
cpu_count="$(nproc 2>/dev/null || echo 1)"
if is_positive_int "$cpu_count"; then
echo "$cpu_count"
else
echo 1
fi
cpu_quota=""
cpu_period=""
if [ -r /sys/fs/cgroup/cpu.max ]; then
read -r cpu_quota cpu_period </sys/fs/cgroup/cpu.max || true
else
for cgroup_dir in /sys/fs/cgroup/cpu /sys/fs/cgroup/cpu,cpuacct; do
if [ -r "$cgroup_dir/cpu.cfs_quota_us" ] && [ -r "$cgroup_dir/cpu.cfs_period_us" ]; then
read -r cpu_quota <"$cgroup_dir/cpu.cfs_quota_us" || true
read -r cpu_period <"$cgroup_dir/cpu.cfs_period_us" || true
break
fi
done
fi
if is_positive_int "$cpu_quota" && is_positive_int "$cpu_period"; then
echo "$(((cpu_quota + cpu_period - 1) / cpu_period))"
return 0
fi
cpu_count="$(nproc 2>/dev/null || echo 1)"
if is_positive_int "$cpu_count"; then
echo "$cpu_count"
else
echo 1
fi
}
: "${FLUXER_ERLANG_NODE_NAME:=fluxer_gateway@127.0.0.1}"
@@ -42,11 +42,11 @@ available_cpu_count() {
: "${FLUXER_ERLANG_DIST_PORT:=8081}"
if ! is_positive_int "${FLUXER_ERLANG_SCHEDULERS:-}"; then
FLUXER_ERLANG_SCHEDULERS="$(available_cpu_count)"
FLUXER_ERLANG_SCHEDULERS="$(available_cpu_count)"
fi
if ! is_positive_int "${FLUXER_ERLANG_DIRTY_CPU_SCHEDULERS:-}"; then
FLUXER_ERLANG_DIRTY_CPU_SCHEDULERS="$(((FLUXER_ERLANG_SCHEDULERS * 2 + 2) / 3))"
FLUXER_ERLANG_DIRTY_CPU_SCHEDULERS="$(((FLUXER_ERLANG_SCHEDULERS * 2 + 2) / 3))"
fi
export FLUXER_ERLANG_NODE_NAME
@@ -6,11 +6,11 @@ set -eu
command="${1:-}"
case "$command" in
eval|console|remote_console|remote|remsh|console_clean|console_boot|daemon|daemon_boot|daemon_attach|escript)
echo "fluxer_gateway: '$command' is disabled in production; use bounded rpc/runtime tools instead." >&2
exit 64
;;
eval | console | remote_console | remote | remsh | console_clean | console_boot | daemon | daemon_boot | daemon_attach | escript)
echo "fluxer_gateway: '$command' is disabled in production; use bounded rpc/runtime tools instead." >&2
exit 64
;;
esac
script_dir="$(CDPATH= cd "$(dirname "$0")" && pwd -P)"
script_dir="$(CDPATH='' cd "$(dirname "$0")" && pwd -P)"
exec "$script_dir/fluxer_gateway.real" "$@"
+1 -1
View File
@@ -130,7 +130,7 @@ strip_members_test() ->
<<"channels">> => [#{<<"id">> => <<"10">>}],
<<"roles">> => [#{<<"id">> => <<"role1">>}]
},
Stripped = strip_members(Data),
Stripped = #{} = strip_members(Data),
?assertEqual(false, maps:is_key(<<"members">>, Stripped)),
?assertEqual(false, maps:is_key(<<"member_role_index">>, Stripped)),
?assertEqual([#{<<"id">> => <<"10">>}], maps:get(<<"channels">>, Stripped)),
@@ -17,9 +17,6 @@
-define(SESSION_CONNECT_MAX_WORKERS, 8).
-define(SESSION_CONNECT_DEFAULT_MAX_QUEUE, 1024).
-define(CONNECT_SNAPSHOT_HEAVY_MEMBER_KEYS, [
<<"members">>, members_normalized, <<"member_role_index">>, members_sorted_ids
]).
-spec ensure_session_connect_queue(term()) -> queue:queue().
ensure_session_connect_queue(Value) when is_list(Value) ->
@@ -366,95 +363,11 @@ queued_session_pid(_) ->
-spec start_worker(map(), map()) -> map().
start_worker(Item, State) ->
Self = self(),
Snapshot = build_connect_snapshot(Item, State),
Snapshot = guild_data:build_connect_snapshot(Item, State),
{_Pid, Ref} = spawn_monitor(fun() -> compute_and_send_done(Item, Self, Snapshot) end),
WorkerRefs = maps:get(session_connect_worker_refs, State, #{}),
State#{session_connect_worker_refs => WorkerRefs#{Ref => true}}.
-spec build_connect_snapshot(map(), map()) -> map().
build_connect_snapshot(Item, State) ->
Base = maps:with(
[
id,
data,
sessions,
member_count,
voice_server_pid,
voice_states,
member_list_engine,
virtual_channel_access
],
State
),
maybe_trim_connect_snapshot(Item, Base, State).
-spec maybe_trim_connect_snapshot(map(), map(), map()) -> map().
maybe_trim_connect_snapshot(Item, Base, State) ->
case has_members_ets(State) of
true -> trim_connect_snapshot(Item, Base);
false -> Base
end.
-spec has_members_ets(map()) -> boolean().
has_members_ets(#{data := #{members_ets := Tab}}) -> is_reference(Tab);
has_members_ets(_) -> false.
-spec trim_connect_snapshot(map(), map()) -> map().
trim_connect_snapshot(Item, #{data := Data} = Base) when is_map(Data) ->
Retained = retained_member_map(Item, Base, Data),
Trimmed = maps:without(?CONNECT_SNAPSHOT_HEAVY_MEMBER_KEYS, Data),
Base#{
data => Trimmed#{
<<"members">> => Retained,
members_normalized => Retained,
<<"member_role_index">> =>
guild_data_index_members:build_member_role_index(Retained)
}
};
trim_connect_snapshot(_Item, Base) ->
Base.
-spec retained_member_map(map(), map(), map()) -> #{integer() => map()}.
retained_member_map(Item, Base, Data) ->
UserIds = [connect_user_id(Item) | voice_state_user_ids(Base)],
lists:foldl(
fun(UserId, Acc) -> retain_member(UserId, Data, Acc) end,
#{},
UserIds
).
-spec retain_member(term(), map(), #{integer() => map()}) -> #{integer() => map()}.
retain_member(UserId, Data, Acc) when is_integer(UserId) ->
case guild_data_index_members:get_member_ets(UserId, Data) of
Member when is_map(Member) -> Acc#{UserId => Member};
_ -> Acc
end;
retain_member(_UserId, _Data, Acc) ->
Acc.
-spec connect_user_id(map()) -> integer() | undefined.
connect_user_id(Item) ->
Request = maps:get(request, Item, #{}),
case maps:get(user_id, Request, undefined) of
UserId when is_integer(UserId) -> UserId;
_ -> undefined
end.
-spec voice_state_user_ids(map()) -> [integer()].
voice_state_user_ids(Base) ->
maps:fold(
fun(_Key, VoiceState, Acc) -> add_voice_state_user_id(VoiceState, Acc) end,
[],
voice_state_utils:voice_states(Base)
).
-spec add_voice_state_user_id(term(), [integer()]) -> [integer()].
add_voice_state_user_id(VoiceState, Acc) ->
case voice_state_utils:voice_state_user_id(VoiceState) of
UserId when is_integer(UserId) -> [UserId | Acc];
_ -> Acc
end.
-spec compute_and_send_done(map(), pid(), map()) -> ok.
compute_and_send_done(Item, GuildPid, Snapshot) ->
Request = maps:get(request, Item, #{}),
+99 -4
View File
@@ -13,6 +13,7 @@
-export([get_vanity_url_channel/1]).
-export([get_first_viewable_text_channel/1]).
-export([get_guild_state/2]).
-export([build_connect_snapshot/2]).
-export([fetch_latest_voice_states/1]).
-export([find_everyone_viewable_text_channel/2]).
@@ -21,6 +22,10 @@
-type user_id() :: integer().
-type guild_id() :: integer().
-define(CONNECT_SNAPSHOT_HEAVY_MEMBER_KEYS, [
<<"members">>, members_normalized, <<"member_role_index">>, members_sorted_ids
]).
-export_type([guild_state/0, guild_reply/1, user_id/0]).
-spec get_guild_data(map(), guild_state()) -> guild_reply(map()).
@@ -147,6 +152,92 @@ get_guild_state(UserId, State) ->
JoinedAt
).
-spec build_connect_snapshot(map(), guild_state()) -> map().
build_connect_snapshot(Item, State) ->
Base = maps:with(
[
id,
data,
sessions,
member_count,
voice_server_pid,
voice_states,
member_list_engine,
virtual_channel_access
],
State
),
maybe_trim_connect_snapshot(Item, Base, State).
-spec maybe_trim_connect_snapshot(map(), map(), guild_state()) -> map().
maybe_trim_connect_snapshot(Item, Base, State) ->
case has_members_ets(State) of
true -> trim_connect_snapshot(Item, Base);
false -> Base
end.
-spec has_members_ets(guild_state()) -> boolean().
has_members_ets(#{data := #{members_ets := Tab}}) -> is_reference(Tab);
has_members_ets(_) -> false.
-spec trim_connect_snapshot(map(), map()) -> map().
trim_connect_snapshot(Item, #{data := Data} = Base) when is_map(Data) ->
Retained = retained_member_map(Item, Base, Data),
Trimmed = maps:without(?CONNECT_SNAPSHOT_HEAVY_MEMBER_KEYS, Data),
Base#{
data => Trimmed#{
<<"members">> => Retained,
members_normalized => Retained,
<<"member_role_index">> =>
guild_data_index_members:build_member_role_index(Retained)
}
};
trim_connect_snapshot(_Item, Base) ->
Base.
-spec retained_member_map(map(), map(), map()) -> #{integer() => map()}.
retained_member_map(Item, Base, Data) ->
UserIds = [connect_user_id(Item) | voice_state_user_ids(Base)],
lists:foldl(
fun(UserId, Acc) -> retain_member(UserId, Data, Acc) end,
#{},
UserIds
).
-spec retain_member(term(), map(), #{integer() => map()}) -> #{integer() => map()}.
retain_member(UserId, Data, Acc) when is_integer(UserId) ->
case guild_data_index_members:get_member_ets(UserId, Data) of
Member when is_map(Member) -> Acc#{UserId => Member};
_ -> Acc
end;
retain_member(_UserId, _Data, Acc) ->
Acc.
-spec connect_user_id(map()) -> integer() | undefined.
connect_user_id(Item) ->
Request = maps:get(request, Item, #{}),
case maps:get(user_id, Request, undefined) of
UserId when is_integer(UserId) -> UserId;
_ -> undefined
end.
-spec voice_state_user_ids(map()) -> [integer()].
voice_state_user_ids(Base) ->
maps:fold(
fun(_Key, VoiceState, Acc) -> add_voice_state_user_id(VoiceState, Acc) end,
[],
voice_state_utils:voice_states(Base)
).
-spec add_voice_state_user_id(term(), [integer()]) -> [integer()].
add_voice_state_user_id(VoiceState, Acc) when is_map(VoiceState) ->
case voice_state_utils:voice_state_user_id(VoiceState) of
UserId when is_integer(UserId) -> [UserId | Acc];
_ -> Acc
end;
add_voice_state_user_id(_VoiceState, Acc) ->
Acc.
-spec resolve_voice_members([map()], map()) -> [map()].
resolve_voice_members(VoiceStates, Data) ->
lists:filtermap(fun(VS) -> resolve_voice_member_entry(VS, Data) end, VoiceStates).
@@ -164,14 +255,18 @@ resolve_voice_member_entry(VoiceState, Data) ->
resolve_voice_member_by_lookup(VoiceState, Data) ->
case voice_state_utils:voice_state_user_id(VoiceState) of
UserId when is_integer(UserId) ->
case guild_data_index_members:get_member_ets(UserId, Data) of
Member when is_map(Member) -> {true, Member};
_ -> false
end;
resolve_voice_member_from_index(UserId, Data);
_ ->
false
end.
-spec resolve_voice_member_from_index(integer(), map()) -> {true, map()} | false.
resolve_voice_member_from_index(UserId, Data) ->
case guild_data_index_members:get_member_ets(UserId, Data) of
Member when is_map(Member) -> {true, Member};
_ -> false
end.
-spec fetch_latest_voice_states(guild_state()) -> guild_state().
fetch_latest_voice_states(State) ->
case maps:get(voice_server_pid, State, undefined) of
+19 -8
View File
@@ -5,6 +5,7 @@
-export([
normalize_data/1,
normalize_map/1,
member_map/1,
member_values/1,
member_list/1,
@@ -36,6 +37,12 @@
-spec normalize_data(term()) -> term().
normalize_data(Data) when is_map(Data) ->
normalize_map(Data);
normalize_data(Data) ->
Data.
-spec normalize_map(map()) -> guild_data().
normalize_map(Data) ->
Data0 =
case guild_data_normalize:guild_data(Data) of
Normalized when is_map(Normalized) -> Normalized;
@@ -60,9 +67,7 @@ normalize_data(Data) when is_map(Data) ->
guild_data_index_roles:build_role_perms_cache(Roles),
overwrite_perms_cache =>
guild_data_index_channels:build_overwrite_perms_cache(Channels)
};
normalize_data(Data) ->
Data.
}.
-spec member_map(term()) -> map().
member_map(Data) -> guild_data_index_members:member_map(Data).
@@ -104,7 +109,8 @@ role_list(Data) -> guild_data_index_roles:role_list(Data).
role_index(Data) -> guild_data_index_roles:role_index(Data).
-spec put_roles(term(), guild_data()) -> guild_data().
put_roles(Roles, Data) -> guild_data_index_roles:put_roles(Roles, Data).
put_roles(Roles, Data) ->
ensure_guild_data(guild_data_index_roles:put_roles(Roles, Data)).
-spec channel_list(term()) -> [map()].
channel_list(Data) -> guild_data_index_channels:channel_list(Data).
@@ -113,7 +119,8 @@ channel_list(Data) -> guild_data_index_channels:channel_list(Data).
channel_index(Data) -> guild_data_index_channels:channel_index(Data).
-spec put_channels(term(), guild_data()) -> guild_data().
put_channels(Channels, Data) -> guild_data_index_channels:put_channels(Channels, Data).
put_channels(Channels, Data) ->
ensure_guild_data(guild_data_index_channels:put_channels(Channels, Data)).
-spec build_role_perms_cache([map()]) -> map().
build_role_perms_cache(Roles) -> guild_data_index_roles:build_role_perms_cache(Roles).
@@ -150,6 +157,10 @@ extract_integer_list(Value) ->
ensure_data_map(State) ->
map_utils:ensure_map(map_utils:get_safe(State, data, #{})).
-spec ensure_guild_data(term()) -> guild_data().
ensure_guild_data(Data) when is_map(Data) ->
Data.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
@@ -159,13 +170,13 @@ normalize_data_builds_indexes_test() ->
<<"roles">> => [#{<<"id">> => <<"100">>}],
<<"channels">> => [#{<<"id">> => <<"200">>}]
},
Normalized = normalize_data(Data),
Normalized = normalize_map(Data),
?assert(is_map(maps:get(<<"members">>, Normalized))),
?assertMatch(#{100 := _}, role_index(Normalized)),
?assertMatch(#{200 := _}, channel_index(Normalized)).
normalize_data_empty_lists_test() ->
Normalized = normalize_data(#{<<"members">> => [], <<"roles">> => [], <<"channels">> => []}),
Normalized = normalize_map(#{<<"members">> => [], <<"roles">> => [], <<"channels">> => []}),
?assertEqual(#{}, maps:get(<<"members">>, Normalized)),
?assertEqual([], maps:get(<<"roles">>, Normalized)),
?assertEqual([], maps:get(<<"channels">>, Normalized)).
@@ -175,7 +186,7 @@ normalize_data_non_map_input_test() ->
?assertEqual(42, normalize_data(42)).
normalize_data_missing_keys_defaults_test() ->
Normalized = normalize_data(#{}),
Normalized = normalize_map(#{}),
?assertEqual(#{}, maps:get(<<"members">>, Normalized)),
?assertEqual([], maps:get(<<"roles">>, Normalized)),
?assertEqual([], maps:get(<<"channels">>, Normalized)).
+2 -2
View File
@@ -23,7 +23,7 @@ init_base_state(GuildState) ->
Data0 = maps:get(data, TransferSafe, #{}),
ExistingVoice = maps:get(voice_states, TransferSafe, #{}),
{VoiceStates, Data1} = extract_voice_states_from_data(Data0, ExistingVoice),
NormalizedData = guild_data_index:normalize_data(Data1),
NormalizedData = guild_data_index:normalize_map(Data1),
MemberTab = ets:new(guild_members_data, [set, public, {read_concurrency, true}]),
populate_member_ets(MemberTab, NormalizedData),
BaseState = TransferSafe#{
@@ -122,7 +122,7 @@ handle_reload(NewData, State) ->
{ReloadVoiceStates, ReloadData} = extract_voice_states_from_data(
NewData, ExistingVoiceStates
),
NormalizedNewData = guild_data_index:normalize_data(ReloadData),
NormalizedNewData = guild_data_index:normalize_map(ReloadData),
NewState0 = State#{voice_states => ReloadVoiceStates, data => NormalizedNewData},
NewState1 = guild_availability:handle_unavailability_transition(State, NewState0),
NewState2 = guild_sessions:refresh_all_viewable_channels(NewState1),
@@ -457,8 +457,8 @@ permission_cache_rebuilds_on_member_role_change_test() ->
{ok, Snapshot} = permission_cache_refresh(
guild_member_update, EventData, OldState, NewState
),
Member = guild_permissions:find_member_by_user_id(UserId, Snapshot),
?assertEqual([10, 11], maps:get(<<"roles">>, Member)).
#{<<"roles">> := Roles} = guild_permissions:find_member_by_user_id(UserId, Snapshot),
?assertEqual([10, 11], Roles).
permission_cache_rebuilds_on_member_timeout_change_test() ->
UserId = 4242,
@@ -473,8 +473,9 @@ permission_cache_rebuilds_on_member_timeout_change_test() ->
{ok, Snapshot} = permission_cache_refresh(
guild_member_update, EventData, OldState, NewState
),
Member = guild_permissions:find_member_by_user_id(UserId, Snapshot),
?assertEqual(Until, maps:get(<<"communication_disabled_until">>, Member)).
#{<<"communication_disabled_until">> := MemberUntil} =
guild_permissions:find_member_by_user_id(UserId, Snapshot),
?assertEqual(Until, MemberUntil).
permission_cache_rebuilds_for_other_mutating_events_test() ->
State = permission_cache_state(7710004, permission_cache_member(4242, [10], <<"old">>)),
@@ -497,7 +498,7 @@ permission_cache_refresh(Event, EventData, OldState, NewState) ->
permission_cache_state(GuildId, Member) ->
#{
id => GuildId,
data => guild_data_index:normalize_data(#{
data => guild_data_index:normalize_map(#{
<<"guild">> => #{<<"owner_id">> => <<"999">>},
<<"roles">> => [prune_role(10, 0), prune_role(11, 0)],
<<"members">> => [Member],
@@ -79,23 +79,26 @@ range(_OSet, _Start, _Count) ->
to_list(_OSet) ->
erlang:nif_error(nif_not_loaded).
-spec nif_path() -> file:filename_all().
-spec nif_path() -> string().
nif_path() ->
filename:join(priv_dir(), ?NIF_NAME).
require_string_path(filename:join(priv_dir(), ?NIF_NAME)).
-spec priv_dir() -> file:filename_all().
-spec require_string_path(file:filename_all()) -> string().
require_string_path(Path) when is_list(Path) -> Path.
-spec priv_dir() -> string().
priv_dir() ->
case code:priv_dir(fluxer_gateway) of
{error, _Reason} -> priv_dir_from_beam();
Dir -> Dir
Dir when is_list(Dir) -> Dir
end.
-spec priv_dir_from_beam() -> file:filename_all().
-spec priv_dir_from_beam() -> string().
priv_dir_from_beam() ->
case code:which(?MODULE) of
Beam when is_list(Beam) ->
AppDir = filename:dirname(filename:dirname(Beam)),
filename:join(AppDir, "priv");
require_string_path(filename:join(AppDir, "priv"));
_ ->
"priv"
end.
@@ -141,17 +141,15 @@ get_session_ranges(SessionId, ListId, Tab) ->
list_ids(Tab) ->
list_ids_loop(Tab, ets:next(Tab, {<<>>, <<>>}), []).
-spec fold_list_subs(
binary(), ets:table(), fun((binary(), [range()], map()) -> map()), map()
) -> map().
-spec fold_list_subs(binary(), ets:table(), fun((binary(), [range()], Acc) -> Acc), Acc) -> Acc.
fold_list_subs(ListId, Tab, Fun, Acc0) ->
fold_list_subs_loop(Tab, ets:next(Tab, {ListId, <<>>}), ListId, Fun, Acc0).
-spec fold_lists(fun((binary(), list_subs(), map()) -> map()), map(), ets:table()) -> map().
-spec fold_lists(fun((binary(), list_subs(), Acc) -> Acc), Acc, ets:table()) -> Acc.
fold_lists(Fun, Acc0, Tab) ->
fold_lists_loop(Fun, Acc0, Tab, ets:next(Tab, {<<>>, <<>>})).
-spec fold_list_ids(fun((binary(), Acc) -> Acc), Acc, ets:table()) -> Acc when Acc :: term().
-spec fold_list_ids(fun((binary(), Acc) -> Acc), Acc, ets:table()) -> Acc.
fold_list_ids(Fun, Acc0, Tab) ->
fold_list_ids_loop(Fun, Acc0, Tab, ets:next(Tab, {<<>>, <<>>})).
@@ -191,8 +189,8 @@ list_ids_loop(Tab, Key, Acc) ->
list_ids_loop(Tab, ets:next(Tab, Key), Acc).
-spec fold_list_subs_loop(
ets:table(), term(), binary(), fun((binary(), [range()], map()) -> map()), map()
) -> map().
ets:table(), term(), binary(), fun((binary(), [range()], Acc) -> Acc), Acc
) -> Acc.
fold_list_subs_loop(_Tab, '$end_of_table', _ListId, _Fun, Acc) ->
Acc;
fold_list_subs_loop(_Tab, {Other, _}, ListId, _Fun, Acc) when Other =/= ListId ->
@@ -202,8 +200,8 @@ fold_list_subs_loop(Tab, {ListId, SId} = Key, ListId, Fun, Acc) when is_binary(S
fold_list_subs_loop(Tab, ets:next(Tab, Key), ListId, Fun, Fun(SId, Ranges, Acc)).
-spec fold_lists_loop(
fun((binary(), list_subs(), map()) -> map()), map(), ets:table(), term()
) -> map().
fun((binary(), list_subs(), Acc) -> Acc), Acc, ets:table(), term()
) -> Acc.
fold_lists_loop(_Fun, Acc, _Tab, '$end_of_table') ->
Acc;
fold_lists_loop(_Fun, Acc, _Tab, {idx_tab, _}) ->
@@ -214,8 +212,7 @@ fold_lists_loop(Fun, Acc, Tab, {ListId, _} = Key) when is_binary(ListId) ->
NextListKey = skip_past_list(Tab, Key, ListId),
fold_lists_loop(Fun, Acc2, Tab, NextListKey).
-spec fold_list_ids_loop(fun((binary(), Acc) -> Acc), Acc, ets:table(), term()) -> Acc when
Acc :: term().
-spec fold_list_ids_loop(fun((binary(), Acc) -> Acc), Acc, ets:table(), term()) -> Acc.
fold_list_ids_loop(_Fun, Acc, _Tab, '$end_of_table') ->
Acc;
fold_list_ids_loop(_Fun, Acc, _Tab, {idx_tab, _}) ->
@@ -46,7 +46,7 @@ put_state(_) ->
-spec put_data(guild_id(), guild_data()) -> ok.
put_data(GuildId, Data) when is_integer(GuildId), is_map(Data) ->
NormalizedData = guild_data_index:normalize_data(Data),
NormalizedData = guild_data_index:normalize_map(Data),
put_normalized_data(GuildId, NormalizedData);
put_data(_, _) ->
ok.
+2 -2
View File
@@ -57,7 +57,7 @@ process_presence(UserId, Payload, Member, State) ->
),
process_presence_change(UserId, OldPresence, PresenceMap, Status, State).
-spec process_presence_change(user_id(), map() | undefined, map(), atom(), guild_state()) ->
-spec process_presence_change(user_id(), map(), map(), atom(), guild_state()) ->
{noreply, guild_state()}.
process_presence_change(UserId, PresenceMap, PresenceMap, Status, State) ->
{noreply, maybe_handle_unchanged_presence(Status, UserId, State)};
@@ -397,7 +397,7 @@ handle_bus_presence_unchanged_payload_is_noop_test() ->
<<"afk">> => false,
<<"user">> => #{<<"id">> => <<"1">>, <<"username">> => <<"Alpha">>}
},
Member = guild_permissions:find_member_by_user_id(1, State),
Member = #{} = guild_permissions:find_member_by_user_id(1, State),
PresenceMap = build_presence_map(Payload, Member),
ets:insert(maps:get(member_presence, State), {1, PresenceMap}),
{noreply, NewState} = handle_bus_presence(1, Payload, State),
+1 -1
View File
@@ -29,7 +29,7 @@ update_state(Event, EventData, State) ->
ensure_normalized(Data) ->
case maps:is_key(members_normalized, Data) of
true -> Data;
false -> guild_data_index:normalize_data(Data)
false -> guild_data_index:normalize_map(Data)
end.
-spec update_data_for_event(event(), event_data(), guild_data(), guild_state()) -> guild_data().
+3 -17
View File
@@ -140,7 +140,7 @@ sync_user_blocked_ids(UserId, BlockedIds) ->
-spec sync_user_blocked_ids_local(integer(), term()) -> ok.
sync_user_blocked_ids_local(UserId, BlockedIds) ->
case integer_list(BlockedIds) of
case push_normalize:integer_list(BlockedIds) of
{ok, TypedBlockedIds} ->
put_blocked_ids_local(UserId, TypedBlockedIds);
error ->
@@ -175,7 +175,7 @@ invalidate_user_badge_count_local(UserId) ->
-spec invalidate_user_badge_counts_local(term()) -> ok.
invalidate_user_badge_counts_local(UserIds) ->
case integer_list(UserIds) of
case push_normalize:integer_list(UserIds) of
{ok, TypedUserIds} ->
lists:foreach(fun invalidate_user_badge_count_local/1, TypedUserIds);
error ->
@@ -419,7 +419,7 @@ dispatch_if_eligible(
-spec handle_sync_user_blocked_ids(integer(), term(), state()) -> {noreply, state()}.
handle_sync_user_blocked_ids(UserId, BlockedIds, State) ->
case integer_list(BlockedIds) of
case push_normalize:integer_list(BlockedIds) of
{ok, TypedBlockedIds} ->
push_ets_cache:put_blocked_ids(UserId, TypedBlockedIds),
{noreply, State};
@@ -465,20 +465,6 @@ log_message_worker_drop(dropped, Params) ->
channel_id => maps:get(<<"channel_id">>, MessageData, undefined)
}).
-spec integer_list(term()) -> {ok, [integer()]} | error.
integer_list(Value) when is_list(Value) ->
integer_list(Value, []);
integer_list(_) ->
error.
-spec integer_list([term()], [integer()]) -> {ok, [integer()]} | error.
integer_list([], Acc) ->
{ok, lists:reverse(Acc)};
integer_list([Value | Rest], Acc) when is_integer(Value) ->
integer_list(Rest, [Value | Acc]);
integer_list(_, _) ->
error.
-spec local_cache_mutation(fun(() -> ok)) -> ok.
local_cache_mutation(Fun) ->
case whereis(?MODULE) of
+7 -15
View File
@@ -527,27 +527,19 @@ send_job_options(UserIds0, Options) ->
ChannelName0 = maps:get(channel_name, Options),
BadgeCountsTtlSeconds = maps:get(badge_counts_ttl_seconds, Options),
MarkdownContext = maps:get(markdown_context, Options, #{}),
case {integer_list(UserIds0), optional_binary(GuildName0), optional_binary(ChannelName0)} of
case
{
push_normalize:integer_list(UserIds0),
optional_binary(GuildName0),
optional_binary(ChannelName0)
}
of
{{ok, UserIds}, {ok, GuildName}, {ok, ChannelName}} when is_map(MarkdownContext) ->
{ok, UserIds, GuildName, ChannelName, BadgeCountsTtlSeconds, MarkdownContext};
_ ->
error
end.
-spec integer_list(term()) -> {ok, [integer()]} | error.
integer_list(Value) when is_list(Value) ->
integer_list(Value, []);
integer_list(_) ->
error.
-spec integer_list([term()], [integer()]) -> {ok, [integer()]} | error.
integer_list([], Acc) ->
{ok, lists:reverse(Acc)};
integer_list([Value | Rest], Acc) when is_integer(Value) ->
integer_list(Rest, [Value | Acc]);
integer_list(_, _) ->
error.
-spec optional_binary(term()) -> {ok, binary() | undefined} | error.
optional_binary(undefined) ->
{ok, undefined};
+14 -10
View File
@@ -104,16 +104,20 @@ resolve_notification_group(Data, MessageTag) ->
Group when is_binary(Group), byte_size(Group) > 0 ->
Group;
_ ->
case
push_utils:normalize_binary(
maps:get(<<"channel_id">>, Data, undefined), undefined
)
of
ChannelId when is_binary(ChannelId), byte_size(ChannelId) > 0 ->
<<"channel:", ChannelId/binary>>;
_ ->
derive_channel_tag_from_message_tag(MessageTag)
end
resolve_channel_notification_group(Data, MessageTag)
end.
-spec resolve_channel_notification_group(map(), binary()) -> binary().
resolve_channel_notification_group(Data, MessageTag) ->
case
push_utils:normalize_binary(
maps:get(<<"channel_id">>, Data, undefined), undefined
)
of
ChannelId when is_binary(ChannelId), byte_size(ChannelId) > 0 ->
<<"channel:", ChannelId/binary>>;
_ ->
derive_channel_tag_from_message_tag(MessageTag)
end.
-spec derive_channel_tag_from_message_tag(binary()) -> binary().
@@ -26,23 +26,26 @@ available() ->
render_push_preview_nif(_Content, _ContextJson) ->
erlang:nif_error(nif_not_loaded).
-spec nif_path() -> file:filename_all().
-spec nif_path() -> string().
nif_path() ->
filename:join(priv_dir(), ?NIF_NAME).
require_string_path(filename:join(priv_dir(), ?NIF_NAME)).
-spec priv_dir() -> file:filename_all().
-spec require_string_path(file:filename_all()) -> string().
require_string_path(Path) when is_list(Path) -> Path.
-spec priv_dir() -> string().
priv_dir() ->
case code:priv_dir(fluxer_gateway) of
{error, _Reason} -> priv_dir_from_beam();
Dir -> Dir
Dir when is_list(Dir) -> Dir
end.
-spec priv_dir_from_beam() -> file:filename_all().
-spec priv_dir_from_beam() -> string().
priv_dir_from_beam() ->
case code:which(?MODULE) of
Beam when is_list(Beam) ->
AppDir = filename:dirname(filename:dirname(Beam)),
filename:join(AppDir, "priv");
require_string_path(filename:join(AppDir, "priv"));
_ ->
"priv"
end.
+16 -1
View File
@@ -5,7 +5,8 @@
-export([
optional_guild_id/1,
notification_level/1
notification_level/1,
integer_list/1
]).
-spec optional_guild_id(term()) -> pos_integer() | undefined.
@@ -21,3 +22,17 @@ notification_level(Value) ->
Level when Level >= -1, Level =< 3 -> Level;
_ -> undefined
end.
-spec integer_list(term()) -> {ok, [integer()]} | error.
integer_list(Value) when is_list(Value) ->
integer_list(Value, []);
integer_list(_) ->
error.
-spec integer_list([term()], [integer()]) -> {ok, [integer()]} | error.
integer_list([], Acc) ->
{ok, lists:reverse(Acc)};
integer_list([Value | Rest], Acc) when is_integer(Value) ->
integer_list(Rest, [Value | Acc]);
integer_list(_, _) ->
error.
@@ -71,8 +71,18 @@ cleanup_indexed_down(SessionId, Ref, Pid, Sessions0, Refs1, State) ->
end.
-spec cleanup_down_fallback(pid(), session_map(), map()) -> map().
cleanup_down_fallback(Pid, Sessions0, State) ->
Sessions1 = process_registry:cleanup_on_down(session, Pid, Sessions0),
cleanup_down_fallback(DeadPid, Sessions0, State) ->
Sessions1 = maps:fold(
fun
(SessionId, {SessionPid, _Ref}, Acc) when SessionPid =:= DeadPid ->
ok = process_registry:safe_unregister({session, SessionId}, DeadPid),
Acc;
(SessionId, SessionRef, Acc) ->
Acc#{SessionId => SessionRef}
end,
#{},
Sessions0
),
replace_sessions(Sessions1, State).
-spec ref_index(map()) -> ref_index().
@@ -217,10 +217,10 @@ dedup_users(Users) ->
Map = lists:foldl(fun add_user_by_id/2, #{}, Users),
maps:values(Map).
-spec add_user_by_id(term(), #{user_id() => map()}) -> #{user_id() => map()}.
-spec add_user_by_id(map() | undefined, #{user_id() => map()}) -> #{user_id() => map()}.
add_user_by_id(undefined, Acc) ->
Acc;
add_user_by_id(U, Acc) ->
add_user_by_id(U, Acc) when is_map(U) ->
case user_id(U) of
undefined -> Acc;
Id -> Acc#{Id => U#{<<"id">> => Id}}
+3 -3
View File
@@ -81,9 +81,9 @@ get_auth_context_matches_get_data_scalars_test() ->
get_auth_context_is_reachable_through_the_query_handler_test() ->
State = test_state(),
Request = {get_guild_auth_context, #{user_id => 200, channel_id => 500}},
{reply, Reply, _} = guild_query_handler:handle_call(Request, {self(), make_ref()}, State),
Context = maps:get(auth_context, Reply),
?assertEqual(500, maps:get(<<"id">>, maps:get(<<"parent_channel">>, Context))).
{reply, #{auth_context := #{<<"parent_channel">> := #{<<"id">> := ParentChannelId}}}, _} =
guild_query_handler:handle_call(Request, {self(), make_ref()}, State),
?assertEqual(500, ParentChannelId).
get_guild_state_filters_channels_test() ->
State = test_state(),
@@ -85,7 +85,7 @@ send_passive_updates_uses_current_channel_index_and_message_only_payload_test()
ChannelId = 1497639278555484216,
OldMessageId = 1500000000000000000,
NewMessageId = 1500000000000000001,
Data0 = guild_data_index:normalize_data(#{
Data0 = guild_data_index:normalize_map(#{
<<"guild">> => #{<<"id">> => GuildId, <<"owner_id">> => UserId},
<<"channels">> => [#{<<"id">> => ChannelId, <<"last_message_id">> => OldMessageId}],
<<"members">> => [#{<<"user">> => #{<<"id">> => UserId}, <<"roles">> => []}],
@@ -7,7 +7,7 @@
handle_role_delete_strips_from_members_test() ->
RoleIdToDelete = <<"200">>,
Data = guild_data_index:normalize_data(#{
Data = guild_data_index:normalize_map(#{
<<"roles">> => [
#{<<"id">> => <<"100">>, <<"name">> => <<"Admin">>},
#{<<"id">> => <<"200">>, <<"name">> => <<"Moderator">>}
@@ -138,7 +138,7 @@ handle_role_delete_removes_role_from_list_test() ->
?assertEqual(<<"Keep">>, maps:get(<<"name">>, hd(Roles))).
strip_role_from_members_no_affected_users_test() ->
Data = guild_data_index:normalize_data(#{
Data = guild_data_index:normalize_map(#{
<<"roles">> => [],
<<"members">> => #{
1 => #{<<"user">> => #{<<"id">> => <<"1">>}, <<"roles">> => [<<"100">>]}
@@ -86,7 +86,9 @@ decoded_voice_state({pre_encoded, Bin}) -> json:decode(Bin);
decoded_voice_state(Payload) -> Payload.
map_path_wire_bytes(VoiceState) ->
iolist_to_binary(json:encode(guild_data_wire:payload(VoiceState))).
iolist_to_binary(
json:encode(guild_data_wire:payload(VoiceState), fun json:encode_value/2)
).
confirm_join(ConnId, GuildState) ->
VoiceServerState = (voice_server_state(GuildState))#{
@@ -177,10 +179,11 @@ guild_fanout_pre_encodes_identical_wire_bytes_test() ->
guild_fanout_payload_never_touches_dm_voice_states_test() ->
{pre_encoded, Bin} = dispatch_payload_for(join_voice_state(<<"conn1">>)),
State = #{dm_voice_states => #{}},
EventData = #{} = json:decode(Bin),
?assertEqual(
State,
session_dispatch_guild:update_dm_voice_states_map(
voice_state_update, json:decode(Bin), State
voice_state_update, EventData, State
)
).
@@ -191,9 +194,10 @@ dm_scoped_voice_state_stays_a_map_and_updates_dm_voice_states_test() ->
State = session_dispatch_guild:update_dm_voice_states_map(
voice_state_update, Payload, #{dm_voice_states => #{}}
),
#{dm_voice_states := #{<<"conn1">> := StoredVoiceState}} = State,
?assertEqual(
broadcast_payload(DmVS),
maps:get(<<"conn1">>, maps:get(dm_voice_states, State))
StoredVoiceState
).
dispatch_payload_for(VoiceState) ->
+2 -1
View File
@@ -5,10 +5,11 @@ name = "fluxer-gifs"
version = "0.1.0"
edition.workspace = true
license.workspace = true
publish = false
[dependencies]
fluxer_common = { path = "../fluxer_common" }
anyhow = "1.0.102"
anyhow = "1.0.104"
base64 = "0.22.1"
fluxer-svc = { path = "../fluxer_svc", default-features = false }
hmac = "0.13.0"
+2 -1
View File
@@ -3,6 +3,7 @@ name = "fluxer-media-proxy"
version = "0.1.0"
edition.workspace = true
license.workspace = true
publish = false
build = "build.rs"
[lib]
@@ -19,7 +20,7 @@ harness = false
[dependencies]
fluxer_common = { path = "../fluxer_common" }
anyhow = "1.0.102"
anyhow = "1.0.104"
axum = {version = "0.8.9", default-features = false, features = ["http1", "json", "query", "tokio"]}
base64 = "0.22.1"
bytes = "1.11.1"
+9 -2
View File
@@ -1,6 +1,6 @@
[graph]
targets = ["x86_64-unknown-linux-gnu"]
all-features = false
all-features = true
no-default-features = false
[advisories]
@@ -28,12 +28,19 @@ registries = []
[bans]
multiple-versions = "warn"
wildcards = "deny"
allow-wildcard-paths = true
highlight = "simplest-path"
workspace-default-features = "allow"
external-default-features = "allow"
allow = []
deny = []
skip = []
skip = [
{ crate = "getrandom@0.3.4", reason = "transitive dependency requires the prior randomness API" },
{ crate = "http@0.2.12", reason = "transitive dependency requires the older HTTP API" },
{ crate = "http-body@0.4.6", reason = "transitive dependency requires the older HTTP body API" },
{ crate = "rand_core@0.9.5", reason = "transitive dependency requires the prior randomness API" },
{ crate = "socket2@0.5.10", reason = "transitive dependency requires the older socket API" },
]
skip-tree = []
[sources]
+327 -55
View File
@@ -63,9 +63,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.102"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "arbitrary"
@@ -90,6 +90,24 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "aws-credential-types"
version = "1.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f20799b373a1be121fe3005fba0c2090af9411573878f224df44b42727fcaf7"
dependencies = [
"aws-smithy-async",
"aws-smithy-runtime-api",
"aws-smithy-types",
"zeroize",
]
[[package]]
name = "aws-lc-rs"
version = "1.17.0"
@@ -112,6 +130,112 @@ dependencies = [
"fs_extra",
]
[[package]]
name = "aws-sigv4"
version = "1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bae38512beae0ffee7010fc24e7a8a123c53efdfef42a61e80fda4882418dc71"
dependencies = [
"aws-credential-types",
"aws-smithy-http",
"aws-smithy-runtime-api",
"aws-smithy-types",
"bytes",
"form_urlencoded",
"hex",
"hmac",
"http 0.2.12",
"http 1.4.2",
"percent-encoding",
"sha2",
"time",
"tracing",
]
[[package]]
name = "aws-smithy-async"
version = "1.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc"
dependencies = [
"futures-util",
"pin-project-lite",
"tokio",
]
[[package]]
name = "aws-smithy-http"
version = "0.63.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231"
dependencies = [
"aws-smithy-runtime-api",
"aws-smithy-types",
"bytes",
"bytes-utils",
"futures-core",
"futures-util",
"http 1.4.2",
"http-body 1.0.1",
"http-body-util",
"percent-encoding",
"pin-project-lite",
"pin-utils",
"tracing",
]
[[package]]
name = "aws-smithy-runtime-api"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9db177daa6ba8afb9ee1aefcf548c907abcf52065e394ee11a92780057fe0e8c"
dependencies = [
"aws-smithy-async",
"aws-smithy-runtime-api-macros",
"aws-smithy-types",
"bytes",
"http 0.2.12",
"http 1.4.2",
"pin-project-lite",
"tokio",
"tracing",
"zeroize",
]
[[package]]
name = "aws-smithy-runtime-api-macros"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d7396fd9500589e62e460e987ecb671bad374934e55ec3b5f498cc7a8a8a7b7"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "aws-smithy-types"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32b42fcf341259d85ca10fac9a2f6448a8ec691c6955a18e45bc3b71a85fab85"
dependencies = [
"base64-simd",
"bytes",
"bytes-utils",
"http 0.2.12",
"http 1.4.2",
"http-body 0.4.6",
"http-body 1.0.1",
"http-body-util",
"itoa",
"num-integer",
"pin-project-lite",
"pin-utils",
"ryu",
"serde",
"time",
]
[[package]]
name = "axum"
version = "0.8.9"
@@ -122,8 +246,8 @@ dependencies = [
"bytes",
"form_urlencoded",
"futures-util",
"http",
"http-body",
"http 1.4.2",
"http-body 1.0.1",
"http-body-util",
"hyper",
"hyper-util",
@@ -152,8 +276,8 @@ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"http 1.4.2",
"http-body 1.0.1",
"http-body-util",
"mime",
"pin-project-lite",
@@ -168,6 +292,16 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64-simd"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195"
dependencies = [
"outref",
"vsimd",
]
[[package]]
name = "bitflags"
version = "2.11.1"
@@ -195,6 +329,16 @@ version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "bytes-utils"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35"
dependencies = [
"bytes",
"either",
]
[[package]]
name = "cc"
version = "1.2.63"
@@ -221,9 +365,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chacha20"
version = "0.10.0"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
dependencies = [
"cfg-if",
"cpufeatures",
@@ -343,9 +487,9 @@ dependencies = [
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
version = "0.9.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
dependencies = [
"crossbeam-utils",
]
@@ -375,10 +519,10 @@ dependencies = [
]
[[package]]
name = "data-url"
version = "0.3.2"
name = "deranged"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
[[package]]
name = "digest"
@@ -409,6 +553,12 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "either"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -447,10 +597,11 @@ dependencies = [
"bytes",
"cc",
"clap",
"fluxer_common",
"hex",
"hmac",
"http",
"http-body",
"http 1.4.2",
"http-body 1.0.1",
"http-body-util",
"libc",
"moka",
@@ -465,7 +616,6 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"svg-hush",
"tempfile",
"thiserror",
"tokio",
@@ -484,6 +634,33 @@ dependencies = [
"libfuzzer-sys",
]
[[package]]
name = "fluxer_common"
version = "0.1.0"
dependencies = [
"anyhow",
"aws-credential-types",
"aws-sigv4",
"axum",
"base64",
"hmac",
"maxminddb",
"moka",
"reqwest",
"serde_json",
"sha2",
"thiserror",
"time",
"tracing",
"urlencoding",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.5"
@@ -670,6 +847,17 @@ dependencies = [
"digest",
]
[[package]]
name = "http"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "http"
version = "1.4.2"
@@ -680,6 +868,17 @@ dependencies = [
"itoa",
]
[[package]]
name = "http-body"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
dependencies = [
"bytes",
"http 0.2.12",
"pin-project-lite",
]
[[package]]
name = "http-body"
version = "1.0.1"
@@ -687,7 +886,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http",
"http 1.4.2",
]
[[package]]
@@ -698,8 +897,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"http 1.4.2",
"http-body 1.0.1",
"pin-project-lite",
]
@@ -734,8 +933,8 @@ dependencies = [
"bytes",
"futures-channel",
"futures-core",
"http",
"http-body",
"http 1.4.2",
"http-body 1.0.1",
"httparse",
"httpdate",
"itoa",
@@ -751,7 +950,7 @@ version = "0.27.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
dependencies = [
"http",
"http 1.4.2",
"hyper",
"hyper-util",
"rustls",
@@ -770,8 +969,8 @@ dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http",
"http-body",
"http 1.4.2",
"http-body 1.0.1",
"hyper",
"ipnet",
"libc",
@@ -910,6 +1109,12 @@ version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
[[package]]
name = "ipnetwork"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
@@ -1069,6 +1274,19 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "maxminddb"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf6467428ad055b71e588bcedcbaf2ff605b3251deb0c52be4a04b674c546dd"
dependencies = [
"ipnetwork",
"log",
"memchr",
"serde",
"thiserror",
]
[[package]]
name = "memchr"
version = "2.8.0"
@@ -1118,6 +1336,30 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "num-conv"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
[[package]]
name = "num-integer"
version = "0.1.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
dependencies = [
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.4"
@@ -1136,6 +1378,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "outref"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
[[package]]
name = "parking_lot"
version = "0.12.5"
@@ -1198,6 +1446,12 @@ dependencies = [
"zerovec",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@@ -1226,12 +1480,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "quick-error"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quinn"
version = "0.11.9"
@@ -1399,10 +1647,11 @@ checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
dependencies = [
"base64",
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"http",
"http-body",
"http 1.4.2",
"http-body 1.0.1",
"http-body-util",
"hyper",
"hyper-rustls",
@@ -1439,7 +1688,7 @@ checksum = "07bc3f1384cffa4f274dad2d4ddd73aed32fed8f786d96c6be8aa4e5fd3c3b58"
dependencies = [
"anyhow",
"async-trait",
"http",
"http 1.4.2",
"reqwest",
"thiserror",
"tower-service",
@@ -1455,7 +1704,7 @@ dependencies = [
"async-trait",
"futures",
"getrandom 0.2.17",
"http",
"http 1.4.2",
"hyper",
"reqwest",
"reqwest-middleware",
@@ -1815,19 +2064,6 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "svg-hush"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "929223e80cdcec0482207576ea09692dd71b2b559057fc172e292ecec9a97559"
dependencies = [
"base64",
"data-url",
"quick-error",
"url",
"xml",
]
[[package]]
name = "syn"
version = "2.0.117"
@@ -1907,6 +2143,36 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "time"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
dependencies = [
"deranged",
"num-conv",
"powerfmt",
"serde_core",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
[[package]]
name = "time-macros"
version = "0.2.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tinystr"
version = "0.8.3"
@@ -2006,8 +2272,8 @@ dependencies = [
"bitflags",
"bytes",
"futures-util",
"http",
"http-body",
"http 1.4.2",
"http-body 1.0.1",
"pin-project-lite",
"tower",
"tower-layer",
@@ -2143,6 +2409,12 @@ dependencies = [
"serde",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "utf8_iter"
version = "1.0.4"
@@ -2172,6 +2444,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "vsimd"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
[[package]]
name = "walkdir"
version = "2.5.0"
@@ -2640,12 +2918,6 @@ dependencies = [
"rand_core 0.9.5",
]
[[package]]
name = "xml"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "636f85e5ca6488e96401b61eb7de54f4e44755c988af0f52cf90230c312a1a89"
[[package]]
name = "yoke"
version = "0.8.2"
@@ -7,11 +7,12 @@ use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
if let Ok(text) = std::str::from_utf8(data) {
let path = external_path::build_external_media_proxy_path(text);
let _ = external_path::reconstruct_original_url(&path);
let sig = signing::create_signature(&path, b"fuzz-secret");
let _ = signing::verify_signature(&path, &sig, b"fuzz-secret");
let _ = signing::verify_signature(text, &sig, b"other-secret");
if let Ok(path) = external_path::build_external_media_proxy_path(text) {
let _ = external_path::reconstruct_original_url(&path);
let sig = signing::create_signature(&path, b"fuzz-secret");
let _ = signing::verify_signature(&path, &sig, b"fuzz-secret");
let _ = signing::verify_signature(text, &sig, b"other-secret");
}
let _ = external_path::reconstruct_original_url(text);
let _ = external_path::percent_decode(text, true);
let _ = external_path::percent_decode(text, false);
-11
View File
@@ -1946,17 +1946,6 @@ static inline float fluxer_pq_oetf(float l) {
return powf(num / den, m2);
}
static inline float fluxer_hlg_oetf_display(float dl) {
if (dl <= 0.0f) return 0.0f;
if (dl >= 1.0f) dl = 1.0f;
float scene = powf(dl, 1.0f / 1.2f);
if (scene <= 1.0f / 12.0f) return sqrtf(3.0f * scene);
const float a = 0.17883277f;
const float b = 0.28466892f;
const float c = 0.55991073f;
return a * logf(12.0f * scene - b) + c;
}
static inline float fluxer_srgb_oetf(float e) {
if (e <= 0.0f) return 0.0f;
if (e >= 1.0f) return 1.0f;
+2 -1
View File
@@ -3,10 +3,11 @@ name = "fluxer-messages"
version = "0.1.0"
edition.workspace = true
license.workspace = true
publish = false
[dependencies]
fluxer_common = { path = "../fluxer_common" }
anyhow = "1.0.102"
anyhow = "1.0.104"
base64 = "0.22.1"
chrono = { version = "0.4.45", default-features = false, features = ["serde"] }
fluxer-svc = { path = "../fluxer_svc" }
+3 -3
View File
@@ -90,7 +90,7 @@ pub struct MessagesShard {
enum MessagesStorage {
Postgres(PostgresMessagesStorage),
#[cfg(feature = "scylla")]
Scylla(ScyllaMessagesStorage),
Scylla(Box<ScyllaMessagesStorage>),
}
#[derive(Clone)]
@@ -339,7 +339,7 @@ impl MessagesShard {
.await?;
Ok(Self {
storage: MessagesStorage::Scylla(ScyllaMessagesStorage {
storage: MessagesStorage::Scylla(Box::new(ScyllaMessagesStorage {
db,
stmt_get_by_id,
stmt_get_latest,
@@ -352,7 +352,7 @@ impl MessagesShard {
stmt_get_attachment_decay,
stmt_get_attachment_decay_many,
stmt_delete_message,
}),
})),
transport,
})
}
+2 -1
View File
@@ -3,9 +3,10 @@ name = "fluxer-snowflakes"
version = "0.1.0"
edition.workspace = true
license.workspace = true
publish = false
[dependencies]
anyhow = "1.0.102"
anyhow = "1.0.104"
fluxer-svc = { path = "../fluxer_svc", default-features = false }
rmp-serde = "1.3"
serde = { version = "1.0.228", features = ["derive"] }
+1 -2
View File
@@ -5,7 +5,7 @@ edition.workspace = true
license.workspace = true
[dependencies]
anyhow = "1.0.102"
anyhow = "1.0.104"
async-nats = "0.49"
async-trait = "0.1"
axum = { version = "0.8.9", default-features = false, features = ["http1", "tokio"] }
@@ -17,7 +17,6 @@ libc = "0.2"
moka = { version = "0.12.15", features = ["future"] }
rmp-serde = "1.3"
rustls = { version = "0.23", features = ["ring"] }
rustls-pemfile = "2.2"
scylla = { version = "1.6.0", features = ["chrono-04"], optional = true }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.150"
+5 -4
View File
@@ -4,9 +4,11 @@ use crate::config::ServiceConfig;
use anyhow::Context;
use chrono::{DateTime, Utc};
use deadpool_postgres::{Client, Manager, Pool, Runtime, Transaction};
use rustls::RootCertStore;
use rustls::{
RootCertStore,
pki_types::{CertificateDer, pem::PemObject},
};
use serde_json::{Map, Number, Value};
use std::io::Cursor;
use std::str::FromStr;
use tokio_postgres::{Config as PgConfig, Row, Statement, config::SslMode, types::ToSql};
use tokio_postgres_rustls::MakeRustlsConnect;
@@ -99,9 +101,8 @@ fn build_tls_connector(ca_pem: Option<&str>) -> anyhow::Result<MakeRustlsConnect
let _ = rustls::crypto::ring::default_provider().install_default();
if let Some(ca_pem) = ca_pem.filter(|value| !value.trim().is_empty()) {
let normalized = ca_pem.replace("\\n", "\n");
let mut reader = Cursor::new(normalized.as_bytes());
let mut roots = RootCertStore::empty();
for cert in rustls_pemfile::certs(&mut reader) {
for cert in CertificateDer::pem_slice_iter(normalized.as_bytes()) {
roots.add(cert.context("failed to parse FLUXER_POSTGRES_SSL_CA certificate")?)?;
}
if roots.is_empty() {

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