mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-08 03:32:27 +09:00
Compare commits
63
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eedfd9275f | ||
|
|
416af4bec4 | ||
|
|
108d282ddd | ||
|
|
a6103244b0 | ||
|
|
38935c83c5 | ||
|
|
c157ab5752 | ||
|
|
574a93257c | ||
|
|
ba7d8781cf | ||
|
|
3256af8d92 | ||
|
|
86043212f2 | ||
|
|
5d85e88532 | ||
|
|
e2abfd476a | ||
|
|
487febac8e | ||
|
|
a3454e8245 | ||
|
|
bf7567b768 | ||
|
|
ac3450ab32 | ||
|
|
5d034becb8 | ||
|
|
f9397d0db9 | ||
|
|
9005139dc8 | ||
|
|
d93604afa2 | ||
|
|
c4f0b2ece0 | ||
|
|
98a42f612b | ||
|
|
cc75e1318d | ||
|
|
9027cbdf3e | ||
|
|
2119e10ed5 | ||
|
|
87f3eb3c81 | ||
|
|
f9bb8bd585 | ||
|
|
bc47a724af | ||
|
|
f32356801d | ||
|
|
efd677f32b | ||
|
|
3cec27ba57 | ||
|
|
1f810ba04d | ||
|
|
522cf08e61 | ||
|
|
025c01ab13 | ||
|
|
dc41b53d60 | ||
|
|
3b552e00ef | ||
|
|
56e04e7b53 | ||
|
|
deac653a9e | ||
|
|
ed9528834d | ||
|
|
4cecbf1f43 | ||
|
|
b019f4a91f | ||
|
|
34b6ecfbd2 | ||
|
|
4ef9c4c65b | ||
|
|
3276039e41 | ||
|
|
03d1354562 | ||
|
|
964845d7a7 | ||
|
|
3bc5dd8e0f | ||
|
|
17292fd6a5 | ||
|
|
f753659899 | ||
|
|
7412ec3395 | ||
|
|
570c8776c4 | ||
|
|
910db6734b | ||
|
|
9e614026d7 | ||
|
|
b38e7c6433 | ||
|
|
83c8e91955 | ||
|
|
0532dd0440 | ||
|
|
a08615e306 | ||
|
|
f4c5fee17e | ||
|
|
c5aaf65a10 | ||
|
|
951e39da3d | ||
|
|
b693d84d2b | ||
|
|
9bbf6c513b | ||
|
|
50cec92738 |
@@ -1,14 +1,14 @@
|
||||
FROM chrislusf/seaweedfs:4.31 AS seaweedfs
|
||||
FROM chrislusf/seaweedfs:4.47 AS seaweedfs
|
||||
|
||||
FROM erlang:28.5.0.1
|
||||
FROM erlang:28.5.0.6
|
||||
|
||||
ARG USERNAME=vscode
|
||||
ARG USER_UID=1000
|
||||
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.123
|
||||
ARG NODE_MAJOR=26
|
||||
ARG ELP_VERSION=2026-08-10
|
||||
ARG PNPM_VERSION=11.27.0
|
||||
ARG WASM_BINDGEN_VERSION=0.2.128
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
@@ -131,7 +131,8 @@ RUN apt-get update \
|
||||
RUN curl --retry 5 --retry-delay 2 --retry-all-errors -fsSL https://deb.nodesource.com/setup_${NODE_MAJOR}.x | bash - \
|
||||
&& apt-get install -y --no-install-recommends nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& corepack enable
|
||||
&& npm install -g "pnpm@${PNPM_VERSION}" \
|
||||
&& pnpm --version
|
||||
|
||||
RUN python3 -m pip install --break-system-packages --no-cache-dir awscli
|
||||
|
||||
@@ -167,7 +168,7 @@ RUN ARCH="$(dpkg --print-architecture)" \
|
||||
arm64) ELP_ARCH="aarch64" ;; \
|
||||
*) echo "Unsupported architecture for ELP: $ARCH" >&2; exit 1 ;; \
|
||||
esac \
|
||||
&& curl --retry 5 --retry-delay 2 --retry-all-errors -fsSL "https://github.com/WhatsApp/erlang-language-platform/releases/download/${ELP_VERSION}/elp-linux-${ELP_ARCH}-unknown-linux-gnu-otp-28.tar.gz" -o /tmp/elp.tgz \
|
||||
&& curl --retry 5 --retry-delay 2 --retry-all-errors -fsSL "https://github.com/WhatsApp/erlang-language-platform/releases/download/${ELP_VERSION}/elp-linux-${ELP_ARCH}-unknown-linux-gnu-otp-28.5.tar.gz" -o /tmp/elp.tgz \
|
||||
&& tar -C /usr/local/bin -xzf /tmp/elp.tgz elp \
|
||||
&& chmod +x /usr/local/bin/elp \
|
||||
&& rm /tmp/elp.tgz
|
||||
@@ -194,7 +195,4 @@ RUN curl --retry 5 --retry-delay 2 --retry-all-errors -fsSL https://sh.rustup.rs
|
||||
&& cargo install wasm-bindgen-cli --version "${WASM_BINDGEN_VERSION}" --locked \
|
||||
&& rm -rf "/home/${USERNAME}/.cargo/registry" "/home/${USERNAME}/.cargo/git"
|
||||
|
||||
RUN corepack prepare "pnpm@${PNPM_VERSION}" --activate \
|
||||
&& pnpm --version
|
||||
|
||||
WORKDIR /workspaces/fluxer
|
||||
|
||||
@@ -9,7 +9,7 @@ services:
|
||||
init: true
|
||||
environment:
|
||||
DOCKER_HOST: unix:///var/run/docker.sock
|
||||
npm_config_store_dir: /home/vscode/.local/share/pnpm/store
|
||||
pnpm_config_store_dir: /home/vscode/.local/share/pnpm/store
|
||||
FLUXER_PUBLIC_PORT: "${FLUXER_DEV_PROXY_PORT:-8088}"
|
||||
FLUXER_PUBLIC_URL: "http://localhost:${FLUXER_DEV_PROXY_PORT:-8088}"
|
||||
FLUXER_API_ENDPOINT: "http://localhost:${FLUXER_DEV_PROXY_PORT:-8088}/api"
|
||||
@@ -292,13 +292,13 @@ services:
|
||||
start_period: 5s
|
||||
|
||||
valkey:
|
||||
image: valkey/valkey:8.1.7-alpine
|
||||
image: valkey/valkey:9.1.2-alpine
|
||||
command: ["valkey-server", "--save", "", "--appendonly", "no"]
|
||||
ports:
|
||||
- "127.0.0.1:${FLUXER_DEV_VALKEY_PORT:-6379}:6379"
|
||||
|
||||
nats:
|
||||
image: nats:2.14.2-alpine
|
||||
image: nats:2.14.7-alpine
|
||||
command: ["-js", "-sd", "/data", "-m", "8222"]
|
||||
volumes:
|
||||
- nats-data:/data
|
||||
@@ -321,9 +321,10 @@ services:
|
||||
- "127.0.0.1:${FLUXER_DEV_LIVEKIT_UDP_PORT:-7882}:${FLUXER_DEV_LIVEKIT_UDP_PORT:-7882}/udp"
|
||||
|
||||
meilisearch:
|
||||
image: getmeili/meilisearch:v1.12
|
||||
image: getmeili/meilisearch:v1.53
|
||||
environment:
|
||||
MEILI_NO_ANALYTICS: "true"
|
||||
MEILI_UPGRADE_DB: "true"
|
||||
MEILI_MASTER_KEY: fluxer-dev-meilisearch
|
||||
volumes:
|
||||
- meilisearch-data:/meili_data
|
||||
@@ -337,7 +338,7 @@ services:
|
||||
start_period: 5s
|
||||
|
||||
mailpit:
|
||||
image: axllent/mailpit:v1.30
|
||||
image: axllent/mailpit:v1.31
|
||||
environment:
|
||||
MP_DATABASE: /data/mailpit.db
|
||||
MP_MAX_MESSAGES: 5000
|
||||
|
||||
@@ -28,9 +28,6 @@ f:media_proxy:
|
||||
f:messages:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: fluxer_messages/**/*
|
||||
f:recon:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: fluxer_recon/**/*
|
||||
f:snowflakes:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: fluxer_snowflakes/**/*
|
||||
|
||||
@@ -58,13 +58,13 @@ jobs:
|
||||
outputs:
|
||||
build_version: ${{ steps.vars.outputs.build_version }}
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
- name: Create token
|
||||
id: create-token
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
|
||||
@@ -101,19 +101,19 @@ jobs:
|
||||
- platform: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
|
||||
- name: resolve source date
|
||||
id: source
|
||||
run: echo "date=$(TZ=UTC git log -1 --no-show-signature --pretty=%cd --date=format-local:%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
|
||||
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
|
||||
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
- uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf
|
||||
- uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc
|
||||
with:
|
||||
context: ${{ inputs.context }}
|
||||
file: ${{ inputs.dockerfile }}
|
||||
@@ -141,15 +141,15 @@ jobs:
|
||||
contents: write
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
|
||||
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
|
||||
toolchain: "1.98.1"
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
|
||||
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -43,13 +43,13 @@ jobs:
|
||||
outputs:
|
||||
build_version: ${{ steps.vars.outputs.build_version }}
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
- name: set variables
|
||||
id: vars
|
||||
run: >-
|
||||
@@ -71,20 +71,19 @@ jobs:
|
||||
BUILD_VERSION: ${{ needs.meta.outputs.build_version }}
|
||||
PUBLIC_ASSET_BASE_URL: ""
|
||||
BUNDLE_LOCAL_ASSETS: "true"
|
||||
FLUXER_APP_PROXY_TIME_FREEZE_ENABLED: "false"
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
- name: prepare docker config
|
||||
run: >-
|
||||
tools/ci/run.sh build-app-proxy
|
||||
--step prepare_docker_config
|
||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
|
||||
- name: configure ghcr auth
|
||||
env:
|
||||
GHCR_USERNAME: ${{ github.actor }}
|
||||
@@ -131,19 +130,19 @@ jobs:
|
||||
- platform: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
|
||||
- name: resolve source date
|
||||
id: source
|
||||
run: echo "date=$(TZ=UTC git log -1 --no-show-signature --pretty=%cd --date=format-local:%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
|
||||
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
|
||||
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf
|
||||
- uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc
|
||||
with:
|
||||
context: .
|
||||
file: fluxer_app_proxy/Dockerfile
|
||||
@@ -155,7 +154,6 @@ jobs:
|
||||
BUILD_VERSION=${{ needs.meta.outputs.build_version }}
|
||||
SOURCE_SHA=${{ github.sha }}
|
||||
SOURCE_DATE=${{ steps.source.outputs.date }}
|
||||
FLUXER_APP_PROXY_TIME_FREEZE_ENABLED=false
|
||||
APP_ASSETS_REF=ghcr.io/${{ env.GHCR_OWNER }}/fluxer-app-proxy-self-hosted:${{ needs.meta.outputs.build_version }}-assets
|
||||
APP_ASSETS_PLATFORM=linux/amd64
|
||||
cache-from: type=registry,ref=ghcr.io/${{ env.GHCR_OWNER }}/fluxer-app-proxy-self-hosted:buildcache-${{ matrix.platform }}
|
||||
@@ -173,15 +171,15 @@ jobs:
|
||||
contents: write
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
|
||||
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
|
||||
toolchain: "1.98.1"
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
|
||||
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -43,13 +43,13 @@ jobs:
|
||||
outputs:
|
||||
build_version: ${{ steps.vars.outputs.build_version }}
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
- name: set variables
|
||||
id: vars
|
||||
run: >-
|
||||
@@ -67,18 +67,18 @@ jobs:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
- name: prepare docker config
|
||||
run: >-
|
||||
tools/ci/run.sh build-app-proxy
|
||||
--step prepare_docker_config
|
||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
|
||||
- name: configure ghcr auth
|
||||
env:
|
||||
GHCR_USERNAME: ${{ github.actor }}
|
||||
@@ -131,13 +131,13 @@ jobs:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
- name: resolve source date
|
||||
id: source
|
||||
run: echo "date=$(TZ=UTC git log -1 --no-show-signature --pretty=%cd --date=format-local:%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
||||
@@ -145,7 +145,7 @@ jobs:
|
||||
run: >-
|
||||
tools/ci/run.sh build-app-proxy
|
||||
--step prepare_docker_config
|
||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
|
||||
- name: configure ghcr auth
|
||||
env:
|
||||
GHCR_USERNAME: ${{ github.actor }}
|
||||
@@ -178,19 +178,19 @@ jobs:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
|
||||
- name: resolve source date
|
||||
id: source
|
||||
run: echo "date=$(TZ=UTC git log -1 --no-show-signature --pretty=%cd --date=format-local:%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
|
||||
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
|
||||
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf
|
||||
- uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc
|
||||
with:
|
||||
context: .
|
||||
file: fluxer_app_proxy/Dockerfile
|
||||
@@ -219,15 +219,15 @@ jobs:
|
||||
contents: write
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
|
||||
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
|
||||
toolchain: "1.98.1"
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
|
||||
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -11,11 +11,6 @@ on:
|
||||
- stable
|
||||
- canary
|
||||
default: stable
|
||||
test_build:
|
||||
description: Stash artifacts under desktop-test/ instead of desktop/ (API will not pick these up as a release).
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
build_version:
|
||||
description: Explicit Fluxer CalVer build version (YYYY.MDD.MICRO, UTC HHMMSS without leading zeroes) to use instead of automatic UTC clock allocation.
|
||||
required: false
|
||||
@@ -32,13 +27,12 @@ permissions:
|
||||
actions: read
|
||||
|
||||
concurrency:
|
||||
group: desktop-${{ inputs.channel }}-${{ inputs.test_build && 'test' || 'release' }}
|
||||
group: desktop-${{ inputs.channel }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CHANNEL: ${{ inputs.channel }}
|
||||
BUILD_CHANNEL: ${{ inputs.channel == 'canary' && 'canary' || 'stable' }}
|
||||
TEST_BUILD: ${{ inputs.test_build && 'true' || 'false' }}
|
||||
|
||||
jobs:
|
||||
meta:
|
||||
@@ -53,19 +47,17 @@ jobs:
|
||||
pub_date: ${{ steps.meta.outputs.pub_date }}
|
||||
channel: ${{ steps.meta.outputs.channel }}
|
||||
build_channel: ${{ steps.meta.outputs.build_channel }}
|
||||
test_build: ${{ steps.meta.outputs.test_build }}
|
||||
s3_prefix: ${{ steps.meta.outputs.s3_prefix }}
|
||||
source_sha: ${{ steps.meta.outputs.source_sha }}
|
||||
steps:
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
ref: main
|
||||
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
|
||||
- name: Create token
|
||||
id: create-token
|
||||
@@ -85,7 +77,6 @@ jobs:
|
||||
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
|
||||
--step set_metadata
|
||||
--channel "${{ inputs.channel }}"
|
||||
--test-build "${{ inputs.test_build }}"
|
||||
|
||||
matrix:
|
||||
name: Resolve build matrix
|
||||
@@ -98,12 +89,12 @@ jobs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
|
||||
- name: Build platform matrix
|
||||
id: set-matrix
|
||||
@@ -113,7 +104,7 @@ jobs:
|
||||
--skip-targets "${{ inputs.skip_targets }}"
|
||||
|
||||
build:
|
||||
name: Build ${{ matrix.platform }} (${{ matrix.arch }}, ${{ matrix.desktop_variant }})
|
||||
name: Build ${{ matrix.platform }} (${{ matrix.arch }})
|
||||
needs:
|
||||
- meta
|
||||
- matrix
|
||||
@@ -137,41 +128,34 @@ jobs:
|
||||
PUBLIC_BUILD_VERSION: ${{ needs.meta.outputs.version }}
|
||||
PUB_DATE: ${{ needs.meta.outputs.pub_date }}
|
||||
SOURCE_SHA: ${{ needs.meta.outputs.source_sha }}
|
||||
S3_DESKTOP_PREFIX: ${{ needs.meta.outputs.s3_prefix }}
|
||||
DESKTOP_HANDOFF_PREFIX: _handoff/desktop/${{ needs.meta.outputs.build_channel }}/${{ needs.meta.outputs.version }}/${{ needs.meta.outputs.source_sha }}
|
||||
S3_ENDPOINT: ${{ vars.DOWNLOADS_S3_ENDPOINT }}
|
||||
S3_BUCKET: ${{ vars.DOWNLOADS_S3_BUCKET }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.DOWNLOADS_AWS_ACCESS_KEY_ID || secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.DOWNLOADS_AWS_SECRET_ACCESS_KEY || secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
DESKTOP_PLATFORM: ${{ matrix.platform }}
|
||||
DESKTOP_ARCH: ${{ matrix.arch }}
|
||||
DESKTOP_VARIANT: ${{ matrix.desktop_variant }}
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
ARCH: ${{ matrix.arch }}
|
||||
ELECTRON_ARCH: ${{ matrix.electron_arch }}
|
||||
steps:
|
||||
- name: Checkout CI helpers
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
ref: ${{ needs.meta.outputs.source_sha }}
|
||||
path: _ci
|
||||
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
ref: ${{ needs.meta.outputs.source_sha }}
|
||||
path: source
|
||||
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
|
||||
- name: Set up Python (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
|
||||
with:
|
||||
python-version: "3.13"
|
||||
python-version: "3.14"
|
||||
|
||||
- name: Ensure python3 command (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
@@ -196,14 +180,14 @@ jobs:
|
||||
--step set_workdir_unix
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 26
|
||||
|
||||
- name: Set up pnpm via corepack
|
||||
- name: Set up pnpm
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
|
||||
--step setup_pnpm_corepack
|
||||
--step setup_pnpm
|
||||
|
||||
- name: Resolve pnpm store path (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
@@ -247,9 +231,9 @@ jobs:
|
||||
|
||||
- name: Set up Rust toolchain (Unix)
|
||||
if: matrix.platform != 'windows'
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
targets: ${{ matrix.platform == 'macos' && 'aarch64-apple-darwin,x86_64-apple-darwin' || (matrix.arch == 'arm64' && 'aarch64-unknown-linux-gnu' || 'x86_64-unknown-linux-gnu') }}
|
||||
|
||||
- name: Install MSVC ARM64 build tools
|
||||
@@ -260,7 +244,7 @@ jobs:
|
||||
|
||||
- name: Set up MSVC env (Windows)
|
||||
if: matrix.platform == 'windows'
|
||||
uses: TheMrMilchmann/setup-msvc-dev@79dac248aac9d0059f86eae9d8b5bfab4e95e97c
|
||||
uses: TheMrMilchmann/setup-msvc-dev@368ef7d1ee4d1171b31d4a7f67f4d954f903f5a9
|
||||
with:
|
||||
arch: ${{ matrix.arch == 'arm64' && 'amd64_arm64' || 'amd64' }}
|
||||
|
||||
@@ -302,9 +286,9 @@ jobs:
|
||||
|
||||
- name: Set up .NET SDK (Windows)
|
||||
if: matrix.platform == 'windows'
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1
|
||||
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68
|
||||
with:
|
||||
dotnet-version: "8.0.x"
|
||||
dotnet-version: "10.0.x"
|
||||
|
||||
- name: Install Velopack CLI
|
||||
if: matrix.platform == 'windows'
|
||||
@@ -363,7 +347,7 @@ jobs:
|
||||
|
||||
- name: Azure login for Artifact Signing
|
||||
if: matrix.platform == 'windows'
|
||||
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43
|
||||
uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
@@ -477,6 +461,12 @@ jobs:
|
||||
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
|
||||
--step prepare_artifacts_unix
|
||||
|
||||
- name: Build AppImage update feed (Linux)
|
||||
if: matrix.platform == 'linux'
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
|
||||
--step build_appimage_update_feed
|
||||
|
||||
- name: Normalize updater YAML (macOS)
|
||||
if: matrix.platform == 'macos'
|
||||
run: >-
|
||||
@@ -495,13 +485,23 @@ jobs:
|
||||
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
|
||||
--step generate_checksums_windows
|
||||
|
||||
- name: Upload artifacts to S3 handoff
|
||||
- name: Stage build artifacts
|
||||
id: handoff
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
|
||||
--step upload_handoff
|
||||
--step stage_handoff
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: ${{ steps.handoff.outputs.artifact_name }}
|
||||
path: upload_staging
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
compression-level: 0
|
||||
|
||||
upload:
|
||||
name: Upload to S3
|
||||
name: Assemble desktop release assets
|
||||
if: ${{ !cancelled() && needs.build.result == 'success' }}
|
||||
needs:
|
||||
- meta
|
||||
@@ -520,34 +520,26 @@ jobs:
|
||||
BUILD_VERSION: ${{ needs.meta.outputs.version }}
|
||||
PUBLIC_BUILD_VERSION: ${{ needs.meta.outputs.version }}
|
||||
PUB_DATE: ${{ needs.meta.outputs.pub_date }}
|
||||
TEST_BUILD: ${{ needs.meta.outputs.test_build }}
|
||||
SOURCE_SHA: ${{ needs.meta.outputs.source_sha }}
|
||||
S3_DESKTOP_PREFIX: ${{ needs.meta.outputs.s3_prefix }}
|
||||
DESKTOP_HANDOFF_PREFIX: _handoff/desktop/${{ needs.meta.outputs.build_channel }}/${{ needs.meta.outputs.version }}/${{ needs.meta.outputs.source_sha }}
|
||||
DESKTOP_RELEASE_ASSETS_PREFIX: _handoff/desktop-release-assets/${{ needs.meta.outputs.build_channel }}/${{ needs.meta.outputs.version }}/${{ needs.meta.outputs.source_sha }}
|
||||
DESKTOP_METADATA_PREFIX: _handoff/desktop-metadata/${{ needs.meta.outputs.build_channel }}/${{ needs.meta.outputs.version }}/${{ needs.meta.outputs.source_sha }}
|
||||
S3_ENDPOINT: ${{ vars.DOWNLOADS_S3_ENDPOINT }}
|
||||
S3_BUCKET: ${{ vars.DOWNLOADS_S3_BUCKET }}
|
||||
PUBLIC_DL_BASE: https://api.fluxer.app/dl
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.DOWNLOADS_AWS_ACCESS_KEY_ID || secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.DOWNLOADS_AWS_SECRET_ACCESS_KEY || secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
steps:
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
ref: ${{ needs.meta.outputs.source_sha }}
|
||||
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
|
||||
- name: Download S3 handoff artifacts
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
|
||||
--step download_handoff
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
path: artifacts
|
||||
pattern: fluxer-desktop-${{ needs.meta.outputs.build_channel }}-*
|
||||
|
||||
- name: Build S3 payload layout (+ manifest.json)
|
||||
- name: Build payload layout (+ manifest.json)
|
||||
env:
|
||||
VERSION: ${{ needs.meta.outputs.version }}
|
||||
PUB_DATE: ${{ needs.meta.outputs.pub_date }}
|
||||
@@ -556,42 +548,27 @@ jobs:
|
||||
--step build_payload
|
||||
|
||||
- name: Prepare GitHub release assets
|
||||
if: needs.meta.outputs.test_build != 'true'
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
|
||||
--step prepare_release_assets
|
||||
|
||||
- name: Publish GitHub release descriptor
|
||||
if: needs.meta.outputs.test_build != 'true'
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
|
||||
--step publish_release_descriptor
|
||||
|
||||
- name: Upload payload to S3
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
|
||||
--step upload_payload
|
||||
|
||||
- name: Upload GitHub release asset handoff
|
||||
if: needs.meta.outputs.test_build != 'true'
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
|
||||
--step upload_release_assets
|
||||
- name: Upload GitHub release assets
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: fluxer-desktop-release-assets
|
||||
path: release_assets
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
compression-level: 0
|
||||
|
||||
- name: Build summary
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
|
||||
--step build_summary
|
||||
|
||||
- name: Cleanup S3 handoff
|
||||
if: ${{ success() }}
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
|
||||
--step cleanup_handoff
|
||||
|
||||
publish_release:
|
||||
name: Publish GitHub desktop release
|
||||
if: ${{ !cancelled() && needs.upload.result == 'success' && needs.meta.outputs.test_build != 'true' }}
|
||||
if: ${{ !cancelled() && needs.upload.result == 'success' }}
|
||||
needs:
|
||||
- meta
|
||||
- upload
|
||||
@@ -603,28 +580,22 @@ jobs:
|
||||
env:
|
||||
CHANNEL: ${{ needs.meta.outputs.build_channel }}
|
||||
VERSION: ${{ needs.meta.outputs.version }}
|
||||
S3_DESKTOP_PREFIX: ${{ needs.meta.outputs.s3_prefix }}
|
||||
DESKTOP_RELEASE_ASSETS_PREFIX: _handoff/desktop-release-assets/${{ needs.meta.outputs.build_channel }}/${{ needs.meta.outputs.version }}/${{ needs.meta.outputs.source_sha }}
|
||||
DESKTOP_METADATA_PREFIX: _handoff/desktop-metadata/${{ needs.meta.outputs.build_channel }}/${{ needs.meta.outputs.version }}/${{ needs.meta.outputs.source_sha }}
|
||||
S3_ENDPOINT: ${{ vars.DOWNLOADS_S3_ENDPOINT }}
|
||||
S3_BUCKET: ${{ vars.DOWNLOADS_S3_BUCKET }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.DOWNLOADS_AWS_ACCESS_KEY_ID || secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.DOWNLOADS_AWS_SECRET_ACCESS_KEY || secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
steps:
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
ref: ${{ needs.meta.outputs.source_sha }}
|
||||
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
|
||||
- name: Download GitHub release assets
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
|
||||
--step download_release_assets
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
name: fluxer-desktop-release-assets
|
||||
path: release_assets
|
||||
|
||||
- name: Create token
|
||||
id: create-token
|
||||
@@ -656,15 +627,3 @@ jobs:
|
||||
release_args+=(--prerelease)
|
||||
fi
|
||||
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- "${release_args[@]}"
|
||||
|
||||
- name: Publish GitHub release readiness marker
|
||||
env:
|
||||
SOURCE_SHA: ${{ needs.meta.outputs.source_sha }}
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
|
||||
--step publish_release_marker
|
||||
|
||||
- name: Publish payload metadata to S3
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
|
||||
--step publish_payload_metadata
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
name: build recon
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build-version:
|
||||
description: "Explicit Fluxer CalVer build version (YYYY.MDD.MICRO, UTC HHMMSS without leading zeroes) to use instead of automatic UTC clock allocation"
|
||||
type: string
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
approve:
|
||||
name: approve build release
|
||||
permissions: {}
|
||||
runs-on: ubuntu-24.04
|
||||
environment: builds
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: approved
|
||||
run: echo "Build release approved."
|
||||
|
||||
image:
|
||||
needs: approve
|
||||
uses: ./.github/workflows/_build-image.yaml
|
||||
secrets: inherit
|
||||
with:
|
||||
image: fluxer-recon
|
||||
dockerfile: fluxer_recon/Dockerfile
|
||||
build-version: ${{ inputs['build-version'] }}
|
||||
@@ -19,22 +19,22 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout fluxer
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: '24'
|
||||
node-version: '26'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -35,24 +35,24 @@ jobs:
|
||||
permission-pull-requests: write
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
token: ${{ steps.create-token.outputs.token }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: "24"
|
||||
node-version: "26"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
permission-pull-requests: write
|
||||
|
||||
- name: Checkout Weblate branch
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
token: ${{ steps.create-token.outputs.token }}
|
||||
ref: ${{ env.WEBLATE_BRANCH }}
|
||||
@@ -48,17 +48,17 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: "24"
|
||||
node-version: "26"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
permission-pull-requests: write
|
||||
|
||||
- name: Label pull request
|
||||
uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213
|
||||
uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13
|
||||
with:
|
||||
repo-token: ${{ steps.create-token.outputs.token }}
|
||||
configuration-path: .github/labeller.yaml
|
||||
|
||||
@@ -56,15 +56,15 @@ jobs:
|
||||
contents: write
|
||||
packages: read
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
|
||||
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
|
||||
toolchain: "1.98.1"
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
|
||||
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -28,12 +28,12 @@ jobs:
|
||||
FLUXER_CI_BIN: ${{ github.workspace }}/target/debug/fluxer-ci
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
targets: wasm32-unknown-unknown
|
||||
|
||||
- name: Restore ci helper
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
with:
|
||||
path: target/debug/fluxer-ci
|
||||
key: >-
|
||||
fluxer-ci-bin-${{ runner.os }}-1.93.0-${{ hashFiles('Cargo.lock', 'Cargo.toml',
|
||||
fluxer-ci-bin-${{ runner.os }}-1.98.1-${{ hashFiles('Cargo.lock', 'Cargo.toml',
|
||||
'tools/ci/Cargo.toml', 'tools/ci/src/**', 'tools/ci/templates/**') }}
|
||||
|
||||
- name: Build ci helper
|
||||
@@ -55,16 +55,16 @@ jobs:
|
||||
with:
|
||||
path: target/debug/fluxer-ci
|
||||
key: >-
|
||||
fluxer-ci-bin-${{ runner.os }}-1.93.0-${{ hashFiles('Cargo.lock', 'Cargo.toml',
|
||||
fluxer-ci-bin-${{ runner.os }}-1.98.1-${{ hashFiles('Cargo.lock', 'Cargo.toml',
|
||||
'tools/ci/Cargo.toml', 'tools/ci/src/**', 'tools/ci/templates/**') }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: '24'
|
||||
node-version: '26'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -83,12 +83,12 @@ jobs:
|
||||
PNPM_TEST_WORKSPACE_CONCURRENCY: '2'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
targets: wasm32-unknown-unknown
|
||||
|
||||
- name: Restore ci helper
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
with:
|
||||
path: target/debug/fluxer-ci
|
||||
key: >-
|
||||
fluxer-ci-bin-${{ runner.os }}-1.93.0-${{ hashFiles('Cargo.lock', 'Cargo.toml',
|
||||
fluxer-ci-bin-${{ runner.os }}-1.98.1-${{ hashFiles('Cargo.lock', 'Cargo.toml',
|
||||
'tools/ci/Cargo.toml', 'tools/ci/src/**', 'tools/ci/templates/**') }}
|
||||
|
||||
- name: Build ci helper
|
||||
@@ -105,12 +105,12 @@ jobs:
|
||||
run: cargo build --locked --package fluxer-ci
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: '24'
|
||||
node-version: '26'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
fluxer_app/pkgs/libfluxcore
|
||||
fluxer_app/src/features/messaging/utils/markdown/parser/MarkdownParserWasmBytes.ts
|
||||
key: >-
|
||||
app-wasm-${{ runner.os }}-1.93.0-${{ hashFiles('Cargo.lock', 'tools/ci/src/app_wasm.rs',
|
||||
app-wasm-${{ runner.os }}-1.98.1-${{ hashFiles('Cargo.lock', 'tools/ci/src/app_wasm.rs',
|
||||
'tools/ci/templates/libfluxcore_wrapper.js', 'tools/ci/templates/libfluxcore_wrapper.d.ts',
|
||||
'fluxer_app/rust/libfluxcore/Cargo.toml', 'fluxer_app/rust/libfluxcore/Cargo.lock',
|
||||
'fluxer_app/rust/libfluxcore/.cargo/config.toml', 'fluxer_app/rust/libfluxcore/src/**',
|
||||
@@ -144,7 +144,7 @@ jobs:
|
||||
fluxer_app/pkgs/libfluxcore
|
||||
fluxer_app/src/features/messaging/utils/markdown/parser/MarkdownParserWasmBytes.ts
|
||||
key: >-
|
||||
app-wasm-${{ runner.os }}-1.93.0-${{ hashFiles('Cargo.lock', 'tools/ci/src/app_wasm.rs',
|
||||
app-wasm-${{ runner.os }}-1.98.1-${{ hashFiles('Cargo.lock', 'tools/ci/src/app_wasm.rs',
|
||||
'tools/ci/templates/libfluxcore_wrapper.js', 'tools/ci/templates/libfluxcore_wrapper.d.ts',
|
||||
'fluxer_app/rust/libfluxcore/Cargo.toml', 'fluxer_app/rust/libfluxcore/Cargo.lock',
|
||||
'fluxer_app/rust/libfluxcore/.cargo/config.toml', 'fluxer_app/rust/libfluxcore/src/**',
|
||||
@@ -156,21 +156,21 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
components: clippy, rustfmt
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: '24'
|
||||
node-version: '26'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache cargo
|
||||
@@ -185,7 +185,7 @@ jobs:
|
||||
rust-${{ runner.os }}-${{ hashFiles('fluxer_media_proxy/tools/install-native-deps.sh') }}-
|
||||
|
||||
- name: Install cargo-deny
|
||||
run: cargo install cargo-deny --version 0.19.6 --locked
|
||||
run: cargo install cargo-deny --version 0.20.2 --locked
|
||||
|
||||
- name: Check Rust dependencies
|
||||
run: cargo deny --locked check -D warnings
|
||||
@@ -273,12 +273,12 @@ jobs:
|
||||
FLUXER_CI_BIN: ${{ github.workspace }}/target/debug/fluxer-ci
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
|
||||
- name: Cache cargo (gateway NIFs)
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
|
||||
@@ -294,7 +294,7 @@ jobs:
|
||||
with:
|
||||
path: target/debug/fluxer-ci
|
||||
key: >-
|
||||
fluxer-ci-bin-${{ runner.os }}-1.93.0-${{ hashFiles('Cargo.lock', 'Cargo.toml',
|
||||
fluxer-ci-bin-${{ runner.os }}-1.98.1-${{ hashFiles('Cargo.lock', 'Cargo.toml',
|
||||
'tools/ci/Cargo.toml', 'tools/ci/src/**', 'tools/ci/templates/**') }}
|
||||
|
||||
- name: Build ci helper
|
||||
@@ -305,7 +305,7 @@ jobs:
|
||||
uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124
|
||||
with:
|
||||
otp-version: '28'
|
||||
rebar3-version: '3.24.0'
|
||||
rebar3-version: '3.27.0'
|
||||
|
||||
- name: Restore rebar3 dependencies
|
||||
id: rebar3-cache
|
||||
@@ -317,10 +317,10 @@ jobs:
|
||||
!fluxer_gateway/_build/default/lib/fluxer_gateway/**
|
||||
!fluxer_gateway/_build/test/lib/fluxer_gateway/**
|
||||
key: >-
|
||||
rebar3-${{ runner.os }}-otp28-rebar3.24.0-${{ hashFiles('fluxer_gateway/rebar.lock',
|
||||
rebar3-${{ runner.os }}-otp28-rebar3.27.0-${{ hashFiles('fluxer_gateway/rebar.lock',
|
||||
'fluxer_gateway/rebar.config') }}
|
||||
restore-keys: |
|
||||
rebar3-${{ runner.os }}-otp28-rebar3.24.0-
|
||||
rebar3-${{ runner.os }}-otp28-rebar3.27.0-
|
||||
|
||||
- name: Check formatting
|
||||
run: |
|
||||
@@ -348,7 +348,7 @@ jobs:
|
||||
!fluxer_gateway/_build/default/lib/fluxer_gateway/**
|
||||
!fluxer_gateway/_build/test/lib/fluxer_gateway/**
|
||||
key: >-
|
||||
rebar3-${{ runner.os }}-otp28-rebar3.24.0-${{ hashFiles('fluxer_gateway/rebar.lock',
|
||||
rebar3-${{ runner.os }}-otp28-rebar3.27.0-${{ hashFiles('fluxer_gateway/rebar.lock',
|
||||
'fluxer_gateway/rebar.config') }}
|
||||
|
||||
knip:
|
||||
@@ -358,12 +358,12 @@ jobs:
|
||||
FLUXER_CI_BIN: ${{ github.workspace }}/target/debug/fluxer-ci
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
toolchain: "1.93.0"
|
||||
toolchain: "1.98.1"
|
||||
targets: wasm32-unknown-unknown
|
||||
|
||||
- name: Restore ci helper
|
||||
@@ -372,7 +372,7 @@ jobs:
|
||||
with:
|
||||
path: target/debug/fluxer-ci
|
||||
key: >-
|
||||
fluxer-ci-bin-${{ runner.os }}-1.93.0-${{ hashFiles('Cargo.lock', 'Cargo.toml',
|
||||
fluxer-ci-bin-${{ runner.os }}-1.98.1-${{ hashFiles('Cargo.lock', 'Cargo.toml',
|
||||
'tools/ci/Cargo.toml', 'tools/ci/src/**', 'tools/ci/templates/**') }}
|
||||
|
||||
- name: Build ci helper
|
||||
@@ -380,12 +380,12 @@ jobs:
|
||||
run: cargo build --locked --package fluxer-ci
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: '24'
|
||||
node-version: '26'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -400,7 +400,7 @@ jobs:
|
||||
fluxer_app/pkgs/libfluxcore
|
||||
fluxer_app/src/features/messaging/utils/markdown/parser/MarkdownParserWasmBytes.ts
|
||||
key: >-
|
||||
app-wasm-${{ runner.os }}-1.93.0-${{ hashFiles('Cargo.lock', 'tools/ci/src/app_wasm.rs',
|
||||
app-wasm-${{ runner.os }}-1.98.1-${{ hashFiles('Cargo.lock', 'tools/ci/src/app_wasm.rs',
|
||||
'tools/ci/templates/libfluxcore_wrapper.js', 'tools/ci/templates/libfluxcore_wrapper.d.ts',
|
||||
'fluxer_app/rust/libfluxcore/Cargo.toml', 'fluxer_app/rust/libfluxcore/Cargo.lock',
|
||||
'fluxer_app/rust/libfluxcore/.cargo/config.toml', 'fluxer_app/rust/libfluxcore/src/**',
|
||||
@@ -419,7 +419,7 @@ jobs:
|
||||
fluxer_app/pkgs/libfluxcore
|
||||
fluxer_app/src/features/messaging/utils/markdown/parser/MarkdownParserWasmBytes.ts
|
||||
key: >-
|
||||
app-wasm-${{ runner.os }}-1.93.0-${{ hashFiles('Cargo.lock', 'tools/ci/src/app_wasm.rs',
|
||||
app-wasm-${{ runner.os }}-1.98.1-${{ hashFiles('Cargo.lock', 'tools/ci/src/app_wasm.rs',
|
||||
'tools/ci/templates/libfluxcore_wrapper.js', 'tools/ci/templates/libfluxcore_wrapper.d.ts',
|
||||
'fluxer_app/rust/libfluxcore/Cargo.toml', 'fluxer_app/rust/libfluxcore/Cargo.lock',
|
||||
'fluxer_app/rust/libfluxcore/.cargo/config.toml', 'fluxer_app/rust/libfluxcore/src/**',
|
||||
@@ -431,15 +431,15 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: '24'
|
||||
node-version: '26'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -456,15 +456,15 @@ jobs:
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: '24'
|
||||
node-version: '26'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -490,15 +490,15 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: '24'
|
||||
node-version: '26'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -515,12 +515,12 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
|
||||
with:
|
||||
python-version: "3.13"
|
||||
python-version: "3.14"
|
||||
|
||||
- name: Install font tooling
|
||||
run: python3 -m pip install -r tools/fonts/requirements.txt
|
||||
|
||||
Generated
+828
-874
File diff suppressed because it is too large
Load Diff
@@ -9,13 +9,11 @@ members = [
|
||||
"fluxer_messages",
|
||||
"fluxer_snowflakes",
|
||||
"tools/ci",
|
||||
"tools/content/update-frozen-snapshot",
|
||||
"tools/dev",
|
||||
"tools/i18n_auto",
|
||||
"fluxer_users",
|
||||
"fluxer_unfurl",
|
||||
"packages/markdown_parser/rust",
|
||||
"fluxer_recon",
|
||||
]
|
||||
exclude = [
|
||||
"packages/markdown_parser/rust/fuzz",
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://fluxer.app/donate">
|
||||
<img src="https://img.shields.io/badge/Donate-fluxer.app%2Fdonate-brightgreen" alt="Donate" /></a>
|
||||
<a href="https://fluxer.app/download">
|
||||
<img src="https://img.shields.io/badge/Download-fluxer.app-4641D9" alt="Download" /></a>
|
||||
<a href="https://docs.fluxer.app">
|
||||
<img src="https://img.shields.io/badge/Docs-docs.fluxer.app-blue" alt="Documentation" /></a>
|
||||
<a href="https://fluxer.app/donate">
|
||||
<img src="https://img.shields.io/badge/Donate-fluxer.app%2Fdonate-brightgreen" alt="Donate" /></a>
|
||||
<a href="./LICENSE">
|
||||
<img src="https://img.shields.io/badge/License-AGPLv3-purple" alt="AGPLv3 License" /></a>
|
||||
</p>
|
||||
@@ -19,5 +21,128 @@
|
||||
Fluxer is a free and open source instant messaging and VoIP chat app built for friends, groups, and communities.
|
||||
|
||||
<p align="center">
|
||||
<img src="./fluxer_static/marketing/screenshots/desktop-readme-1920w.png" alt="Fluxer app showcase" width="900">
|
||||
<img src="./fluxer_static/marketing/screenshots/desktop-readme-1920w.png" alt="Fluxer running side by side on a desktop monitor and a phone" width="640">
|
||||
</p>
|
||||
|
||||
## Download
|
||||
|
||||
| Windows | macOS | Linux | Android | iOS |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Installer (x64)][win-setup-x64] | [Disk image][mac-dmg] | [Flatpak][flatpak-ref] | [APK][android-apk] | [TestFlight][ios-testflight] |
|
||||
| [Installer (ARM64)][win-setup-arm64] | | [deb (x64)][linux-deb-x64] | [Obtainium][obtainium] | |
|
||||
| [Portable (x64)][win-portable-x64] | | [deb (ARM64)][linux-deb-arm64] | | |
|
||||
| [Portable (ARM64)][win-portable-arm64] | | [rpm (x64)][linux-rpm-x64] | | |
|
||||
| | | [rpm (ARM64)][linux-rpm-arm64] | | |
|
||||
| | | [AppImage (x64)][linux-appimage-x64] | | |
|
||||
| | | [AppImage (ARM64)][linux-appimage-arm64] | | |
|
||||
| | | [tar.gz (x64)][linux-targz-x64] | | |
|
||||
| | | [tar.gz (ARM64)][linux-targz-arm64] | | |
|
||||
|
||||
The macOS disk image is universal and runs on both Apple silicon and Intel. Windows and Linux need the build that matches your processor.
|
||||
|
||||
On Linux, prefer a package repository over a file. Fluxer then updates with the rest of your system.
|
||||
|
||||
## Linux package repositories
|
||||
|
||||
All four repositories serve stable and canary. The package is `fluxer` for stable and `fluxer-canary` for canary.
|
||||
|
||||
### Flatpak
|
||||
|
||||
Opening [this reference file][flatpak-ref] hands the install to your desktop software manager. Some desktops also accept `flatpak+https://pkgs.fluxer.com/flatpak/fluxer.flatpakref` pasted into the address bar.
|
||||
|
||||
From a terminal:
|
||||
|
||||
```sh
|
||||
flatpak install https://pkgs.fluxer.com/flatpak/fluxer.flatpakref
|
||||
```
|
||||
|
||||
### Debian and Ubuntu
|
||||
|
||||
```sh
|
||||
sudo install -d -m 0755 /etc/apt/keyrings
|
||||
sudo curl -fsSL -o /etc/apt/keyrings/fluxer-archive-keyring.gpg https://pkgs.fluxer.com/keys/fluxer-archive-keyring.gpg
|
||||
sudo curl -fsSL -o /etc/apt/sources.list.d/fluxer.sources https://pkgs.fluxer.com/deb/fluxer.sources
|
||||
sudo apt update && sudo apt install fluxer
|
||||
```
|
||||
|
||||
### Fedora and RHEL
|
||||
|
||||
```sh
|
||||
sudo curl -fsSL -o /etc/yum.repos.d/fluxer.repo https://pkgs.fluxer.com/rpm/fluxer.repo
|
||||
sudo dnf install fluxer
|
||||
```
|
||||
|
||||
RHEL, Rocky, Alma and CentOS Stream need `sudo dnf install epel-release` first, because the base repositories do not ship `libXScrnSaver`. Fedora does not need this.
|
||||
|
||||
### Arch Linux
|
||||
|
||||
The repository is signed, so pacman needs the key in its own keyring once:
|
||||
|
||||
```sh
|
||||
sudo pacman-key --init
|
||||
curl -fsSL -o /tmp/fluxer-archive-keyring.asc https://pkgs.fluxer.com/keys/fluxer-archive-keyring.asc
|
||||
sudo pacman-key --add /tmp/fluxer-archive-keyring.asc
|
||||
sudo pacman-key --lsign-key 09D01339EE128925F75E675C855C5BDE34D205D2
|
||||
```
|
||||
|
||||
`--lsign-key` is the step that makes pacman trust the key. Then add the repository:
|
||||
|
||||
```sh
|
||||
sudo tee -a /etc/pacman.conf >/dev/null <<'REPO'
|
||||
|
||||
[fluxer]
|
||||
SigLevel = Required TrustedOnly
|
||||
Server = https://pkgs.fluxer.com/arch/$repo/os/$arch
|
||||
REPO
|
||||
sudo pacman -Syu fluxer
|
||||
```
|
||||
|
||||
Write `$repo` and `$arch` literally. Both are pacman variables, not shell ones, which is why the heredoc above is quoted.
|
||||
|
||||
Full setup notes, including the canary channel, live in the [Linux repositories documentation][docs-linux].
|
||||
|
||||
## Other ways to run it
|
||||
|
||||
- [Open Fluxer in a browser](https://web.fluxer.app) with no install at all.
|
||||
- [Host your own instance][docs-selfhost] from this repository.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Documentation home][docs]
|
||||
- [Downloads][docs-downloads]
|
||||
- [Self-hosting][docs-selfhost]
|
||||
|
||||
## License
|
||||
|
||||
The source is licensed under the [AGPL-3.0-or-later](./LICENSE) license.
|
||||
|
||||
Fluxer branding, icons, default avatars, badge artwork, screenshots and marketing
|
||||
imagery are copyright Fluxer and all rights reserved, as set out in
|
||||
[fluxer_static/LICENSE](./fluxer_static/LICENSE). Third-party material keeps its
|
||||
own terms, listed in
|
||||
[fluxer_static/THIRD_PARTY_LICENSES.md](./fluxer_static/THIRD_PARTY_LICENSES.md).
|
||||
|
||||
Public availability of this repository does not grant trademark, brand, or
|
||||
endorsement rights.
|
||||
|
||||
[win-setup-x64]: https://pkgs.fluxer.com/desktop/stable/win32/x64/latest/setup
|
||||
[win-setup-arm64]: https://pkgs.fluxer.com/desktop/stable/win32/arm64/latest/setup
|
||||
[win-portable-x64]: https://pkgs.fluxer.com/desktop/stable/win32/x64/latest/portable
|
||||
[win-portable-arm64]: https://pkgs.fluxer.com/desktop/stable/win32/arm64/latest/portable
|
||||
[mac-dmg]: https://pkgs.fluxer.com/desktop/stable/darwin/arm64/latest/dmg
|
||||
[linux-deb-x64]: https://pkgs.fluxer.com/desktop/stable/linux/x64/latest/deb
|
||||
[linux-deb-arm64]: https://pkgs.fluxer.com/desktop/stable/linux/arm64/latest/deb
|
||||
[linux-rpm-x64]: https://pkgs.fluxer.com/desktop/stable/linux/x64/latest/rpm
|
||||
[linux-rpm-arm64]: https://pkgs.fluxer.com/desktop/stable/linux/arm64/latest/rpm
|
||||
[linux-appimage-x64]: https://pkgs.fluxer.com/desktop/stable/linux/x64/latest/appimage
|
||||
[linux-appimage-arm64]: https://pkgs.fluxer.com/desktop/stable/linux/arm64/latest/appimage
|
||||
[linux-targz-x64]: https://pkgs.fluxer.com/desktop/stable/linux/x64/latest/tar_gz
|
||||
[linux-targz-arm64]: https://pkgs.fluxer.com/desktop/stable/linux/arm64/latest/tar_gz
|
||||
[flatpak-ref]: https://pkgs.fluxer.com/flatpak/fluxer.flatpakref
|
||||
[android-apk]: https://github.com/fluxerapp/flutter_client/releases
|
||||
[obtainium]: https://obtainium.imranr.dev/
|
||||
[ios-testflight]: https://testflight.apple.com/join/PKZR6pK9
|
||||
[docs]: https://docs.fluxer.app
|
||||
[docs-downloads]: https://docs.fluxer.app/downloads/overview/
|
||||
[docs-linux]: https://docs.fluxer.app/downloads/linux-repositories/
|
||||
[docs-selfhost]: https://docs.fluxer.app/operator/get-started/
|
||||
|
||||
+3
-2
@@ -48,7 +48,7 @@
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"preset": "recommended",
|
||||
"complexity": {
|
||||
"noForEach": "off",
|
||||
"noImportantStyles": "off",
|
||||
@@ -83,6 +83,7 @@
|
||||
}
|
||||
},
|
||||
"useConst": "error",
|
||||
"noDescendingSpecificity": "off",
|
||||
"noNonNullAssertion": "off",
|
||||
"noParameterAssign": "off",
|
||||
"noRestrictedImports": {
|
||||
@@ -98,7 +99,7 @@
|
||||
}
|
||||
},
|
||||
"a11y": {
|
||||
"recommended": true,
|
||||
"preset": "recommended",
|
||||
"useAriaPropsForRole": "error",
|
||||
"useValidAriaRole": "error",
|
||||
"useValidAriaValues": "error",
|
||||
|
||||
Vendored
+1
-6
@@ -52,7 +52,6 @@ FLUXER_S3_SECRET_ACCESS_KEY=fluxer-secret
|
||||
FLUXER_S3_FORCE_PATH_STYLE=true
|
||||
FLUXER_S3_BUCKET_CDN=fluxer
|
||||
FLUXER_S3_BUCKET_UPLOADS=fluxer-uploads
|
||||
FLUXER_S3_BUCKET_DOWNLOADS=fluxer-downloads
|
||||
FLUXER_S3_BUCKET_REPORTS=fluxer-reports
|
||||
FLUXER_S3_BUCKET_HARVESTS=fluxer-harvests
|
||||
FLUXER_S3_BUCKET_STATIC=fluxer-static
|
||||
@@ -65,11 +64,6 @@ FLUXER_LIVEKIT_API_SECRET=fluxer-livekit-development-secret
|
||||
FLUXER_LIVEKIT_WEBHOOK_URL=http://localhost:8088/api/webhooks/livekit
|
||||
FLUXER_LIVEKIT_DEFAULT_REGION={"id":"local","name":"Local","emoji":"LC","latitude":59.3293,"longitude":18.0686}
|
||||
|
||||
FLUXER_RECON_MODE=observing
|
||||
FLUXER_RECON_EXPECTED_ROOMS=64
|
||||
FLUXER_RECON_WARMUP_SECONDS=15
|
||||
FLUXER_RECON_MAX_HOT_ROOMS=8
|
||||
|
||||
FLUXER_API_PORT=8080
|
||||
FLUXER_API_PRESIGNED_ATTACHMENT_UPLOADS_ENABLED=true
|
||||
FLUXER_API_WORKER_MODE=all_lanes
|
||||
@@ -135,6 +129,7 @@ PUBLIC_RELEASE_CHANNEL=canary
|
||||
PUBLIC_BOOTSTRAP_API_ENDPOINT=/api
|
||||
PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT=http://localhost:8088/api
|
||||
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64=Zmx1eGVyLWRldi11cGxvYWQtcmVsYXktc2VjcmV0LTAwMDA=
|
||||
FLUXER_MEDIA_PROXY_ATTACHMENT_URL_SECRETS_BASE64=Zmx1eGVyLWRldi1hdHRhY2htZW50LXVybC1zZWNyZXQ=
|
||||
AWS_EC2_METADATA_DISABLED=true
|
||||
AWS_ACCESS_KEY_ID=fluxer
|
||||
AWS_SECRET_ACCESS_KEY=fluxer-secret
|
||||
|
||||
@@ -79,34 +79,42 @@ deny = [
|
||||
{ crate = "fuse-sys", reason = "libfuse2 FFI crate; Fluxer AppImages must not reintroduce libfuse2 through native Rust dependencies" },
|
||||
]
|
||||
skip = [
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older digest API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older digest API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older crypto API" },
|
||||
{ crate = "[email protected].7", reason = "transitive dependency requires the older digest API" },
|
||||
{ crate = "[email protected].6", reason = "transitive dependency requires the older digest API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older digest API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older hashbrown API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older randomness API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the prior randomness API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older hashbrown API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older hashbrown API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the prior hashbrown API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older digest API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older HTTP API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older HTTP body API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older WASI API" },
|
||||
{ crate = "[email protected].6", reason = "transitive dependency requires the older randomness API" },
|
||||
{ crate = "[email protected].4", reason = "transitive dependency requires the prior randomness API" },
|
||||
{ crate = "[email protected].8", reason = "transitive dependency requires the older randomness API" },
|
||||
{ crate = "[email protected].5", reason = "transitive dependency requires the prior randomness API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older randomness API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the prior randomness API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older randomness API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the prior randomness API" },
|
||||
{ crate = "[email protected].6", reason = "transitive dependency requires the older digest API" },
|
||||
{ crate = "[email protected].7", reason = "transitive dependency requires the older digest API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older digest API" },
|
||||
{ crate = "s[email protected]0", reason = "transitive dependency requires the older socket API" },
|
||||
{ crate = "s[email protected].0", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]+wasi-snapshot-preview1", reason = "transitive dependency requires the legacy WASI API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older Windows API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the prior Windows API" },
|
||||
{ crate = "[email protected]", reason = "transitive dependency requires the older WASI binding API" },
|
||||
]
|
||||
skip-tree = []
|
||||
|
||||
|
||||
+109
-185
@@ -1,108 +1,64 @@
|
||||
# Every variable docker-compose.yml reads is named here, uncommented when it has
|
||||
# no default and commented with its default when it has one. A name absent from
|
||||
# this file reaches a service only through a Compose override. Compose expands
|
||||
# top to bottom, so a line using ${...} must sit below every name it reads.
|
||||
# Every variable docker-compose.yml reads, uncommented when it has no default and
|
||||
# commented with its default when it has one. Compose expands top to bottom, so a
|
||||
# line using ${...} must sit below every name it reads.
|
||||
|
||||
FLUXER_DOMAIN=chat.example.com
|
||||
FLUXER_PUBLIC_SCHEME=https
|
||||
FLUXER_PUBLIC_PORT=443
|
||||
|
||||
# The lines above are the address browsers use, and every advertised endpoint
|
||||
# carries FLUXER_PUBLIC_PORT. They do not move what the host publishes.
|
||||
# FLUXER_HTTP_PORT and FLUXER_HTTPS_PORT below do that, and a non-default port
|
||||
# needs the matching one set as well. Complete recipes sit beside them.
|
||||
# The address browsers use. FLUXER_HTTP_PORT and FLUXER_HTTPS_PORT below decide
|
||||
# which host ports Fluxer binds.
|
||||
|
||||
# How browsers reach this instance.
|
||||
#
|
||||
# Default: Fluxer binds 80 and 443 and gets its own Let's Encrypt certificate.
|
||||
# Point DNS at this host.
|
||||
#
|
||||
# Behind your own reverse proxy (nginx, Traefik, HAProxy, Cloudflare Tunnel,
|
||||
# another Caddy): uncomment COMPOSE_FILE below. Fluxer then serves plain HTTP on
|
||||
# 127.0.0.1:8080 instead, and your proxy forwards everything to it. Keep
|
||||
# FLUXER_PUBLIC_SCHEME and FLUXER_PUBLIC_PORT describing the PUBLIC address your
|
||||
# proxy serves, not this local port.
|
||||
# By default Fluxer binds 80 and 443 and gets its own certificate. Point DNS here.
|
||||
# Behind your own reverse proxy, uncomment this instead: Fluxer then serves plain
|
||||
# HTTP on 127.0.0.1:8080. Keep the scheme and port above describing the public
|
||||
# address, not this one.
|
||||
#COMPOSE_FILE=docker-compose.yml:docker-compose.proxy.yml
|
||||
|
||||
# Where the plain-HTTP port binds when the proxy overlay is in use. Leave it on
|
||||
# loopback when the proxy runs on this host. Use 0.0.0.0:8080 only when the proxy
|
||||
# is on another machine, and firewall the port to that machine.
|
||||
# Where that plain-HTTP port binds. Use 0.0.0.0:8080 only when the proxy is on
|
||||
# another machine, and firewall it to that machine.
|
||||
#FLUXER_EDGE_BIND=127.0.0.1:8080
|
||||
|
||||
# Which upstream hops may set X-Forwarded-For. Fluxer rewrites the header from
|
||||
# this to the real client address, so IP bans, rate limits and abuse detection
|
||||
# see the caller rather than the proxy. The default covers proxies on private or
|
||||
# loopback addresses, which is every same-host setup. Set it to your proxy's
|
||||
# address if it reaches Fluxer from a public IP.
|
||||
# Which hops may set X-Forwarded-For. The default covers private and loopback
|
||||
# addresses. Set your proxy's address if it reaches Fluxer from a public IP.
|
||||
#FLUXER_EDGE_TRUSTED_PROXIES=private_ranges
|
||||
|
||||
# The origin browsers see, without a trailing slash. Leave it unset and each
|
||||
# service builds one from the three values at the top of this file. Set it and it
|
||||
# wins: every service reads the host, the scheme and the port out of it and
|
||||
# ignores those three names. Use it when browsers reach the instance on a host
|
||||
# FLUXER_DOMAIN does not name. It has to be a bare origin, a scheme and a host
|
||||
# and an optional port and nothing after them, or the services refuse to start.
|
||||
# It does not move the edge listener or the published ports either, so set the
|
||||
# publish below to the port written here.
|
||||
# The origin browsers see, no trailing slash. Set it when browsers reach the
|
||||
# instance on a host FLUXER_DOMAIN does not name, and it wins over the three
|
||||
# values above. Scheme, host and optional port only. It does not move the
|
||||
# published ports.
|
||||
#FLUXER_PUBLIC_ORIGIN=https://chat.example.com
|
||||
|
||||
# Overrides the address the edge listens on inside its container. Compose builds
|
||||
# it from FLUXER_PUBLIC_SCHEME and FLUXER_DOMAIN with no port, and the edge keeps
|
||||
# its container ports at 80 and 443 whatever the public port is. Caddy matches a
|
||||
# site by host and ignores the port in the Host header, so a request arriving on
|
||||
# a non-default published port still lands on this site. Put a port in this value
|
||||
# only if you also publish that same container port below, or nothing will be
|
||||
# listening where the publish points. Honoured in the default mode only:
|
||||
# docker-compose.proxy.yml sets the literal :8080 and tunnel.compose.yml the
|
||||
# literal :80, and Compose lets the last file win, so a value here is discarded
|
||||
# under either overlay with no warning. Set it for an unusual default-mode
|
||||
# layout, such as serving several hostnames. Write the scheme into it: a bare
|
||||
# hostname means automatic HTTPS on 443 whatever FLUXER_PUBLIC_SCHEME says.
|
||||
# The address the edge listens on inside its container. Both proxy overlays set
|
||||
# this themselves, so a value here is ignored under either. Include the scheme.
|
||||
#FLUXER_EDGE_SITE_ADDRESS=https://chat.example.com
|
||||
|
||||
# The old name for the value above, read only when FLUXER_EDGE_SITE_ADDRESS is
|
||||
# unset, so an existing .env keeps the listener it already had.
|
||||
# The old name for the line above, read only when it is unset.
|
||||
#FLUXER_CADDY_SITE_ADDRESS=
|
||||
|
||||
# Host side of the edge's publishes, and the only names that decide which host
|
||||
# ports Fluxer binds. The container side is fixed. Container 80 carries the
|
||||
# HTTP to HTTPS redirect and the Let's Encrypt HTTP challenge under an https
|
||||
# scheme, and the site itself under an http one. Container 443 carries the TLS
|
||||
# site. FLUXER_HTTPS_PORT moves the TCP and the UDP publish together, because
|
||||
# HTTP/3 needs both on the same port. Both take an optional bind address in front
|
||||
# of the port, and 127.0.0.1 keeps the publish off every public interface. Give
|
||||
# them different host ports: the same host port on both is two publishes of one
|
||||
# port and the edge refuses to start.
|
||||
# Host ports. Container 80 handles the redirect and the certificate challenge,
|
||||
# container 443 the TLS site. FLUXER_HTTPS_PORT moves TCP and UDP together, since
|
||||
# HTTP/3 needs both. Both accept a bind address. Give them different host ports.
|
||||
#FLUXER_HTTP_PORT=80
|
||||
#FLUXER_HTTPS_PORT=443
|
||||
#FLUXER_HTTP_PORT=127.0.0.1:80
|
||||
#FLUXER_HTTPS_PORT=127.0.0.1:443
|
||||
|
||||
# HTTPS on 8443, complete. Host 80 stays published and still answers the ACME
|
||||
# challenge. Let's Encrypt only ever connects to the public 80 or 443, so the
|
||||
# certificate is issued if a router in front forwards public 80 to this host and
|
||||
# is not issued otherwise. Serve your own certificate from the Caddyfile when it
|
||||
# cannot.
|
||||
# HTTPS on 8443. Host 80 stays published for the certificate challenge, which
|
||||
# only ever arrives on public 80 or 443. Serve your own certificate if nothing
|
||||
# forwards those.
|
||||
#FLUXER_PUBLIC_PORT=8443
|
||||
#FLUXER_HTTPS_PORT=8443
|
||||
|
||||
# Plain HTTP on 19080, complete. The port 80 publish moves to 19080, so nothing
|
||||
# binds host 80. Under an http scheme nothing listens on container 443, so the
|
||||
# last line parks that publish on loopback for a host that wants 443 for
|
||||
# something else. Drop it and 443 is published and idle, which is what earlier
|
||||
# releases did.
|
||||
# Plain HTTP on 19080. Nothing binds host 80, and the last line parks the idle
|
||||
# 443 publish on loopback.
|
||||
#FLUXER_PUBLIC_SCHEME=http
|
||||
#FLUXER_PUBLIC_PORT=19080
|
||||
#FLUXER_HTTP_PORT=19080
|
||||
#FLUXER_HTTPS_PORT=127.0.0.1:443
|
||||
|
||||
# A tunnel or another proxy in front of the stack needs no HTTPS publish at all.
|
||||
# tunnel.compose.yml ships beside this file and replaces Caddy's published ports
|
||||
# with a single loopback HTTP publish, so nothing binds 443, and points the edge
|
||||
# at plain HTTP on that publish so it stops redirecting to https. FLUXER_HTTP_PORT
|
||||
# still moves that one publish. Set the line below and plain docker compose
|
||||
# commands pick the file up, or add it to your own -f flags if you pass any. The
|
||||
# file uses the !override tag, which needs Compose 2.24.4 or newer.
|
||||
# A tunnel needs no HTTPS publish. tunnel.compose.yml ships beside this file and
|
||||
# leaves one loopback HTTP publish. Needs Compose 2.24.4 or newer.
|
||||
#COMPOSE_FILE=docker-compose.yml:tunnel.compose.yml
|
||||
|
||||
FLUXER_REGISTRY_OWNER=fluxerapp
|
||||
@@ -111,11 +67,8 @@ FLUXER_IMAGE_TAG=v1
|
||||
|
||||
POSTGRES_PASSWORD=CHANGE_ME
|
||||
MEILI_MASTER_KEY=CHANGE_ME
|
||||
# The stack ships its own Postgres and its own object store, and points at both
|
||||
# by service name. Set these to run either one outside the stack. Leave them
|
||||
# unset and the bundled services are used. Taking a service out of the stack
|
||||
# means an upgrade skips the backup step that reaches into it, and backing that
|
||||
# store up belongs to whoever runs it.
|
||||
# Set these to run Postgres or the object store outside the stack. Backing up a
|
||||
# store you moved out is yours to arrange, and an upgrade skips it.
|
||||
#FLUXER_POSTGRES_HOST=db.example.com
|
||||
#FLUXER_POSTGRES_PORT=5432
|
||||
#FLUXER_POSTGRES_DATABASE=fluxer
|
||||
@@ -125,19 +78,15 @@ MEILI_MASTER_KEY=CHANGE_ME
|
||||
#FLUXER_S3_PUBLIC_ENDPOINT=https://cdn.example.com
|
||||
#FLUXER_S3_REGION=eu-central-1
|
||||
#FLUXER_S3_FORCE_PATH_STYLE=false
|
||||
# Bucket names. The bundled object store creates whichever names these hold, so
|
||||
# the two stay in step. An object store outside the stack needs the buckets to
|
||||
# Bucket names. The bundled store creates these. An outside store needs them to
|
||||
# exist already.
|
||||
#FLUXER_S3_BUCKET_CDN=fluxer
|
||||
#FLUXER_S3_BUCKET_UPLOADS=fluxer-uploads
|
||||
#FLUXER_S3_BUCKET_DOWNLOADS=fluxer-downloads
|
||||
#FLUXER_S3_BUCKET_REPORTS=fluxer-reports
|
||||
#FLUXER_S3_BUCKET_HARVESTS=fluxer-harvests
|
||||
|
||||
# The rest of the bundled services, pointed somewhere else the same way. Leave a
|
||||
# line unset and the service in the stack is used. Taking a service out of the
|
||||
# stack goes in an override file listed in COMPOSE_FILE, because an upgrade
|
||||
# replaces docker-compose.yml.
|
||||
# The other bundled services, pointed elsewhere. Removing a service from the
|
||||
# stack belongs in an override file, since an upgrade replaces docker-compose.yml.
|
||||
#FLUXER_KV_URL=redis://cache.example.com:6379/0
|
||||
#FLUXER_NATS_URL=nats://mq.example.com:4222
|
||||
#FLUXER_NATS_JETSTREAM_URL=nats://mq.example.com:4222
|
||||
@@ -145,24 +94,27 @@ MEILI_MASTER_KEY=CHANGE_ME
|
||||
#FLUXER_SEARCH_URL=https://search.example.com
|
||||
#FLUXER_LIVEKIT_INTERNAL_URL=http://livekit.example.com:7880
|
||||
|
||||
# Voice off. The livekit service still runs until an override file takes it out.
|
||||
# Voice off. The livekit service still runs until an override removes it.
|
||||
#FLUXER_LIVEKIT_ENABLED=false
|
||||
|
||||
# Optional systems, each off unless the instance is configured for it.
|
||||
# Optional systems, each off unless configured.
|
||||
#FLUXER_SMS_ENABLED=false
|
||||
#FLUXER_STRIPE_ENABLED=false
|
||||
#FLUXER_NCMEC_ENABLED=false
|
||||
#FLUXER_CLAMAV_ENABLED=false
|
||||
|
||||
# The client address. Set the header name a proxy in front actually writes, and
|
||||
# turn the trust off when nothing sits in front, because a trusted header an
|
||||
# attacker can set is a spoofed client address.
|
||||
# Outside lookups, off unless turned on. The Tor exit list comes from
|
||||
# onionoo.torproject.org and the breached password check asks
|
||||
# api.pwnedpasswords.com.
|
||||
#FLUXER_TOR_EXIT_LIST_ENABLED=true
|
||||
#FLUXER_BREACHED_PASSWORD_CHECK_ENABLED=true
|
||||
|
||||
# The client address. Name the header your proxy actually writes, and turn the
|
||||
# trust off when nothing sits in front.
|
||||
#FLUXER_CLIENT_IP_HEADER_NAME=cf-connecting-ip
|
||||
#FLUXER_TRUST_CLIENT_IP_HEADER=true
|
||||
|
||||
# How much the services write. trace, debug, info, warn, error or fatal. Every
|
||||
# service names the object storage endpoint and its addressing at info on start,
|
||||
# so a bucket that answers 404 is visible without raising this.
|
||||
# How much the services write. trace, debug, info, warn, error or fatal.
|
||||
#LOG_LEVEL=debug
|
||||
|
||||
FLUXER_S3_ACCESS_KEY=fluxer
|
||||
@@ -177,32 +129,43 @@ FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64=CHANGE_ME
|
||||
FLUXER_ADMIN_SECRET_KEY_BASE=CHANGE_ME
|
||||
FLUXER_ADMIN_OAUTH_CLIENT_SECRET=CHANGE_ME
|
||||
|
||||
# The token every service sends to NATS. The bundled NATS runs without
|
||||
# authentication, so this stays empty unless a Compose override points the stack
|
||||
# at an external NATS that requires a token. Compose forwards the name to every
|
||||
# container that connects.
|
||||
# The token every service sends to NATS. The bundled NATS needs none, so this
|
||||
# stays empty unless an override points at an external one.
|
||||
#FLUXER_NATS_AUTH_TOKEN=
|
||||
|
||||
FLUXER_VAPID_PUBLIC_KEY=CHANGE_ME
|
||||
FLUXER_VAPID_PRIVATE_KEY=CHANGE_ME
|
||||
|
||||
# The VAPID contact address defaults to admin@ followed by FLUXER_DOMAIN. Set it
|
||||
# only if that mailbox does not exist.
|
||||
# Defaults to admin@ followed by FLUXER_DOMAIN. Set it if that mailbox does not
|
||||
# exist.
|
||||
#[email protected]
|
||||
|
||||
# Passkeys follow FLUXER_DOMAIN by default. Set these only if browsers reach the
|
||||
# instance on a different host, and note that changing FLUXER_PASSKEY_RP_ID
|
||||
# invalidates every passkey already registered against the old value.
|
||||
# Passkeys follow FLUXER_DOMAIN. Set these only if browsers use another host.
|
||||
# Changing the RP ID invalidates every passkey registered against the old value.
|
||||
#FLUXER_PASSKEY_RP_ID=chat.example.com
|
||||
#FLUXER_PASSKEY_RP_NAME=Fluxer
|
||||
#FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS=https://chat.example.com
|
||||
#FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS=http://chat.example.com:19080
|
||||
|
||||
# Extra Content-Security-Policy sources, appended to the built-in ones. Set these
|
||||
# only when a browser must reach an origin the defaults do not cover, such as a
|
||||
# voice server hosted on a domain other than FLUXER_DOMAIN. Separate several
|
||||
# sources with spaces or commas. Every one of them is empty by default, and the
|
||||
# three carrying a value below are illustrations, not defaults.
|
||||
# Optional media policies, both off by default. See the operator docs.
|
||||
#
|
||||
# CORS limits which web origins may read media. A request with no Origin is
|
||||
# always served. Add https://web.fluxer.app if people use the hosted client.
|
||||
#
|
||||
# Signatures make an attachment read need a signed URL, so a copied link stops
|
||||
# working. Needs a secret from openssl rand -base64 32, first entry signs and
|
||||
# every entry verifies.
|
||||
#
|
||||
# Each mode is off, report or enforce. Start at report. media-proxy reads these
|
||||
# at start, so apply with docker compose up -d media-proxy.
|
||||
#FLUXER_MEDIA_PROXY_CORS_MODE=enforce
|
||||
#FLUXER_MEDIA_PROXY_CORS_ALLOWED_ORIGINS=https://chat.example.com,https://web.fluxer.app
|
||||
#FLUXER_MEDIA_PROXY_ATTACHMENT_URL_SECRETS_BASE64=
|
||||
#FLUXER_MEDIA_PROXY_ATTACHMENT_SIGNATURE_MODE=enforce
|
||||
|
||||
# Extra Content-Security-Policy sources, appended to the built-in ones. Set one
|
||||
# only when a browser must reach an origin the defaults do not cover. Separate
|
||||
# several with spaces or commas. The three values below are illustrations.
|
||||
#FLUXER_CSP_EXTRA_DEFAULT_SRC=
|
||||
#FLUXER_CSP_EXTRA_CONNECT_SRC=wss://livekit.example.com:7881
|
||||
#FLUXER_CSP_EXTRA_IMG_SRC=https://cdn.example.com
|
||||
@@ -214,39 +177,29 @@ FLUXER_VAPID_PRIVATE_KEY=CHANGE_ME
|
||||
#FLUXER_CSP_EXTRA_WORKER_SRC=
|
||||
#FLUXER_CSP_EXTRA_MANIFEST_SRC=
|
||||
|
||||
# One report-uri for Content-Security-Policy violation reports. Empty leaves the
|
||||
# directive off the header.
|
||||
# One report-uri for CSP violation reports. Empty leaves the directive off.
|
||||
#FLUXER_CSP_REPORT_URI=
|
||||
|
||||
# Allow the SSO identity provider to resolve to a private or internal address.
|
||||
# Off by default: the API refuses to call non-public addresses so a misconfigured
|
||||
# provider URL cannot be used to reach internal services. Turn it on only when the
|
||||
# provider genuinely lives on your own network, such as split-horizon DNS or a LAN
|
||||
# identity provider, and only when you trust everyone who can configure SSO.
|
||||
# Let the SSO provider resolve to a private address. Off by default, so a
|
||||
# misconfigured provider URL cannot reach internal services. Turn it on only for
|
||||
# a provider on your own network.
|
||||
#FLUXER_SSO_ALLOW_PRIVATE_ADDRESSES=true
|
||||
|
||||
# Both reach LiveKit as LIVEKIT_KEYS and the webhook signing key, and the API as
|
||||
# FLUXER_LIVEKIT_API_KEY and FLUXER_LIVEKIT_API_SECRET. Change them together.
|
||||
# These reach both LiveKit and the api. Change them together.
|
||||
LIVEKIT_API_KEY=fluxer
|
||||
LIVEKIT_API_SECRET=CHANGE_ME
|
||||
|
||||
# The URL browsers use for voice signalling. Compose builds it from
|
||||
# FLUXER_PUBLIC_ORIGIN, or from FLUXER_PUBLIC_SCHEME, FLUXER_DOMAIN and
|
||||
# FLUXER_PUBLIC_PORT, as that origin followed by /livekit. The client rewrites a
|
||||
# leading http to ws itself. Set it only when LiveKit is served from another
|
||||
# host.
|
||||
# The URL browsers use for voice signalling. Built from the public origin plus
|
||||
# /livekit. Set it only when LiveKit is served from another host.
|
||||
#FLUXER_LIVEKIT_URL=
|
||||
|
||||
# Media ports. LiveKit advertises these in ICE candidates, so the host must
|
||||
# forward the same numbers.
|
||||
# Media ports. LiveKit advertises these, so forward the same numbers.
|
||||
#FLUXER_LIVEKIT_TCP_PORT=7881
|
||||
#FLUXER_LIVEKIT_UDP_PORT=7882
|
||||
|
||||
# LiveKit finds the address browsers dial by asking a STUN server. A host that
|
||||
# cannot reach one over UDP stops with "could not resolve external IP", and the
|
||||
# address is then set by hand: put it in FLUXER_LIVEKIT_NODE_IP and set
|
||||
# FLUXER_LIVEKIT_USE_EXTERNAL_IP to false. Point the STUN entries at another
|
||||
# server to keep the lookup and leave Google out of it.
|
||||
# LiveKit finds its public address over STUN. A host that cannot reach one stops
|
||||
# with "could not resolve external IP", so set the address by hand instead, or
|
||||
# point STUN elsewhere.
|
||||
#FLUXER_LIVEKIT_USE_EXTERNAL_IP=false
|
||||
#FLUXER_LIVEKIT_NODE_IP=203.0.113.10
|
||||
#FLUXER_LIVEKIT_STUN_PRIMARY=stun.l.google.com:19302
|
||||
@@ -273,11 +226,9 @@ FLUXER_CAPTCHA_TURNSTILE_SITE_KEY=
|
||||
FLUXER_CAPTCHA_TURNSTILE_SECRET_KEY=
|
||||
FLUXER_DISCOVERY_ENABLED=true
|
||||
|
||||
# Container memory. The limits sum to 18.25 GiB, which is a sum of ceilings and
|
||||
# not an allocation, so the defaults fit a host with 8 GB and are sized for 16 GB.
|
||||
# The reservations are cgroup memory.low, which biases the kernel away from
|
||||
# reclaiming from services whose death takes the instance down. They reserve
|
||||
# nothing. Lower the limits on a smaller host.
|
||||
# Container memory. These are ceilings, not allocations, and the defaults suit a
|
||||
# 16 GB host. The reservations bias the kernel away from reclaiming from services
|
||||
# whose death takes the instance down. Lower the limits on a smaller host.
|
||||
#FLUXER_CADDY_MEMORY_LIMIT=256mb
|
||||
#FLUXER_POSTGRES_MEMORY_LIMIT=5gb
|
||||
#FLUXER_POSTGRES_MEMORY_RESERVATION=3gb
|
||||
@@ -308,32 +259,22 @@ FLUXER_DISCOVERY_ENABLED=true
|
||||
#FLUXER_UNFURL_SHARD_MEMORY_LIMIT=256mb
|
||||
#FLUXER_ADMIN_MEMORY_LIMIT=256mb
|
||||
|
||||
# Meilisearch indexing memory. Keep it well under FLUXER_MEILISEARCH_MEMORY_LIMIT,
|
||||
# which is the container ceiling the indexer shares with the search process.
|
||||
# Meilisearch indexing memory. Keep it well under the container limit above.
|
||||
#FLUXER_MEILISEARCH_MAX_INDEXING_MEMORY=384mb
|
||||
|
||||
# SeaweedFS heap ceiling. Go collects against this value instead of against the
|
||||
# container limit, which it cannot see, so without it an upload burst grows the
|
||||
# heap past FLUXER_SEAWEEDFS_MEMORY_LIMIT and the kernel OOM-kills the container
|
||||
# mid-upload (exit 137). Keep it near three quarters of that limit, and raise both
|
||||
# together: the peak is the parts of one upload in flight at once, which is 25 MB
|
||||
# times 20 for a 500 MB attachment.
|
||||
# SeaweedFS heap ceiling. Go cannot see the container limit, so without this an
|
||||
# upload burst gets the container OOM-killed. Keep it near three quarters of
|
||||
# FLUXER_SEAWEEDFS_MEMORY_LIMIT and raise both together.
|
||||
#FLUXER_SEAWEEDFS_GOMEMLIMIT=1536MiB
|
||||
|
||||
# Node sizes its own heap from the container memory limit by default, at roughly
|
||||
# 55 percent of it, which always leaves room for the buffers and stacks that live
|
||||
# outside the heap. Leave these unset unless you have a reason to pin the value.
|
||||
# Any value set here must stay well below the container limit above: a heap ceiling
|
||||
# above the container limit makes the kernel OOM-kill the container (exit 137, no
|
||||
# diagnostics) instead of Node reporting a JavaScript heap out of memory error.
|
||||
# Node sizes its heap from the container limit by default. Leave these unset
|
||||
# unless you need to pin it. A heap ceiling above the container limit gets the
|
||||
# container OOM-killed instead of reporting a heap error.
|
||||
#FLUXER_API_NODE_HEAP_MB=1792
|
||||
#FLUXER_WORKER_NODE_HEAP_MB=1792
|
||||
|
||||
# Bundled Postgres tuning. Keep these consistent with FLUXER_POSTGRES_MEMORY_LIMIT:
|
||||
# budget roughly shared_buffers + (server max_connections x 12 MB) +
|
||||
# (3 x autovacuum_work_mem) + 300 MB for page cache and WAL. Note this is the
|
||||
# server setting, distinct from the per-service FLUXER_POSTGRES_MAX_CONNECTIONS
|
||||
# pool sizes used by the api, worker and shards.
|
||||
# Bundled Postgres tuning. Keep it consistent with the memory limit above. This
|
||||
# is the server setting, not the per-service pool sizes.
|
||||
#FLUXER_POSTGRES_SERVER_MAX_CONNECTIONS=150
|
||||
#FLUXER_POSTGRES_SHARED_BUFFERS=512MB
|
||||
#FLUXER_POSTGRES_EFFECTIVE_CACHE_SIZE=2GB
|
||||
@@ -341,47 +282,30 @@ FLUXER_DISCOVERY_ENABLED=true
|
||||
#FLUXER_POSTGRES_MAINTENANCE_WORK_MEM=256MB
|
||||
#FLUXER_POSTGRES_AUTOVACUUM_WORK_MEM=128MB
|
||||
|
||||
# The bundled Valkey holds durable state as well as cache. The bulk message
|
||||
# deletion queue and the account deletion queue are sorted sets with no expiry,
|
||||
# and nothing else stores the first of the two. It therefore runs with an
|
||||
# append-only file on a named volume and with noeviction, so an over-limit write
|
||||
# fails loudly instead of silently deleting queued work. Distributed locks all
|
||||
# carry a TTL and are not what the durability is for. Only change the policy if
|
||||
# you have moved that durable state elsewhere.
|
||||
# The bundled Valkey holds durable state as well as cache, so it runs with an
|
||||
# append-only file and with noeviction, which fails an over-limit write instead
|
||||
# of dropping queued work. Change the policy only if that state lives elsewhere.
|
||||
#FLUXER_VALKEY_MAXMEMORY=192mb
|
||||
#FLUXER_VALKEY_MAXMEMORY_POLICY=noeviction
|
||||
|
||||
# The gateway derives its BEAM scheduler count from the container CPU quota,
|
||||
# clamped to this range. The floor matters: a single scheduler lets one blocking
|
||||
# operation stall every websocket on the node. The ceiling stops a large host
|
||||
# from starting far more schedulers than the container can actually use.
|
||||
# The gateway derives its scheduler count from the CPU quota, clamped here. One
|
||||
# scheduler lets a single blocking operation stall every websocket on the node.
|
||||
#FLUXER_ERLANG_SCHEDULERS_MIN=2
|
||||
#FLUXER_ERLANG_SCHEDULERS_MAX=16
|
||||
|
||||
# In-flight request ceiling for the services Compose forwards it to: the users
|
||||
# and messages routers and their shards. Leave it unset and each service uses its
|
||||
# built-in default. Set it and the one value replaces that default on all of
|
||||
# them, so size it for the busiest. The built-in defaults are 192 for
|
||||
# messages, 320 for snowflakes and 64 elsewhere, and they govern every service
|
||||
# Compose does not forward this to. A router holds a slot for the whole round
|
||||
# trip to its shard, so this is a ceiling on requests in flight at once and not a
|
||||
# rate: too low a value does not slow requests down, it rejects them, and the api
|
||||
# turns that rejection into a 503.
|
||||
# In-flight request ceiling for the users and messages routers and their shards.
|
||||
# One value replaces the built-in default on all of them, so size it for the
|
||||
# busiest. Too low a value rejects requests rather than slowing them, and the api
|
||||
# turns that into a 503.
|
||||
#FLUXER_SVC_MAX_CONCURRENT_REQUESTS=192
|
||||
|
||||
# The api and the Rust services name their fixed Postgres statement shapes so the
|
||||
# server can reuse their plans. Named prepared statements require a session that
|
||||
# outlives the transaction, so set this to false if you put a transaction-pooling
|
||||
# connection pooler such as PgBouncer in front of Postgres. One setting governs
|
||||
# every service. The bundled compose talks to Postgres directly, where naming is
|
||||
# a win and the default is correct.
|
||||
# Named prepared statements need a session that outlives the transaction, so set
|
||||
# this to false behind a transaction-pooling connection pooler. The bundled
|
||||
# compose talks to Postgres directly, where the default is correct.
|
||||
#FLUXER_POSTGRES_PREPARED_STATEMENTS=true
|
||||
|
||||
# The api bounds how long a client may take to send a request. The header timeout
|
||||
# covers the request line and headers only, while the request timeout covers the
|
||||
# whole exchange, so a slow uploader is bounded by the second value and not by
|
||||
# the first. Raise both if you front large uploads or serve clients on high
|
||||
# latency links. The header timeout is clamped down to the request timeout, so
|
||||
# raising it alone does nothing. Both are milliseconds, between 1000 and 3600000.
|
||||
# How long a client may take to send a request. The header timeout covers the
|
||||
# request line and headers, the request timeout the whole exchange, and the first
|
||||
# is clamped down to the second. Milliseconds, 1000 to 3600000.
|
||||
#FLUXER_API_HEADERS_TIMEOUT_MS=30000
|
||||
#FLUXER_API_REQUEST_TIMEOUT_MS=120000
|
||||
|
||||
@@ -24,6 +24,8 @@ x-fluxer-env: &fluxer-env
|
||||
FLUXER_CLIENT_IP_HEADER_NAME: ${FLUXER_CLIENT_IP_HEADER_NAME:-x-forwarded-for}
|
||||
FLUXER_API_HEADERS_TIMEOUT_MS: ${FLUXER_API_HEADERS_TIMEOUT_MS:-30000}
|
||||
FLUXER_API_REQUEST_TIMEOUT_MS: ${FLUXER_API_REQUEST_TIMEOUT_MS:-120000}
|
||||
FLUXER_TOR_EXIT_LIST_ENABLED: "${FLUXER_TOR_EXIT_LIST_ENABLED:-false}"
|
||||
FLUXER_BREACHED_PASSWORD_CHECK_ENABLED: "${FLUXER_BREACHED_PASSWORD_CHECK_ENABLED:-false}"
|
||||
|
||||
FLUXER_KV_URL: ${FLUXER_KV_URL:-redis://valkey:6379/0}
|
||||
FLUXER_NATS_URL: ${FLUXER_NATS_URL:-nats://nats:4222}
|
||||
@@ -44,7 +46,6 @@ x-fluxer-env: &fluxer-env
|
||||
FLUXER_S3_FORCE_PATH_STYLE: "${FLUXER_S3_FORCE_PATH_STYLE:-true}"
|
||||
FLUXER_S3_BUCKET_CDN: ${FLUXER_S3_BUCKET_CDN:-fluxer}
|
||||
FLUXER_S3_BUCKET_UPLOADS: ${FLUXER_S3_BUCKET_UPLOADS:-fluxer-uploads}
|
||||
FLUXER_S3_BUCKET_DOWNLOADS: ${FLUXER_S3_BUCKET_DOWNLOADS:-fluxer-downloads}
|
||||
FLUXER_S3_BUCKET_REPORTS: ${FLUXER_S3_BUCKET_REPORTS:-fluxer-reports}
|
||||
FLUXER_S3_BUCKET_HARVESTS: ${FLUXER_S3_BUCKET_HARVESTS:-fluxer-harvests}
|
||||
AWS_ACCESS_KEY_ID: ${FLUXER_S3_ACCESS_KEY:?set FLUXER_S3_ACCESS_KEY in .env}
|
||||
@@ -97,6 +98,7 @@ x-fluxer-env: &fluxer-env
|
||||
FLUXER_GATEWAY_RPC_AUTH_TOKEN: ${FLUXER_GATEWAY_RPC_AUTH_TOKEN:?set FLUXER_GATEWAY_RPC_AUTH_TOKEN in .env}
|
||||
FLUXER_MEDIA_PROXY_SECRET_KEY: ${FLUXER_MEDIA_PROXY_SECRET_KEY:?set FLUXER_MEDIA_PROXY_SECRET_KEY in .env}
|
||||
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64: ${FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64:?set FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 in .env}
|
||||
FLUXER_MEDIA_PROXY_ATTACHMENT_URL_SECRETS_BASE64: ${FLUXER_MEDIA_PROXY_ATTACHMENT_URL_SECRETS_BASE64:-}
|
||||
FLUXER_ADMIN_SECRET_KEY_BASE: ${FLUXER_ADMIN_SECRET_KEY_BASE:?set FLUXER_ADMIN_SECRET_KEY_BASE in .env}
|
||||
FLUXER_ADMIN_OAUTH_CLIENT_SECRET: ${FLUXER_ADMIN_OAUTH_CLIENT_SECRET:?set FLUXER_ADMIN_OAUTH_CLIENT_SECRET in .env}
|
||||
|
||||
@@ -122,7 +124,7 @@ x-fluxer-svc-healthcheck: &fluxer-svc-healthcheck
|
||||
|
||||
services:
|
||||
edge:
|
||||
image: caddy:2.10-alpine
|
||||
image: caddy:2.11-alpine
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
@@ -200,7 +202,7 @@ services:
|
||||
retries: 10
|
||||
|
||||
valkey:
|
||||
image: valkey/valkey:8.1-alpine
|
||||
image: valkey/valkey:9.1-alpine
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
@@ -236,7 +238,7 @@ services:
|
||||
retries: 10
|
||||
|
||||
meilisearch:
|
||||
image: getmeili/meilisearch:v1.12
|
||||
image: getmeili/meilisearch:v1.53
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
@@ -246,6 +248,7 @@ services:
|
||||
environment:
|
||||
MEILI_ENV: production
|
||||
MEILI_NO_ANALYTICS: "true"
|
||||
MEILI_UPGRADE_DB: "true"
|
||||
MEILI_MAX_INDEXING_MEMORY: ${FLUXER_MEILISEARCH_MAX_INDEXING_MEMORY:-384mb}
|
||||
MEILI_MASTER_KEY: ${MEILI_MASTER_KEY:?set MEILI_MASTER_KEY in .env}
|
||||
volumes:
|
||||
@@ -257,7 +260,7 @@ services:
|
||||
retries: 10
|
||||
|
||||
seaweedfs:
|
||||
image: chrislusf/seaweedfs:4.34
|
||||
image: chrislusf/seaweedfs:4.47
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
@@ -266,7 +269,7 @@ services:
|
||||
networks: [fluxer]
|
||||
environment:
|
||||
GOMEMLIMIT: ${FLUXER_SEAWEEDFS_GOMEMLIMIT:-1536MiB}
|
||||
command: ["server", "-s3", "-dir=/data"]
|
||||
command: ["server", "-s3", "-dir=/data", "-master.telemetry=false"]
|
||||
volumes:
|
||||
- seaweedfs-data:/data
|
||||
healthcheck:
|
||||
@@ -277,7 +280,7 @@ services:
|
||||
start_period: 60s
|
||||
|
||||
seaweedfs-init:
|
||||
image: chrislusf/seaweedfs:4.34
|
||||
image: chrislusf/seaweedfs:4.47
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
@@ -291,14 +294,13 @@ services:
|
||||
FLUXER_S3_SECRET_KEY: ${FLUXER_S3_SECRET_KEY:?set FLUXER_S3_SECRET_KEY in .env}
|
||||
FLUXER_S3_BUCKET_CDN: ${FLUXER_S3_BUCKET_CDN:-fluxer}
|
||||
FLUXER_S3_BUCKET_UPLOADS: ${FLUXER_S3_BUCKET_UPLOADS:-fluxer-uploads}
|
||||
FLUXER_S3_BUCKET_DOWNLOADS: ${FLUXER_S3_BUCKET_DOWNLOADS:-fluxer-downloads}
|
||||
FLUXER_S3_BUCKET_REPORTS: ${FLUXER_S3_BUCKET_REPORTS:-fluxer-reports}
|
||||
FLUXER_S3_BUCKET_HARVESTS: ${FLUXER_S3_BUCKET_HARVESTS:-fluxer-harvests}
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- >
|
||||
buckets="$$FLUXER_S3_BUCKET_CDN $$FLUXER_S3_BUCKET_UPLOADS $$FLUXER_S3_BUCKET_DOWNLOADS $$FLUXER_S3_BUCKET_REPORTS $$FLUXER_S3_BUCKET_HARVESTS";
|
||||
buckets="$$FLUXER_S3_BUCKET_CDN $$FLUXER_S3_BUCKET_UPLOADS $$FLUXER_S3_BUCKET_REPORTS $$FLUXER_S3_BUCKET_HARVESTS";
|
||||
missing="$$buckets";
|
||||
for attempt in $$(seq 1 60); do
|
||||
if ! nc -z seaweedfs 9333 2>/dev/null; then
|
||||
@@ -413,7 +415,6 @@ services:
|
||||
NODE_OPTIONS: --enable-source-maps${FLUXER_WORKER_NODE_HEAP_MB:+ --max-old-space-size=$FLUXER_WORKER_NODE_HEAP_MB}
|
||||
FLUXER_API_WORKER_MODE: all_lanes
|
||||
FLUXER_API_WORKER_ENABLE_CRON_SCHEDULER: "true"
|
||||
FLUXER_API_WORKER_ENABLE_VOICE_RECONCILIATION: "true"
|
||||
FLUXER_POSTGRES_MAX_CONNECTIONS: "25"
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "const age=Date.now()-require('node:fs').statSync('/tmp/fluxer-worker-heartbeat').mtimeMs;if(age>30000){console.error('worker heartbeat is '+Math.round(age)+'ms old');process.exit(1)}"]
|
||||
@@ -473,6 +474,9 @@ services:
|
||||
FLUXER_MEDIA_PROXY_MODE: upload
|
||||
FLUXER_MEDIA_PROXY_STORAGE_BACKEND: s3
|
||||
FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/media
|
||||
FLUXER_MEDIA_PROXY_CORS_MODE: ${FLUXER_MEDIA_PROXY_CORS_MODE:-off}
|
||||
FLUXER_MEDIA_PROXY_CORS_ALLOWED_ORIGINS: ${FLUXER_MEDIA_PROXY_CORS_ALLOWED_ORIGINS:-${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}:${FLUXER_PUBLIC_PORT:-443}}}
|
||||
FLUXER_MEDIA_PROXY_ATTACHMENT_SIGNATURE_MODE: ${FLUXER_MEDIA_PROXY_ATTACHMENT_SIGNATURE_MODE:-off}
|
||||
FLUXER_S3_READ_SIGNED: "true"
|
||||
depends_on:
|
||||
seaweedfs-init: {condition: service_completed_successfully}
|
||||
|
||||
+13
-13
@@ -9,32 +9,32 @@ build = "build.rs"
|
||||
[dependencies]
|
||||
anyhow = "1.0.104"
|
||||
axum = { version = "0.8.9", features = ["macros"] }
|
||||
base64 = "0.22.1"
|
||||
base64 = "0.23.1"
|
||||
chrono = { version = "0.4", default-features = false, features = ["serde"] }
|
||||
cookie = "0.18.1"
|
||||
cookie = "0.18.2"
|
||||
fluxer_common = { path = "../fluxer_common" }
|
||||
hmac = "0.13.0"
|
||||
maud = { version = "0.27.0", features = ["axum"] }
|
||||
rand = "0.10"
|
||||
regress = "0.11"
|
||||
reqwest = { version = "0.13.4", default-features = false, features = ["json", "rustls"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.150"
|
||||
regress = "0.12"
|
||||
reqwest = { version = "0.13.5", default-features = false, features = ["json", "rustls"] }
|
||||
serde = { version = "1.0.229", features = ["derive"] }
|
||||
serde_json = "1.0.151"
|
||||
sha2 = "0.11.0"
|
||||
time = { version = "0.3.47", features = ["formatting", "parsing"] }
|
||||
tokio = { version = "1.52.3", features = ["macros", "net", "rt-multi-thread", "signal"] }
|
||||
time = { version = "0.3.55", features = ["formatting", "parsing"] }
|
||||
tokio = { version = "1.53.1", features = ["macros", "net", "rt-multi-thread", "signal"] }
|
||||
tower = { version = "0.5.3", features = ["util"] }
|
||||
tower-http = { version = "0.6.11", features = ["compression-gzip", "trace"] }
|
||||
tower-http = { version = "0.7.1", features = ["compression-gzip", "trace"] }
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
||||
url = "2.5"
|
||||
urlencoding = "2.1.3"
|
||||
progenitor-client = { version = "0.14.0", default-features = false }
|
||||
progenitor-client = { version = "0.15.0", default-features = false }
|
||||
|
||||
[build-dependencies]
|
||||
openapiv3 = "2.2.0"
|
||||
prettyplease = "0.2"
|
||||
progenitor = { version = "0.14.0", default-features = false }
|
||||
prettyplease = "0.3"
|
||||
progenitor = { version = "0.15.0", default-features = false }
|
||||
serde_json = "1"
|
||||
sha2 = "0.11.0"
|
||||
syn = "2"
|
||||
syn = "3"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
FROM rust:1-bookworm AS builder
|
||||
FROM rust:1-trixie AS builder
|
||||
|
||||
ARG BUILD_VERSION=""
|
||||
ARG TARGETARCH
|
||||
@@ -9,7 +9,7 @@ WORKDIR /usr/src/app
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates nodejs npm pkg-config \
|
||||
&& npm install -g pnpm@10.29.3 \
|
||||
&& npm install -g pnpm@11.27.0 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN npm install --no-audit --no-fund @tailwindcss/[email protected] [email protected]
|
||||
@@ -57,7 +57,7 @@ RUN test "$(ls target/release/build/fluxer_admin-*/out/static/fonts/*.woff2 | wc
|
||||
&& ls target/release/build/fluxer_admin-*/out/static/fonts/fonts.*.css \
|
||||
&& echo "Latin-core fonts bundled successfully"
|
||||
|
||||
FROM debian:bookworm-slim AS runtime
|
||||
FROM debian:trixie-slim AS runtime
|
||||
|
||||
ARG BUILD_VERSION=""
|
||||
ARG SOURCE_SHA=""
|
||||
|
||||
+12
-2
@@ -54,9 +54,9 @@ fn generate_admin_api(manifest_dir: &Path, out_dir: &Path) {
|
||||
.generate_tokens(&spec)
|
||||
.expect("failed to generate admin API client");
|
||||
|
||||
let content = prettyplease::unparse(
|
||||
let content = relax_required_nullable_fields(&prettyplease::unparse(
|
||||
&syn::parse2::<syn::File>(tokens).expect("failed to parse generated tokens"),
|
||||
);
|
||||
));
|
||||
|
||||
let output_path = out_dir.join("admin_api_generated.rs");
|
||||
fs::write(&output_path, content).expect("failed to write generated API code");
|
||||
@@ -190,6 +190,16 @@ fn object_schema_mut<'a>(
|
||||
|
||||
const MAX_SCHEMA_REFERENCE_DEPTH: usize = 32;
|
||||
|
||||
fn relax_required_nullable_fields(generated: &str) -> String {
|
||||
const PRESENCE_CHECK: &str =
|
||||
"#[serde(deserialize_with = \"::std::option::Option::deserialize\")]";
|
||||
generated
|
||||
.lines()
|
||||
.filter(|line| line.trim() != PRESENCE_CHECK)
|
||||
.flat_map(|line| [line, "\n"])
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn relax_progenitor_schema_strictness(spec: &mut openapiv3::OpenAPI) {
|
||||
let registry = spec.components.clone().unwrap_or_default();
|
||||
|
||||
|
||||
+104
-436
@@ -933,6 +933,22 @@
|
||||
},
|
||||
"description": "Filter by target entity ID (user, channel, role, invite code, etc.)"
|
||||
},
|
||||
{
|
||||
"name": "access",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"description": "Only return entries recorded by reads or only entries recorded by writes",
|
||||
"x-enumNames": ["read", "write"],
|
||||
"x-enumDescriptions": [
|
||||
"An entry recorded by an operation that only reads data",
|
||||
"An entry recorded by an operation that changes data or triggers work"
|
||||
],
|
||||
"enum": ["read", "write"],
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Only return entries recorded by reads or only entries recorded by writes"
|
||||
},
|
||||
{
|
||||
"name": "sort_by",
|
||||
"in": "query",
|
||||
@@ -5427,59 +5443,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/admin/system/heap-snapshots": {
|
||||
"post": {
|
||||
"operationId": "create_admin_system_heap_snapshot",
|
||||
"summary": "Create a V8 heap snapshot",
|
||||
"tags": ["Admin"],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {"application/octet-stream": {"schema": {"$ref": "#/components/schemas/HeapSnapshotResponse"}}}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request - The request was malformed or contained invalid data",
|
||||
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized - Authentication is required or the token is invalid",
|
||||
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden - You do not have permission to perform this action",
|
||||
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
|
||||
},
|
||||
"429": {
|
||||
"description": "Too Many Requests - You are being rate limited",
|
||||
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ThrottledError"}}},
|
||||
"headers": {
|
||||
"Retry-After": {
|
||||
"description": "Number of seconds to wait before retrying (only on 429)",
|
||||
"schema": {"type": "integer"}
|
||||
},
|
||||
"X-RateLimit-Limit": {
|
||||
"description": "The number of requests that can be made in the current window",
|
||||
"schema": {"type": "integer"}
|
||||
},
|
||||
"X-RateLimit-Remaining": {
|
||||
"description": "The number of remaining requests that can be made",
|
||||
"schema": {"type": "integer"}
|
||||
},
|
||||
"X-RateLimit-Reset": {
|
||||
"description": "Unix timestamp when the rate limit resets",
|
||||
"schema": {"type": "integer"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error - An unexpected error occurred",
|
||||
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
|
||||
}
|
||||
},
|
||||
"description": "Writes a V8 heap snapshot of the current process and returns the snapshot file. Used for diagnosing memory leaks. Requires SYSTEM_HEAP_SNAPSHOT permission.",
|
||||
"security": [{"adminApiKey": []}]
|
||||
}
|
||||
},
|
||||
"/admin/users": {
|
||||
"get": {
|
||||
"operationId": "list_admin_users",
|
||||
@@ -9874,7 +9837,7 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"acls": {
|
||||
"maxItems": 112,
|
||||
"maxItems": 111,
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/AdminAclType"},
|
||||
"description": "List of access control permissions to assign"
|
||||
@@ -9904,7 +9867,6 @@
|
||||
"required": ["users", "total"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"HeapSnapshotResponse": {"type": "string", "format": "binary", "description": "V8 heap snapshot file"},
|
||||
"SendSystemDmRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -10562,16 +10524,8 @@
|
||||
},
|
||||
"gateway_rollout": {"$ref": "#/components/schemas/GatewayRolloutConfigResponse"},
|
||||
"voice_noise_suppression": {"$ref": "#/components/schemas/VoiceNoiseSuppressionConfigResponse"},
|
||||
"guild_activity_log_presentation": {
|
||||
"$ref": "#/components/schemas/GuildActivityLogPresentationConfigResponse"
|
||||
},
|
||||
"screen_share_delivery": {"$ref": "#/components/schemas/ScreenShareDeliveryConfigResponse"},
|
||||
"experiment_delivery": {"$ref": "#/components/schemas/ExperimentDeliveryConfigResponse"},
|
||||
"message_hover_tracking": {"$ref": "#/components/schemas/MessageHoverTrackingConfigResponse"},
|
||||
"message_keyboard_focus": {"$ref": "#/components/schemas/MessageKeyboardFocusConfigResponse"},
|
||||
"blocked_message_groups": {"$ref": "#/components/schemas/BlockedMessageGroupsConfigResponse"},
|
||||
"expression_info_card": {"$ref": "#/components/schemas/ExpressionInfoCardConfigResponse"},
|
||||
"guild_header_collapse": {"$ref": "#/components/schemas/GuildHeaderCollapseConfigResponse"},
|
||||
"typing_indicator_rework": {"$ref": "#/components/schemas/TypingIndicatorReworkConfigResponse"},
|
||||
"registration": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -10588,13 +10542,13 @@
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"expires_at": {
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"max_uses": {"nullable": true, "type": "integer", "minimum": 1, "maximum": 9007199254740991},
|
||||
"use_count": {"type": "integer", "minimum": 0, "maximum": 9007199254740991},
|
||||
@@ -10602,14 +10556,14 @@
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"approval_required": {"type": "boolean"},
|
||||
"last_used_at": {
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"last_used_by_user_id": {
|
||||
"nullable": true,
|
||||
@@ -10645,7 +10599,7 @@
|
||||
"requested_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"registration_url_id": {"nullable": true, "type": "string", "minLength": 1, "maxLength": 128},
|
||||
"client_ip": {"nullable": true, "type": "string"}
|
||||
@@ -10680,7 +10634,9 @@
|
||||
"logo_url": {"nullable": true, "type": "string"},
|
||||
"wordmark_url": {"nullable": true, "type": "string"},
|
||||
"favicon_url": {"nullable": true, "type": "string"},
|
||||
"theme_color": {"nullable": true, "type": "string"}
|
||||
"theme_color": {"nullable": true, "type": "string"},
|
||||
"status_page_url": {"nullable": true, "type": "string"},
|
||||
"status_page_incident_history_url": {"nullable": true, "type": "string"}
|
||||
},
|
||||
"required": [
|
||||
"product_name",
|
||||
@@ -10689,7 +10645,9 @@
|
||||
"logo_url",
|
||||
"wordmark_url",
|
||||
"favicon_url",
|
||||
"theme_color"
|
||||
"theme_color",
|
||||
"status_page_url",
|
||||
"status_page_incident_history_url"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
@@ -10898,12 +10856,14 @@
|
||||
"min_lifetime_days": {
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true,
|
||||
"maximum": 9007199254740991
|
||||
},
|
||||
"max_lifetime_days": {
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true,
|
||||
"maximum": 9007199254740991
|
||||
},
|
||||
@@ -10911,12 +10871,14 @@
|
||||
"renew_threshold_days": {
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true,
|
||||
"maximum": 9007199254740991
|
||||
},
|
||||
"renew_window_days": {
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true,
|
||||
"maximum": 9007199254740991
|
||||
},
|
||||
@@ -10927,15 +10889,31 @@
|
||||
"min_size_mb": {"type": "number", "minimum": 0, "exclusiveMinimum": true},
|
||||
"max_size_mb": {"type": "number", "minimum": 0, "exclusiveMinimum": true},
|
||||
"max_eligible_size_mb": {"type": "number", "minimum": 0, "exclusiveMinimum": true},
|
||||
"min_lifetime_days": {"type": "integer", "exclusiveMinimum": true, "maximum": 9007199254740991},
|
||||
"max_lifetime_days": {"type": "integer", "exclusiveMinimum": true, "maximum": 9007199254740991},
|
||||
"curve": {"type": "number", "minimum": 0, "maximum": 1},
|
||||
"renew_threshold_days": {
|
||||
"min_lifetime_days": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true,
|
||||
"maximum": 9007199254740991
|
||||
},
|
||||
"renew_window_days": {"type": "integer", "exclusiveMinimum": true, "maximum": 9007199254740991}
|
||||
"max_lifetime_days": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true,
|
||||
"maximum": 9007199254740991
|
||||
},
|
||||
"curve": {"type": "number", "minimum": 0, "maximum": 1},
|
||||
"renew_threshold_days": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true,
|
||||
"maximum": 9007199254740991
|
||||
},
|
||||
"renew_window_days": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
@@ -10974,14 +10952,8 @@
|
||||
"sso",
|
||||
"gateway_rollout",
|
||||
"voice_noise_suppression",
|
||||
"guild_activity_log_presentation",
|
||||
"screen_share_delivery",
|
||||
"experiment_delivery",
|
||||
"message_hover_tracking",
|
||||
"message_keyboard_focus",
|
||||
"blocked_message_groups",
|
||||
"expression_info_card",
|
||||
"guild_header_collapse",
|
||||
"typing_indicator_rework",
|
||||
"registration",
|
||||
"self_hosted",
|
||||
"app_public",
|
||||
@@ -10999,7 +10971,7 @@
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"max_uses": {"nullable": true, "type": "integer", "minimum": 1, "maximum": 1000000},
|
||||
"approval_required": {"default": false, "type": "boolean"}
|
||||
@@ -11017,13 +10989,13 @@
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"expires_at": {
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"max_uses": {"nullable": true, "type": "integer", "minimum": 1, "maximum": 9007199254740991},
|
||||
"use_count": {"type": "integer", "minimum": 0, "maximum": 9007199254740991},
|
||||
@@ -11031,14 +11003,14 @@
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"approval_required": {"type": "boolean"},
|
||||
"last_used_at": {
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"last_used_by_user_id": {
|
||||
"nullable": true,
|
||||
@@ -11115,38 +11087,14 @@
|
||||
"nullable": true,
|
||||
"allOf": [{"$ref": "#/components/schemas/VoiceNoiseSuppressionConfigUpdateRequest"}]
|
||||
},
|
||||
"guild_activity_log_presentation": {
|
||||
"screen_share_delivery": {
|
||||
"nullable": true,
|
||||
"allOf": [{"$ref": "#/components/schemas/GuildActivityLogPresentationConfigUpdateRequest"}]
|
||||
"allOf": [{"$ref": "#/components/schemas/ScreenShareDeliveryConfigUpdateRequest"}]
|
||||
},
|
||||
"experiment_delivery": {
|
||||
"nullable": true,
|
||||
"allOf": [{"$ref": "#/components/schemas/ExperimentDeliveryConfigUpdateRequest"}]
|
||||
},
|
||||
"message_hover_tracking": {
|
||||
"nullable": true,
|
||||
"allOf": [{"$ref": "#/components/schemas/MessageHoverTrackingConfigUpdateRequest"}]
|
||||
},
|
||||
"message_keyboard_focus": {
|
||||
"nullable": true,
|
||||
"allOf": [{"$ref": "#/components/schemas/MessageKeyboardFocusConfigUpdateRequest"}]
|
||||
},
|
||||
"blocked_message_groups": {
|
||||
"nullable": true,
|
||||
"allOf": [{"$ref": "#/components/schemas/BlockedMessageGroupsConfigUpdateRequest"}]
|
||||
},
|
||||
"expression_info_card": {
|
||||
"nullable": true,
|
||||
"allOf": [{"$ref": "#/components/schemas/ExpressionInfoCardConfigUpdateRequest"}]
|
||||
},
|
||||
"guild_header_collapse": {
|
||||
"nullable": true,
|
||||
"allOf": [{"$ref": "#/components/schemas/GuildHeaderCollapseConfigUpdateRequest"}]
|
||||
},
|
||||
"typing_indicator_rework": {
|
||||
"nullable": true,
|
||||
"allOf": [{"$ref": "#/components/schemas/TypingIndicatorReworkConfigUpdateRequest"}]
|
||||
},
|
||||
"registration": {
|
||||
"nullable": true,
|
||||
"type": "object",
|
||||
@@ -11188,7 +11136,9 @@
|
||||
"logo_url": {"nullable": true, "type": "string", "maxLength": 2048},
|
||||
"wordmark_url": {"nullable": true, "type": "string", "maxLength": 2048},
|
||||
"favicon_url": {"nullable": true, "type": "string", "maxLength": 2048},
|
||||
"theme_color": {"nullable": true, "type": "string", "maxLength": 64}
|
||||
"theme_color": {"nullable": true, "type": "string", "maxLength": 64},
|
||||
"status_page_url": {"nullable": true, "type": "string", "maxLength": 2048},
|
||||
"status_page_incident_history_url": {"nullable": true, "type": "string", "maxLength": 2048}
|
||||
}
|
||||
},
|
||||
"setup": {"nullable": true, "type": "object", "properties": {"configured": {"type": "boolean"}}},
|
||||
@@ -11298,12 +11248,14 @@
|
||||
"min_lifetime_days": {
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true,
|
||||
"maximum": 9007199254740991
|
||||
},
|
||||
"max_lifetime_days": {
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true,
|
||||
"maximum": 9007199254740991
|
||||
},
|
||||
@@ -11311,12 +11263,14 @@
|
||||
"renew_threshold_days": {
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true,
|
||||
"maximum": 9007199254740991
|
||||
},
|
||||
"renew_window_days": {
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
@@ -11348,7 +11302,7 @@
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"window_hours": {"type": "number", "minimum": 0, "exclusiveMinimum": true, "maximum": 8760},
|
||||
"member_threshold": {"type": "integer", "exclusiveMinimum": true, "maximum": 1000000}
|
||||
"member_threshold": {"type": "integer", "minimum": 0, "exclusiveMinimum": true, "maximum": 1000000}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12665,6 +12619,16 @@
|
||||
}
|
||||
},
|
||||
"action": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||
"access": {
|
||||
"description": "Whether the recorded operation read data or changed it",
|
||||
"x-enumNames": ["read", "write"],
|
||||
"x-enumDescriptions": [
|
||||
"An entry recorded by an operation that only reads data",
|
||||
"An entry recorded by an operation that changes data or triggers work"
|
||||
],
|
||||
"enum": ["read", "write"],
|
||||
"type": "string"
|
||||
},
|
||||
"audit_log_reason": {"nullable": true, "type": "string", "minLength": 1, "maxLength": 4000},
|
||||
"metadata": {"type": "object", "additionalProperties": {"type": "string", "maxLength": 4000}},
|
||||
"created_at": {"type": "string"}
|
||||
@@ -12682,6 +12646,7 @@
|
||||
"related_guilds",
|
||||
"related_channels",
|
||||
"action",
|
||||
"access",
|
||||
"audit_log_reason",
|
||||
"metadata",
|
||||
"created_at"
|
||||
@@ -12770,7 +12735,7 @@
|
||||
},
|
||||
"acls": {
|
||||
"description": "Replacement list of access control permissions for the key",
|
||||
"maxItems": 112,
|
||||
"maxItems": 111,
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/AdminAclType"}
|
||||
}
|
||||
@@ -12788,7 +12753,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"acls": {
|
||||
"maxItems": 112,
|
||||
"maxItems": 111,
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of access control permissions for the key"
|
||||
@@ -12818,7 +12783,7 @@
|
||||
"maximum": 365
|
||||
},
|
||||
"acls": {
|
||||
"maxItems": 112,
|
||||
"maxItems": 111,
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/AdminAclType"},
|
||||
"description": "List of access control permissions for the key"
|
||||
@@ -12839,7 +12804,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"acls": {
|
||||
"maxItems": 112,
|
||||
"maxItems": 111,
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of access control permissions for the key"
|
||||
@@ -12852,7 +12817,7 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"acls": {
|
||||
"maxItems": 112,
|
||||
"maxItems": 111,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||
"description": "Every admin access control permission the admin API recognises"
|
||||
@@ -12943,7 +12908,6 @@
|
||||
"report:view",
|
||||
"report:view:reporter_pii",
|
||||
"system_dm:send",
|
||||
"system:heap_snapshot",
|
||||
"user:cancel:bulk_message_deletion",
|
||||
"user:delete",
|
||||
"user:disable:suspicious",
|
||||
@@ -13055,14 +13019,14 @@
|
||||
"description": "ISO 8601 timestamp when the bot token was created",
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"client_secret_created_at": {
|
||||
"nullable": true,
|
||||
"description": "ISO 8601 timestamp when the client secret was created",
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"version": {
|
||||
"description": "The optimistic locking version of the application record",
|
||||
@@ -13490,7 +13454,7 @@
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
|
||||
"description": "The ISO 8601 timestamp of when the message was created"
|
||||
},
|
||||
"edited_timestamp": {
|
||||
@@ -13498,7 +13462,7 @@
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"pinned": {"type": "boolean", "description": "Whether the message is pinned"},
|
||||
"mention_everyone": {"type": "boolean", "description": "Whether the message mentions @everyone"},
|
||||
@@ -13598,7 +13562,7 @@
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
}
|
||||
},
|
||||
"required": ["participants"],
|
||||
@@ -13635,7 +13599,7 @@
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
|
||||
"description": "The ISO 8601 timestamp of when the message was created"
|
||||
},
|
||||
"edited_timestamp": {
|
||||
@@ -13643,7 +13607,7 @@
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"pinned": {"type": "boolean", "description": "Whether the message is pinned"},
|
||||
"mention_everyone": {"type": "boolean", "description": "Whether the message mentions @everyone"},
|
||||
@@ -13743,7 +13707,7 @@
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
}
|
||||
},
|
||||
"required": ["participants"],
|
||||
@@ -13907,7 +13871,7 @@
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
|
||||
"description": "The ISO 8601 timestamp of when the original message was created"
|
||||
},
|
||||
"edited_timestamp": {
|
||||
@@ -13915,7 +13879,7 @@
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"mentions": {
|
||||
"description": "The user IDs mentioned in the snapshot",
|
||||
@@ -14084,7 +14048,7 @@
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"description": {"description": "The description of the embed", "nullable": true, "type": "string"},
|
||||
"author": {
|
||||
@@ -14296,7 +14260,7 @@
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"description": {"description": "The description of the embed", "nullable": true, "type": "string"},
|
||||
"author": {
|
||||
@@ -15168,7 +15132,7 @@
|
||||
"joined_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
|
||||
"description": "ISO8601 timestamp of when the user joined the guild"
|
||||
},
|
||||
"mute": {"type": "boolean", "description": "Whether the member is muted in voice channels"},
|
||||
@@ -15178,7 +15142,7 @@
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"profile_flags": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/GuildMemberProfileFlags"}]},
|
||||
"mention_flags": {
|
||||
@@ -15213,114 +15177,6 @@
|
||||
"enum": ["open", "approval", "closed"],
|
||||
"type": "string"
|
||||
},
|
||||
"TypingIndicatorReworkConfigUpdateRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"rollout_basis_points": {"type": "integer", "minimum": 0, "maximum": 10000},
|
||||
"rollout_salt": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||
"included_user_ids": {
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
},
|
||||
"excluded_user_ids": {
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"GuildHeaderCollapseConfigUpdateRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"rollout_basis_points": {"type": "integer", "minimum": 0, "maximum": 10000},
|
||||
"rollout_salt": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||
"included_user_ids": {
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
},
|
||||
"excluded_user_ids": {
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ExpressionInfoCardConfigUpdateRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"rollout_basis_points": {"type": "integer", "minimum": 0, "maximum": 10000},
|
||||
"rollout_salt": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||
"included_user_ids": {
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
},
|
||||
"excluded_user_ids": {
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"BlockedMessageGroupsConfigUpdateRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"rollout_basis_points": {"type": "integer", "minimum": 0, "maximum": 10000},
|
||||
"rollout_salt": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||
"included_user_ids": {
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
},
|
||||
"excluded_user_ids": {
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"MessageKeyboardFocusConfigUpdateRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"rollout_basis_points": {"type": "integer", "minimum": 0, "maximum": 10000},
|
||||
"rollout_salt": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||
"included_user_ids": {
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
},
|
||||
"excluded_user_ids": {
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"MessageHoverTrackingConfigUpdateRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"rollout_basis_points": {"type": "integer", "minimum": 0, "maximum": 10000},
|
||||
"rollout_salt": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||
"included_user_ids": {
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
},
|
||||
"excluded_user_ids": {
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ExperimentDeliveryConfigUpdateRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -15328,7 +15184,7 @@
|
||||
"poll_jitter_percent": {"type": "integer", "minimum": 0, "maximum": 50}
|
||||
}
|
||||
},
|
||||
"GuildActivityLogPresentationConfigUpdateRequest": {
|
||||
"ScreenShareDeliveryConfigUpdateRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
@@ -15381,7 +15237,6 @@
|
||||
"required": ["guild_id", "backend"]
|
||||
}
|
||||
},
|
||||
"stereo_enabled": {"type": "boolean"},
|
||||
"suppression_strength": {"type": "integer", "minimum": 0, "maximum": 100}
|
||||
}
|
||||
},
|
||||
@@ -15403,186 +15258,6 @@
|
||||
"type": "string",
|
||||
"enum": ["none", "standard", "gate", "speex", "rnnoise", "gtcrn", "deep_filter"]
|
||||
},
|
||||
"TypingIndicatorReworkConfigResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"default": false, "type": "boolean"},
|
||||
"config_version": {"default": 0, "type": "integer", "minimum": 0, "maximum": 9007199254740991},
|
||||
"rollout_basis_points": {"default": 0, "type": "integer", "minimum": 0, "maximum": 10000},
|
||||
"rollout_salt": {"default": "typing-indicator-rework-v1", "type": "string", "minLength": 1, "maxLength": 64},
|
||||
"included_user_ids": {
|
||||
"default": [],
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
},
|
||||
"excluded_user_ids": {
|
||||
"default": [],
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"config_version",
|
||||
"rollout_basis_points",
|
||||
"rollout_salt",
|
||||
"included_user_ids",
|
||||
"excluded_user_ids"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"GuildHeaderCollapseConfigResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"default": false, "type": "boolean"},
|
||||
"config_version": {"default": 0, "type": "integer", "minimum": 0, "maximum": 9007199254740991},
|
||||
"rollout_basis_points": {"default": 0, "type": "integer", "minimum": 0, "maximum": 10000},
|
||||
"rollout_salt": {"default": "guild-header-collapse-v1", "type": "string", "minLength": 1, "maxLength": 64},
|
||||
"included_user_ids": {
|
||||
"default": [],
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
},
|
||||
"excluded_user_ids": {
|
||||
"default": [],
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"config_version",
|
||||
"rollout_basis_points",
|
||||
"rollout_salt",
|
||||
"included_user_ids",
|
||||
"excluded_user_ids"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ExpressionInfoCardConfigResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"default": false, "type": "boolean"},
|
||||
"config_version": {"default": 0, "type": "integer", "minimum": 0, "maximum": 9007199254740991},
|
||||
"rollout_basis_points": {"default": 0, "type": "integer", "minimum": 0, "maximum": 10000},
|
||||
"rollout_salt": {"default": "expression-info-card-v1", "type": "string", "minLength": 1, "maxLength": 64},
|
||||
"included_user_ids": {
|
||||
"default": [],
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
},
|
||||
"excluded_user_ids": {
|
||||
"default": [],
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"config_version",
|
||||
"rollout_basis_points",
|
||||
"rollout_salt",
|
||||
"included_user_ids",
|
||||
"excluded_user_ids"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"BlockedMessageGroupsConfigResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"default": false, "type": "boolean"},
|
||||
"config_version": {"default": 0, "type": "integer", "minimum": 0, "maximum": 9007199254740991},
|
||||
"rollout_basis_points": {"default": 0, "type": "integer", "minimum": 0, "maximum": 10000},
|
||||
"rollout_salt": {"default": "blocked-message-groups-v1", "type": "string", "minLength": 1, "maxLength": 64},
|
||||
"included_user_ids": {
|
||||
"default": [],
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
},
|
||||
"excluded_user_ids": {
|
||||
"default": [],
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"config_version",
|
||||
"rollout_basis_points",
|
||||
"rollout_salt",
|
||||
"included_user_ids",
|
||||
"excluded_user_ids"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"MessageKeyboardFocusConfigResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"default": false, "type": "boolean"},
|
||||
"config_version": {"default": 0, "type": "integer", "minimum": 0, "maximum": 9007199254740991},
|
||||
"rollout_basis_points": {"default": 0, "type": "integer", "minimum": 0, "maximum": 10000},
|
||||
"rollout_salt": {"default": "message-keyboard-focus-v1", "type": "string", "minLength": 1, "maxLength": 64},
|
||||
"included_user_ids": {
|
||||
"default": [],
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
},
|
||||
"excluded_user_ids": {
|
||||
"default": [],
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"config_version",
|
||||
"rollout_basis_points",
|
||||
"rollout_salt",
|
||||
"included_user_ids",
|
||||
"excluded_user_ids"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"MessageHoverTrackingConfigResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"default": false, "type": "boolean"},
|
||||
"config_version": {"default": 0, "type": "integer", "minimum": 0, "maximum": 9007199254740991},
|
||||
"rollout_basis_points": {"default": 0, "type": "integer", "minimum": 0, "maximum": 10000},
|
||||
"rollout_salt": {"default": "message-hover-tracking-v1", "type": "string", "minLength": 1, "maxLength": 64},
|
||||
"included_user_ids": {
|
||||
"default": [],
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
},
|
||||
"excluded_user_ids": {
|
||||
"default": [],
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"config_version",
|
||||
"rollout_basis_points",
|
||||
"rollout_salt",
|
||||
"included_user_ids",
|
||||
"excluded_user_ids"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ExperimentDeliveryConfigResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -15592,18 +15267,13 @@
|
||||
"required": ["poll_interval_seconds", "poll_jitter_percent"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"GuildActivityLogPresentationConfigResponse": {
|
||||
"ScreenShareDeliveryConfigResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"default": false, "type": "boolean"},
|
||||
"config_version": {"default": 0, "type": "integer", "minimum": 0, "maximum": 9007199254740991},
|
||||
"rollout_basis_points": {"default": 0, "type": "integer", "minimum": 0, "maximum": 10000},
|
||||
"rollout_salt": {
|
||||
"default": "guild-activity-log-presentation-v1",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 64
|
||||
},
|
||||
"rollout_salt": {"default": "screen-share-delivery-v1", "type": "string", "minLength": 1, "maxLength": 64},
|
||||
"included_user_ids": {
|
||||
"default": [],
|
||||
"maxItems": 1000,
|
||||
@@ -15671,7 +15341,6 @@
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"stereo_enabled": {"default": false, "type": "boolean"},
|
||||
"suppression_strength": {"default": 80, "type": "integer", "minimum": 0, "maximum": 100}
|
||||
},
|
||||
"required": [
|
||||
@@ -15685,7 +15354,6 @@
|
||||
"included_user_ids",
|
||||
"excluded_user_ids",
|
||||
"guild_overrides",
|
||||
"stereo_enabled",
|
||||
"suppression_strength"
|
||||
],
|
||||
"additionalProperties": false
|
||||
@@ -15823,7 +15491,7 @@
|
||||
"pending_bulk_message_deletion_at": {"nullable": true, "type": "string"},
|
||||
"deletion_reason_code": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/Int32Type"}]},
|
||||
"deletion_public_reason": {"nullable": true, "type": "string"},
|
||||
"acls": {"maxItems": 112, "type": "array", "items": {"type": "string"}},
|
||||
"acls": {"maxItems": 111, "type": "array", "items": {"type": "string"}},
|
||||
"traits": {"maxItems": 100, "type": "array", "items": {"type": "string"}},
|
||||
"has_totp": {"type": "boolean"},
|
||||
"authenticator_types": {"maxItems": 10, "type": "array", "items": {"$ref": "#/components/schemas/Int32Type"}},
|
||||
|
||||
@@ -79,7 +79,6 @@ pub const REPORT_RESOLVE: &str = "report:resolve";
|
||||
pub const REPORT_VIEW: &str = "report:view";
|
||||
pub const REPORT_VIEW_REPORTER_PII: &str = "report:view:reporter_pii";
|
||||
pub const SYSTEM_DM_SEND: &str = "system_dm:send";
|
||||
pub const SYSTEM_HEAP_SNAPSHOT: &str = "system:heap_snapshot";
|
||||
pub const USER_CANCEL_BULK_MESSAGE_DELETION: &str = "user:cancel:bulk_message_deletion";
|
||||
pub const USER_DELETE: &str = "user:delete";
|
||||
pub const USER_DISABLE_SUSPICIOUS: &str = "user:disable:suspicious";
|
||||
@@ -192,7 +191,6 @@ pub const ALL_ACLS: &[&str] = &[
|
||||
REPORT_VIEW,
|
||||
REPORT_VIEW_REPORTER_PII,
|
||||
SYSTEM_DM_SEND,
|
||||
SYSTEM_HEAP_SNAPSHOT,
|
||||
USER_CANCEL_BULK_MESSAGE_DELETION,
|
||||
USER_DELETE,
|
||||
USER_DISABLE_SUSPICIOUS,
|
||||
|
||||
@@ -10,6 +10,7 @@ pub struct SearchAuditLogsParams {
|
||||
pub admin_user_id: Option<String>,
|
||||
pub target_id: Option<String>,
|
||||
pub target_type: Option<String>,
|
||||
pub access: Option<String>,
|
||||
pub sort_by: Option<String>,
|
||||
pub sort_order: Option<String>,
|
||||
pub limit: u32,
|
||||
@@ -21,6 +22,12 @@ impl AdminApiClient {
|
||||
&self,
|
||||
params: &SearchAuditLogsParams,
|
||||
) -> ApiResult<AuditLogsListResponse> {
|
||||
let access = params
|
||||
.access
|
||||
.as_deref()
|
||||
.map(audit_access)
|
||||
.transpose()?
|
||||
.map(|value| value.to_string());
|
||||
let sort_by = params
|
||||
.sort_by
|
||||
.as_deref()
|
||||
@@ -46,6 +53,7 @@ impl AdminApiClient {
|
||||
params.target_type.as_deref().unwrap_or_default(),
|
||||
),
|
||||
("target_id", params.target_id.as_deref().unwrap_or_default()),
|
||||
("access", access.as_deref().unwrap_or_default()),
|
||||
("sort_by", sort_by.as_deref().unwrap_or_default()),
|
||||
("sort_order", sort_order.as_deref().unwrap_or_default()),
|
||||
("limit", limit.as_str()),
|
||||
@@ -55,6 +63,11 @@ impl AdminApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
fn audit_access(value: &str) -> ApiResult<generated_types::ListAdminAuditLogsAccess> {
|
||||
generated_types::ListAdminAuditLogsAccess::try_from(value)
|
||||
.map_err(|e| ApiError::Parse(e.to_string()))
|
||||
}
|
||||
|
||||
fn audit_sort_by(value: &str) -> ApiResult<generated_types::ListAdminAuditLogsSortBy> {
|
||||
let value = match value {
|
||||
"created_at" => "createdAt",
|
||||
@@ -83,6 +96,13 @@ mod tests {
|
||||
assert_eq!(audit_sort_order("desc").unwrap().to_string(), "desc");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_only_known_access_filters() {
|
||||
assert_eq!(audit_access("read").unwrap().to_string(), "read");
|
||||
assert_eq!(audit_access("write").unwrap().to_string(), "write");
|
||||
assert!(audit_access("all").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_lossy_audit_totals() {
|
||||
for total in [serde_json::json!(1.5), serde_json::json!(-1)] {
|
||||
@@ -99,6 +119,7 @@ mod tests {
|
||||
"admin_user_id": "234567890123456789",
|
||||
"admin_user": null,
|
||||
"action": "USER_UPDATE",
|
||||
"access": "write",
|
||||
"target_id": "345678901234567890",
|
||||
"target_type": "user",
|
||||
"target_user": null,
|
||||
@@ -118,6 +139,26 @@ mod tests {
|
||||
assert_eq!(generated.logs[0].action.to_string(), "USER_UPDATE");
|
||||
|
||||
let response: AuditLogsListResponse = serde_json::from_value(json.clone()).unwrap();
|
||||
assert_eq!(response.logs[0].access.as_deref(), Some("write"));
|
||||
assert_eq!(serde_json::to_value(response).unwrap(), json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserializes_audit_entries_from_an_api_without_access() {
|
||||
let json = serde_json::json!({
|
||||
"logs": [{
|
||||
"log_id": "123456789012345678",
|
||||
"admin_user_id": "234567890123456789",
|
||||
"action": "USER_UPDATE",
|
||||
"target_id": "345678901234567890",
|
||||
"target_type": "user",
|
||||
"audit_log_reason": null,
|
||||
"metadata": {},
|
||||
"created_at": "2026-09-11T12:00:00.000Z"
|
||||
}],
|
||||
"total": 1
|
||||
});
|
||||
let response: AuditLogsListResponse = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(response.logs[0].access, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ impl AdminApiClient {
|
||||
audit_log_reason: Option<&str>,
|
||||
) -> ApiResult<BulkJobResponse> {
|
||||
let body = generated_types::AdminBulkJobCreateRequest::UpdateUserFlags {
|
||||
add_flags: user_flags(add_flags),
|
||||
remove_flags: user_flags(remove_flags),
|
||||
add_flags: user_flags(add_flags)?,
|
||||
remove_flags: user_flags(remove_flags)?,
|
||||
user_ids: snowflakes(user_ids),
|
||||
};
|
||||
self.post_typed_with_reason("/admin/bulk-jobs", &body, audit_log_reason)
|
||||
@@ -112,11 +112,13 @@ fn snowflakes(values: &[String]) -> Vec<generated_types::SnowflakeType> {
|
||||
values.iter().map(|value| snowflake(value)).collect()
|
||||
}
|
||||
|
||||
fn user_flags(values: &[String]) -> Vec<generated_types::UserFlags> {
|
||||
fn user_flags(values: &[String]) -> ApiResult<Vec<generated_types::UserFlags>> {
|
||||
values
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(generated_types::UserFlags::from)
|
||||
.map(|value| {
|
||||
generated_types::UserFlags::try_from(value.as_str())
|
||||
.map_err(|error| ApiError::Parse(error.to_string()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ pub struct AuditLogEntry {
|
||||
#[serde(default)]
|
||||
pub admin_user: Option<AuditLogUserSummary>,
|
||||
pub action: String,
|
||||
#[serde(default)]
|
||||
pub access: Option<String>,
|
||||
pub target_id: String,
|
||||
pub target_type: String,
|
||||
#[serde(default)]
|
||||
|
||||
@@ -23,21 +23,9 @@ pub struct InstanceConfigResponse {
|
||||
#[serde(default)]
|
||||
pub voice_noise_suppression: VoiceNoiseSuppressionConfigResponse,
|
||||
#[serde(default)]
|
||||
pub screen_share_delivery: ScreenShareDeliveryConfigResponse,
|
||||
#[serde(default)]
|
||||
pub experiment_delivery: ExperimentDeliveryConfigResponse,
|
||||
#[serde(default)]
|
||||
pub message_hover_tracking: MessageHoverTrackingConfigResponse,
|
||||
#[serde(default)]
|
||||
pub message_keyboard_focus: MessageKeyboardFocusConfigResponse,
|
||||
#[serde(default)]
|
||||
pub blocked_message_groups: BlockedMessageGroupsConfigResponse,
|
||||
#[serde(default)]
|
||||
pub guild_activity_log_presentation: GuildActivityLogPresentationConfigResponse,
|
||||
#[serde(default)]
|
||||
pub expression_info_card: ExpressionInfoCardConfigResponse,
|
||||
#[serde(default)]
|
||||
pub guild_header_collapse: GuildHeaderCollapseConfigResponse,
|
||||
#[serde(default)]
|
||||
pub typing_indicator_rework: TypingIndicatorReworkConfigResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
@@ -340,6 +328,8 @@ pub struct AppBrandingConfigResponse {
|
||||
pub wordmark_url: Option<String>,
|
||||
pub favicon_url: Option<String>,
|
||||
pub theme_color: Option<String>,
|
||||
pub status_page_url: Option<String>,
|
||||
pub status_page_incident_history_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for AppBrandingConfigResponse {
|
||||
@@ -352,6 +342,8 @@ impl Default for AppBrandingConfigResponse {
|
||||
wordmark_url: None,
|
||||
favicon_url: None,
|
||||
theme_color: None,
|
||||
status_page_url: None,
|
||||
status_page_incident_history_url: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -456,7 +448,8 @@ impl VoiceE2eeScope {
|
||||
}
|
||||
}
|
||||
|
||||
pub const VOICE_NS_MAX_TARGETED_USERS: usize = 1_000;
|
||||
pub const EXPERIMENT_MAX_TARGETED_USERS: usize = 1_000;
|
||||
pub const SCREEN_SHARE_DELIVERY_DEFAULT_SALT: &str = "screen-share-delivery-v1";
|
||||
pub const VOICE_NS_MAX_GUILD_OVERRIDES: usize = 200;
|
||||
|
||||
impl NoiseSuppressionBackend {
|
||||
@@ -502,7 +495,6 @@ pub struct VoiceNoiseSuppressionConfigResponse {
|
||||
pub included_user_ids: Vec<String>,
|
||||
pub excluded_user_ids: Vec<String>,
|
||||
pub guild_overrides: Vec<VoiceNoiseSuppressionGuildOverride>,
|
||||
pub stereo_enabled: bool,
|
||||
pub suppression_strength: u32,
|
||||
}
|
||||
|
||||
@@ -519,7 +511,6 @@ impl Default for VoiceNoiseSuppressionConfigResponse {
|
||||
included_user_ids: Vec::new(),
|
||||
excluded_user_ids: Vec::new(),
|
||||
guild_overrides: Vec::new(),
|
||||
stereo_enabled: false,
|
||||
suppression_strength: 80,
|
||||
}
|
||||
}
|
||||
@@ -546,14 +537,12 @@ pub struct VoiceNoiseSuppressionConfigUpdateRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub guild_overrides: Option<Vec<VoiceNoiseSuppressionGuildOverride>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stereo_enabled: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub suppression_strength: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct MessageHoverTrackingConfigResponse {
|
||||
pub struct ScreenShareDeliveryConfigResponse {
|
||||
pub enabled: bool,
|
||||
pub config_version: u64,
|
||||
pub rollout_basis_points: u32,
|
||||
@@ -562,13 +551,13 @@ pub struct MessageHoverTrackingConfigResponse {
|
||||
pub excluded_user_ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for MessageHoverTrackingConfigResponse {
|
||||
impl Default for ScreenShareDeliveryConfigResponse {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
config_version: 0,
|
||||
rollout_basis_points: 0,
|
||||
rollout_salt: "message-hover-tracking-v1".to_owned(),
|
||||
rollout_salt: SCREEN_SHARE_DELIVERY_DEFAULT_SALT.to_owned(),
|
||||
included_user_ids: Vec::new(),
|
||||
excluded_user_ids: Vec::new(),
|
||||
}
|
||||
@@ -576,235 +565,7 @@ impl Default for MessageHoverTrackingConfigResponse {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct MessageHoverTrackingConfigUpdateRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rollout_basis_points: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rollout_salt: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub included_user_ids: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub excluded_user_ids: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct MessageKeyboardFocusConfigResponse {
|
||||
pub enabled: bool,
|
||||
pub config_version: u64,
|
||||
pub rollout_basis_points: u32,
|
||||
pub rollout_salt: String,
|
||||
pub included_user_ids: Vec<String>,
|
||||
pub excluded_user_ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for MessageKeyboardFocusConfigResponse {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
config_version: 0,
|
||||
rollout_basis_points: 0,
|
||||
rollout_salt: "message-keyboard-focus-v1".to_owned(),
|
||||
included_user_ids: Vec::new(),
|
||||
excluded_user_ids: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct MessageKeyboardFocusConfigUpdateRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rollout_basis_points: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rollout_salt: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub included_user_ids: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub excluded_user_ids: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct BlockedMessageGroupsConfigResponse {
|
||||
pub enabled: bool,
|
||||
pub config_version: u64,
|
||||
pub rollout_basis_points: u32,
|
||||
pub rollout_salt: String,
|
||||
pub included_user_ids: Vec<String>,
|
||||
pub excluded_user_ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for BlockedMessageGroupsConfigResponse {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
config_version: 0,
|
||||
rollout_basis_points: 0,
|
||||
rollout_salt: "blocked-message-groups-v1".to_owned(),
|
||||
included_user_ids: Vec::new(),
|
||||
excluded_user_ids: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct BlockedMessageGroupsConfigUpdateRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rollout_basis_points: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rollout_salt: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub included_user_ids: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub excluded_user_ids: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct GuildActivityLogPresentationConfigResponse {
|
||||
pub enabled: bool,
|
||||
pub config_version: u64,
|
||||
pub rollout_basis_points: u32,
|
||||
pub rollout_salt: String,
|
||||
pub included_user_ids: Vec<String>,
|
||||
pub excluded_user_ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for GuildActivityLogPresentationConfigResponse {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
config_version: 0,
|
||||
rollout_basis_points: 0,
|
||||
rollout_salt: "guild-activity-log-presentation-v1".to_owned(),
|
||||
included_user_ids: Vec::new(),
|
||||
excluded_user_ids: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct GuildActivityLogPresentationConfigUpdateRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rollout_basis_points: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rollout_salt: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub included_user_ids: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub excluded_user_ids: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct ExpressionInfoCardConfigResponse {
|
||||
pub enabled: bool,
|
||||
pub config_version: u64,
|
||||
pub rollout_basis_points: u32,
|
||||
pub rollout_salt: String,
|
||||
pub included_user_ids: Vec<String>,
|
||||
pub excluded_user_ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for ExpressionInfoCardConfigResponse {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
config_version: 0,
|
||||
rollout_basis_points: 0,
|
||||
rollout_salt: "expression-info-card-v1".to_owned(),
|
||||
included_user_ids: Vec::new(),
|
||||
excluded_user_ids: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct ExpressionInfoCardConfigUpdateRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rollout_basis_points: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rollout_salt: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub included_user_ids: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub excluded_user_ids: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct GuildHeaderCollapseConfigResponse {
|
||||
pub enabled: bool,
|
||||
pub config_version: u64,
|
||||
pub rollout_basis_points: u32,
|
||||
pub rollout_salt: String,
|
||||
pub included_user_ids: Vec<String>,
|
||||
pub excluded_user_ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for GuildHeaderCollapseConfigResponse {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
config_version: 0,
|
||||
rollout_basis_points: 0,
|
||||
rollout_salt: "guild-header-collapse-v1".to_owned(),
|
||||
included_user_ids: Vec::new(),
|
||||
excluded_user_ids: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct GuildHeaderCollapseConfigUpdateRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rollout_basis_points: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rollout_salt: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub included_user_ids: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub excluded_user_ids: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct TypingIndicatorReworkConfigResponse {
|
||||
pub enabled: bool,
|
||||
pub config_version: u64,
|
||||
pub rollout_basis_points: u32,
|
||||
pub rollout_salt: String,
|
||||
pub included_user_ids: Vec<String>,
|
||||
pub excluded_user_ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for TypingIndicatorReworkConfigResponse {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
config_version: 0,
|
||||
rollout_basis_points: 0,
|
||||
rollout_salt: "typing-indicator-rework-v1".to_owned(),
|
||||
included_user_ids: Vec::new(),
|
||||
excluded_user_ids: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct TypingIndicatorReworkConfigUpdateRequest {
|
||||
pub struct ScreenShareDeliveryConfigUpdateRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -933,21 +694,9 @@ pub struct InstanceConfigUpdateRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub voice_noise_suppression: Option<VoiceNoiseSuppressionConfigUpdateRequest>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub screen_share_delivery: Option<ScreenShareDeliveryConfigUpdateRequest>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub experiment_delivery: Option<ExperimentDeliveryConfigUpdateRequest>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub message_hover_tracking: Option<MessageHoverTrackingConfigUpdateRequest>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub message_keyboard_focus: Option<MessageKeyboardFocusConfigUpdateRequest>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub blocked_message_groups: Option<BlockedMessageGroupsConfigUpdateRequest>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub guild_activity_log_presentation: Option<GuildActivityLogPresentationConfigUpdateRequest>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub expression_info_card: Option<ExpressionInfoCardConfigUpdateRequest>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub guild_header_collapse: Option<GuildHeaderCollapseConfigUpdateRequest>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub typing_indicator_rework: Option<TypingIndicatorReworkConfigUpdateRequest>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
@@ -1152,6 +901,10 @@ pub struct AppBrandingConfigUpdateRequest {
|
||||
pub favicon_url: Option<Option<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub theme_color: Option<Option<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub status_page_url: Option<Option<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub status_page_incident_history_url: Option<Option<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
@@ -1280,118 +1033,39 @@ mod tests {
|
||||
.expect("admin schema");
|
||||
let noise = serde_json::from_value::<VoiceNoiseSuppressionConfigResponse>(json!({}))
|
||||
.expect("default noise config");
|
||||
let screen_share = serde_json::from_value::<ScreenShareDeliveryConfigResponse>(json!({}))
|
||||
.expect("default screen share config");
|
||||
let delivery = serde_json::from_value::<ExperimentDeliveryConfigResponse>(json!({}))
|
||||
.expect("default delivery config");
|
||||
let hover = serde_json::from_value::<MessageHoverTrackingConfigResponse>(json!({}))
|
||||
.expect("default message hover tracking config");
|
||||
let keyboard = serde_json::from_value::<MessageKeyboardFocusConfigResponse>(json!({}))
|
||||
.expect("default message keyboard focus config");
|
||||
let blocked = serde_json::from_value::<BlockedMessageGroupsConfigResponse>(json!({}))
|
||||
.expect("default blocked message groups config");
|
||||
let activity_log =
|
||||
serde_json::from_value::<GuildActivityLogPresentationConfigResponse>(json!({}))
|
||||
.expect("default guild activity log presentation config");
|
||||
let expression = serde_json::from_value::<ExpressionInfoCardConfigResponse>(json!({}))
|
||||
.expect("default expression info card config");
|
||||
let collapse = serde_json::from_value::<GuildHeaderCollapseConfigResponse>(json!({}))
|
||||
.expect("default guild header collapse config");
|
||||
let typing = serde_json::from_value::<TypingIndicatorReworkConfigResponse>(json!({}))
|
||||
.expect("default typing indicator rework config");
|
||||
let noise = serde_json::to_value(noise).expect("serializable noise config");
|
||||
let screen_share =
|
||||
serde_json::to_value(screen_share).expect("serializable screen share config");
|
||||
let delivery = serde_json::to_value(delivery).expect("serializable delivery config");
|
||||
let hover =
|
||||
serde_json::to_value(hover).expect("serializable message hover tracking config");
|
||||
let keyboard =
|
||||
serde_json::to_value(keyboard).expect("serializable message keyboard focus config");
|
||||
let blocked =
|
||||
serde_json::to_value(blocked).expect("serializable blocked message groups config");
|
||||
let activity_log = serde_json::to_value(activity_log)
|
||||
.expect("serializable guild activity log presentation config");
|
||||
let expression =
|
||||
serde_json::to_value(expression).expect("serializable expression info card config");
|
||||
let collapse =
|
||||
serde_json::to_value(collapse).expect("serializable guild header collapse config");
|
||||
let typing =
|
||||
serde_json::to_value(typing).expect("serializable typing indicator rework config");
|
||||
let generated_noise: generated_types::VoiceNoiseSuppressionConfigResponse =
|
||||
serde_json::from_value(noise.clone()).expect("generated noise config contract");
|
||||
let generated_screen_share: generated_types::ScreenShareDeliveryConfigResponse =
|
||||
serde_json::from_value(screen_share.clone())
|
||||
.expect("generated screen share config contract");
|
||||
let generated_delivery: generated_types::ExperimentDeliveryConfigResponse =
|
||||
serde_json::from_value(delivery.clone()).expect("generated delivery config contract");
|
||||
let generated_hover: generated_types::MessageHoverTrackingConfigResponse =
|
||||
serde_json::from_value(hover.clone())
|
||||
.expect("generated message hover tracking config contract");
|
||||
let generated_keyboard: generated_types::MessageKeyboardFocusConfigResponse =
|
||||
serde_json::from_value(keyboard.clone())
|
||||
.expect("generated message keyboard focus config contract");
|
||||
let generated_blocked: generated_types::BlockedMessageGroupsConfigResponse =
|
||||
serde_json::from_value(blocked.clone())
|
||||
.expect("generated blocked message groups config contract");
|
||||
let generated_activity_log: generated_types::GuildActivityLogPresentationConfigResponse =
|
||||
serde_json::from_value(activity_log.clone())
|
||||
.expect("generated guild activity log presentation config contract");
|
||||
let generated_expression: generated_types::ExpressionInfoCardConfigResponse =
|
||||
serde_json::from_value(expression.clone())
|
||||
.expect("generated expression info card config contract");
|
||||
let generated_collapse: generated_types::GuildHeaderCollapseConfigResponse =
|
||||
serde_json::from_value(collapse.clone())
|
||||
.expect("generated guild header collapse config contract");
|
||||
let generated_typing: generated_types::TypingIndicatorReworkConfigResponse =
|
||||
serde_json::from_value(typing.clone())
|
||||
.expect("generated typing indicator rework config contract");
|
||||
assert_eq!(
|
||||
serde_json::to_value(generated_noise).expect("serializable generated noise config"),
|
||||
noise
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(generated_screen_share)
|
||||
.expect("serializable generated screen share config"),
|
||||
screen_share
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(generated_delivery)
|
||||
.expect("serializable generated delivery config"),
|
||||
delivery
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(generated_hover)
|
||||
.expect("serializable generated message hover tracking config"),
|
||||
hover
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(generated_keyboard)
|
||||
.expect("serializable generated message keyboard focus config"),
|
||||
keyboard
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(generated_blocked)
|
||||
.expect("serializable generated blocked message groups config"),
|
||||
blocked
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(generated_activity_log)
|
||||
.expect("serializable generated guild activity log presentation config"),
|
||||
activity_log
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(generated_expression)
|
||||
.expect("serializable generated expression info card config"),
|
||||
expression
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(generated_collapse)
|
||||
.expect("serializable generated guild header collapse config"),
|
||||
collapse
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(generated_typing)
|
||||
.expect("serializable generated typing indicator rework config"),
|
||||
typing
|
||||
);
|
||||
for (name, value) in [
|
||||
("VoiceNoiseSuppressionConfigResponse", noise),
|
||||
("ScreenShareDeliveryConfigResponse", screen_share),
|
||||
("ExperimentDeliveryConfigResponse", delivery),
|
||||
("MessageHoverTrackingConfigResponse", hover),
|
||||
("MessageKeyboardFocusConfigResponse", keyboard),
|
||||
("BlockedMessageGroupsConfigResponse", blocked),
|
||||
("GuildActivityLogPresentationConfigResponse", activity_log),
|
||||
("ExpressionInfoCardConfigResponse", expression),
|
||||
("GuildHeaderCollapseConfigResponse", collapse),
|
||||
("TypingIndicatorReworkConfigResponse", typing),
|
||||
] {
|
||||
for (field, value) in value.as_object().expect("config object") {
|
||||
assert_eq!(
|
||||
@@ -1403,44 +1077,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_client_accepts_unknown_response_fields() {
|
||||
const GENERATED_CLIENT: &str =
|
||||
include_str!(concat!(env!("OUT_DIR"), "/admin_api_generated.rs"));
|
||||
assert!(
|
||||
!GENERATED_CLIENT.contains("deny_unknown_fields"),
|
||||
"fluxer_admin/build.rs must clear additionalProperties so a new API field cannot \
|
||||
blank an admin page"
|
||||
);
|
||||
let mut section = serde_json::to_value(ExpressionInfoCardConfigResponse::default())
|
||||
.expect("serializable expression info card config");
|
||||
section
|
||||
.as_object_mut()
|
||||
.expect("expression info card object")
|
||||
.insert("future_knob".to_owned(), json!(7));
|
||||
serde_json::from_value::<generated_types::ExpressionInfoCardConfigResponse>(section)
|
||||
.expect("generated instance config section tolerates unknown fields");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_audit_log_change_accepts_scalar_and_object_values() {
|
||||
for value in [json!("old"), json!(7), json!(true), json!(null)] {
|
||||
let change = serde_json::from_value::<generated_types::AuditLogChangeSchema>(
|
||||
json!({"key": "name", "old_value": value, "new_value": {"added": [], "removed": []}}),
|
||||
)
|
||||
.expect("generated audit log change tolerates scalar values");
|
||||
assert_eq!(change.key, "name");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expression_info_card_update_preserves_empty_lists_and_omitted_fields() {
|
||||
let update = ExpressionInfoCardConfigUpdateRequest {
|
||||
fn screen_share_delivery_update_preserves_empty_lists_and_omitted_fields() {
|
||||
let update = ScreenShareDeliveryConfigUpdateRequest {
|
||||
included_user_ids: Some(Vec::new()),
|
||||
excluded_user_ids: Some(Vec::new()),
|
||||
..Default::default()
|
||||
};
|
||||
let value = serde_json::to_value(update).expect("serializable update");
|
||||
serde_json::from_value::<generated_types::ExpressionInfoCardConfigUpdateRequest>(
|
||||
serde_json::from_value::<generated_types::ScreenShareDeliveryConfigUpdateRequest>(
|
||||
value.clone(),
|
||||
)
|
||||
.expect("generated update contract");
|
||||
@@ -1449,179 +1093,7 @@ mod tests {
|
||||
json!({"included_user_ids": [], "excluded_user_ids": []})
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(ExpressionInfoCardConfigUpdateRequest::default())
|
||||
.expect("serializable update"),
|
||||
json!({})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guild_header_collapse_update_preserves_empty_lists_and_omitted_fields() {
|
||||
let update = GuildHeaderCollapseConfigUpdateRequest {
|
||||
included_user_ids: Some(Vec::new()),
|
||||
excluded_user_ids: Some(Vec::new()),
|
||||
..Default::default()
|
||||
};
|
||||
let value = serde_json::to_value(update).expect("serializable update");
|
||||
serde_json::from_value::<generated_types::GuildHeaderCollapseConfigUpdateRequest>(
|
||||
value.clone(),
|
||||
)
|
||||
.expect("generated update contract");
|
||||
assert_eq!(
|
||||
value,
|
||||
json!({"included_user_ids": [], "excluded_user_ids": []})
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(GuildHeaderCollapseConfigUpdateRequest::default())
|
||||
.expect("serializable update"),
|
||||
json!({})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guild_header_collapse_response_defaults_to_the_guild_header_collapse_v1_salt() {
|
||||
let config = GuildHeaderCollapseConfigResponse::default();
|
||||
assert!(!config.enabled);
|
||||
assert_eq!(config.config_version, 0);
|
||||
assert_eq!(config.rollout_basis_points, 0);
|
||||
assert_eq!(config.rollout_salt, "guild-header-collapse-v1");
|
||||
assert!(config.included_user_ids.is_empty());
|
||||
assert!(config.excluded_user_ids.is_empty());
|
||||
assert_eq!(
|
||||
serde_json::from_value::<GuildHeaderCollapseConfigResponse>(json!({}))
|
||||
.expect("default guild header collapse config")
|
||||
.rollout_salt,
|
||||
"guild-header-collapse-v1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typing_indicator_rework_update_preserves_empty_lists_and_omitted_fields() {
|
||||
let update = TypingIndicatorReworkConfigUpdateRequest {
|
||||
included_user_ids: Some(Vec::new()),
|
||||
excluded_user_ids: Some(Vec::new()),
|
||||
..Default::default()
|
||||
};
|
||||
let value = serde_json::to_value(update).expect("serializable update");
|
||||
serde_json::from_value::<generated_types::TypingIndicatorReworkConfigUpdateRequest>(
|
||||
value.clone(),
|
||||
)
|
||||
.expect("generated update contract");
|
||||
assert_eq!(
|
||||
value,
|
||||
json!({"included_user_ids": [], "excluded_user_ids": []})
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(TypingIndicatorReworkConfigUpdateRequest::default())
|
||||
.expect("serializable update"),
|
||||
json!({})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typing_indicator_rework_response_defaults_to_the_typing_indicator_rework_v1_salt() {
|
||||
let config = TypingIndicatorReworkConfigResponse::default();
|
||||
assert!(!config.enabled);
|
||||
assert_eq!(config.config_version, 0);
|
||||
assert_eq!(config.rollout_basis_points, 0);
|
||||
assert_eq!(config.rollout_salt, "typing-indicator-rework-v1");
|
||||
assert!(config.included_user_ids.is_empty());
|
||||
assert!(config.excluded_user_ids.is_empty());
|
||||
assert_eq!(
|
||||
serde_json::from_value::<TypingIndicatorReworkConfigResponse>(json!({}))
|
||||
.expect("default typing indicator rework config")
|
||||
.rollout_salt,
|
||||
"typing-indicator-rework-v1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_hover_tracking_update_preserves_empty_lists_and_omitted_fields() {
|
||||
let update = MessageHoverTrackingConfigUpdateRequest {
|
||||
included_user_ids: Some(Vec::new()),
|
||||
excluded_user_ids: Some(Vec::new()),
|
||||
..Default::default()
|
||||
};
|
||||
let value = serde_json::to_value(update).expect("serializable update");
|
||||
serde_json::from_value::<generated_types::MessageHoverTrackingConfigUpdateRequest>(
|
||||
value.clone(),
|
||||
)
|
||||
.expect("generated update contract");
|
||||
assert_eq!(
|
||||
value,
|
||||
json!({"included_user_ids": [], "excluded_user_ids": []})
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(MessageHoverTrackingConfigUpdateRequest::default())
|
||||
.expect("serializable update"),
|
||||
json!({})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_keyboard_focus_update_preserves_empty_lists_and_omitted_fields() {
|
||||
let update = MessageKeyboardFocusConfigUpdateRequest {
|
||||
included_user_ids: Some(Vec::new()),
|
||||
excluded_user_ids: Some(Vec::new()),
|
||||
..Default::default()
|
||||
};
|
||||
let value = serde_json::to_value(update).expect("serializable update");
|
||||
serde_json::from_value::<generated_types::MessageKeyboardFocusConfigUpdateRequest>(
|
||||
value.clone(),
|
||||
)
|
||||
.expect("generated update contract");
|
||||
assert_eq!(
|
||||
value,
|
||||
json!({"included_user_ids": [], "excluded_user_ids": []})
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(MessageKeyboardFocusConfigUpdateRequest::default())
|
||||
.expect("serializable update"),
|
||||
json!({})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocked_message_groups_update_preserves_empty_lists_and_omitted_fields() {
|
||||
let update = BlockedMessageGroupsConfigUpdateRequest {
|
||||
included_user_ids: Some(Vec::new()),
|
||||
excluded_user_ids: Some(Vec::new()),
|
||||
..Default::default()
|
||||
};
|
||||
let value = serde_json::to_value(update).expect("serializable update");
|
||||
serde_json::from_value::<generated_types::BlockedMessageGroupsConfigUpdateRequest>(
|
||||
value.clone(),
|
||||
)
|
||||
.expect("generated update contract");
|
||||
assert_eq!(
|
||||
value,
|
||||
json!({"included_user_ids": [], "excluded_user_ids": []})
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(BlockedMessageGroupsConfigUpdateRequest::default())
|
||||
.expect("serializable update"),
|
||||
json!({})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guild_activity_log_presentation_update_preserves_empty_lists_and_omitted_fields() {
|
||||
let update = GuildActivityLogPresentationConfigUpdateRequest {
|
||||
included_user_ids: Some(Vec::new()),
|
||||
excluded_user_ids: Some(Vec::new()),
|
||||
..Default::default()
|
||||
};
|
||||
let value = serde_json::to_value(update).expect("serializable update");
|
||||
serde_json::from_value::<generated_types::GuildActivityLogPresentationConfigUpdateRequest>(
|
||||
value.clone(),
|
||||
)
|
||||
.expect("generated update contract");
|
||||
assert_eq!(
|
||||
value,
|
||||
json!({"included_user_ids": [], "excluded_user_ids": []})
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(GuildActivityLogPresentationConfigUpdateRequest::default())
|
||||
serde_json::to_value(ScreenShareDeliveryConfigUpdateRequest::default())
|
||||
.expect("serializable update"),
|
||||
json!({})
|
||||
);
|
||||
|
||||
@@ -95,8 +95,8 @@ impl AdminApiClient {
|
||||
remove_flags: &[String],
|
||||
) -> ApiResult<AdminUser> {
|
||||
let body = generated_types::AdminUserFlagsUpdateRequest {
|
||||
add_flags: user_flags(add_flags),
|
||||
remove_flags: user_flags(remove_flags),
|
||||
add_flags: user_flags(add_flags)?,
|
||||
remove_flags: user_flags(remove_flags)?,
|
||||
};
|
||||
let response = self
|
||||
.generated()
|
||||
@@ -567,11 +567,13 @@ fn bool_param(value: bool) -> &'static str {
|
||||
if value { "true" } else { "false" }
|
||||
}
|
||||
|
||||
fn user_flags(values: &[String]) -> Vec<generated_types::UserFlags> {
|
||||
fn user_flags(values: &[String]) -> ApiResult<Vec<generated_types::UserFlags>> {
|
||||
values
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(generated_types::UserFlags::from)
|
||||
.map(|value| {
|
||||
generated_types::UserFlags::try_from(value.as_str())
|
||||
.map_err(|error| ApiError::Parse(error.to_string()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +121,7 @@ pub async fn render(
|
||||
admin_user_id: None,
|
||||
target_id: Some(guild_id.to_owned()),
|
||||
target_type: Some("guild".to_owned()),
|
||||
access: Some("write".to_owned()),
|
||||
sort_by: Some("created_at".to_owned()),
|
||||
sort_order: Some("desc".to_owned()),
|
||||
limit: 50,
|
||||
@@ -134,7 +135,7 @@ pub async fn render(
|
||||
@if let Some(resp) = resp {
|
||||
@if resp.logs.is_empty() {
|
||||
p class="text-sm text-neutral-500" {
|
||||
"No admin audit log entries for this guild."
|
||||
"No admin write actions have been recorded for this guild."
|
||||
}
|
||||
} @else {
|
||||
(table_container(table(maud::html! {
|
||||
|
||||
@@ -28,6 +28,7 @@ struct AuditLogsQuery {
|
||||
admin_user_id: Option<String>,
|
||||
target_id: Option<String>,
|
||||
target_type: Option<String>,
|
||||
access: Option<String>,
|
||||
sort_by: Option<String>,
|
||||
sort_order: Option<String>,
|
||||
limit: Option<u32>,
|
||||
@@ -119,6 +120,7 @@ async fn audit_logs_page(
|
||||
admin_user_id: query.admin_user_id.as_deref().unwrap_or(""),
|
||||
target_id: query.target_id.as_deref().unwrap_or(""),
|
||||
target_type: query.target_type.as_deref().unwrap_or(""),
|
||||
access: query.access.as_deref().unwrap_or(""),
|
||||
sort_by: query.sort_by.as_deref().unwrap_or("createdAt"),
|
||||
sort_order: query.sort_order.as_deref().unwrap_or("desc"),
|
||||
limit,
|
||||
@@ -131,6 +133,7 @@ async fn audit_logs_page(
|
||||
admin_user_id: nonempty(params.admin_user_id),
|
||||
target_id: nonempty(params.target_id),
|
||||
target_type: nonempty(params.target_type),
|
||||
access: nonempty(params.access),
|
||||
sort_by: Some(params.sort_by.to_owned()),
|
||||
sort_order: Some(params.sort_order.to_owned()),
|
||||
limit,
|
||||
|
||||
@@ -6,12 +6,10 @@ use crate::{
|
||||
types::{
|
||||
AppBrandingConfigUpdateRequest, AppLegalConfigUpdateRequest,
|
||||
AppPublicConfigUpdateRequest, AppRegistrationConfigUpdateRequest,
|
||||
AppSetupConfigUpdateRequest, BlockedMessageGroupsConfigUpdateRequest,
|
||||
CreateRegistrationUrlRequest, DeferredPhoneGateUpdateRequest,
|
||||
ExperimentDeliveryConfigUpdateRequest, ExpressionInfoCardConfigUpdateRequest,
|
||||
GatewayRolloutConfigUpdateRequest, GatewayRolloutMode,
|
||||
GuildActivityLogPresentationConfigUpdateRequest,
|
||||
GuildHeaderCollapseConfigUpdateRequest, InstanceAttachmentDecayUpdateRequest,
|
||||
AppSetupConfigUpdateRequest, CreateRegistrationUrlRequest,
|
||||
DeferredPhoneGateUpdateRequest, EXPERIMENT_MAX_TARGETED_USERS,
|
||||
ExperimentDeliveryConfigUpdateRequest, GatewayRolloutConfigUpdateRequest,
|
||||
GatewayRolloutMode, InstanceAttachmentDecayUpdateRequest,
|
||||
InstanceBlueskyIntegrationUpdateRequest, InstanceBlueskyKeyIntegrationUpdateRequest,
|
||||
InstanceCaptchaIntegrationUpdateRequest, InstanceConfigUpdateRequest,
|
||||
InstanceEmailIntegrationUpdateRequest, InstanceEmailSmtpIntegrationUpdateRequest,
|
||||
@@ -19,12 +17,10 @@ use crate::{
|
||||
InstanceIntegrationsUpdateRequest, InstanceMediaUpdateRequest,
|
||||
InstancePolicyUpdateRequest, InstanceRegistrationConfigUpdateRequest,
|
||||
InstanceServicesUpdateRequest, InstanceYoutubeIntegrationUpdateRequest,
|
||||
LimitConfigUpdateRequest, LimitRule, LimitRuleFilters,
|
||||
MessageHoverTrackingConfigUpdateRequest, MessageKeyboardFocusConfigUpdateRequest,
|
||||
NoiseSuppressionBackend, PremiumMode, RegistrationMode, SsoConfigUpdateRequest,
|
||||
TypingIndicatorReworkConfigUpdateRequest, VOICE_NS_MAX_GUILD_OVERRIDES,
|
||||
VOICE_NS_MAX_TARGETED_USERS, VoiceE2eeScope, VoiceNoiseSuppressionConfigUpdateRequest,
|
||||
VoiceNoiseSuppressionGuildOverride,
|
||||
LimitConfigUpdateRequest, LimitRule, LimitRuleFilters, NoiseSuppressionBackend,
|
||||
PremiumMode, RegistrationMode, ScreenShareDeliveryConfigUpdateRequest,
|
||||
SsoConfigUpdateRequest, VOICE_NS_MAX_GUILD_OVERRIDES, VoiceE2eeScope,
|
||||
VoiceNoiseSuppressionConfigUpdateRequest, VoiceNoiseSuppressionGuildOverride,
|
||||
},
|
||||
},
|
||||
config::AdminConfig,
|
||||
@@ -211,33 +207,7 @@ pub async fn instance_config_post(
|
||||
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
|
||||
Err(message) => FlashData::error(message),
|
||||
},
|
||||
"update_message_hover_tracking" => match build_message_hover_tracking_update(&form) {
|
||||
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
|
||||
Err(message) => FlashData::error(message),
|
||||
},
|
||||
"update_message_keyboard_focus" => match build_message_keyboard_focus_update(&form) {
|
||||
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
|
||||
Err(message) => FlashData::error(message),
|
||||
},
|
||||
"update_blocked_message_groups" => match build_blocked_message_groups_update(&form) {
|
||||
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
|
||||
Err(message) => FlashData::error(message),
|
||||
},
|
||||
"update_guild_activity_log_presentation" => {
|
||||
match build_guild_activity_log_presentation_update(&form) {
|
||||
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
|
||||
Err(message) => FlashData::error(message),
|
||||
}
|
||||
}
|
||||
"update_expression_info_card" => match build_expression_info_card_update(&form) {
|
||||
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
|
||||
Err(message) => FlashData::error(message),
|
||||
},
|
||||
"update_guild_header_collapse" => match build_guild_header_collapse_update(&form) {
|
||||
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
|
||||
Err(message) => FlashData::error(message),
|
||||
},
|
||||
"update_typing_indicator_rework" => match build_typing_indicator_rework_update(&form) {
|
||||
"update_screen_share_delivery" => match build_screen_share_delivery_update(&form) {
|
||||
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
|
||||
Err(message) => FlashData::error(message),
|
||||
},
|
||||
@@ -548,9 +518,9 @@ fn parse_experiment_user_ids(value: &str, label: &str) -> Result<Vec<String>, St
|
||||
if ids.iter().any(|existing| existing == candidate) {
|
||||
continue;
|
||||
}
|
||||
if ids.len() == VOICE_NS_MAX_TARGETED_USERS {
|
||||
if ids.len() == EXPERIMENT_MAX_TARGETED_USERS {
|
||||
return Err(format!(
|
||||
"{label} must contain at most {VOICE_NS_MAX_TARGETED_USERS} unique IDs"
|
||||
"{label} must contain at most {EXPERIMENT_MAX_TARGETED_USERS} unique IDs"
|
||||
));
|
||||
}
|
||||
ids.push(candidate.to_owned());
|
||||
@@ -651,7 +621,6 @@ fn build_voice_noise_suppression_update(
|
||||
guild_overrides: Some(parse_voice_noise_suppression_guild_overrides(
|
||||
form.first("voice_ns_guild_overrides").unwrap_or_default(),
|
||||
)?),
|
||||
stereo_enabled: Some(form.bool_value("voice_ns_stereo_enabled")),
|
||||
suppression_strength: parse_form_number(
|
||||
form,
|
||||
"voice_ns_suppression_strength",
|
||||
@@ -664,213 +633,30 @@ fn build_voice_noise_suppression_update(
|
||||
})
|
||||
}
|
||||
|
||||
fn build_message_hover_tracking_update(
|
||||
fn build_screen_share_delivery_update(
|
||||
form: &MultiValueForm,
|
||||
) -> Result<InstanceConfigUpdateRequest, String> {
|
||||
Ok(InstanceConfigUpdateRequest {
|
||||
message_hover_tracking: Some(MessageHoverTrackingConfigUpdateRequest {
|
||||
enabled: Some(form.bool_value("message_hover_enabled")),
|
||||
screen_share_delivery: Some(ScreenShareDeliveryConfigUpdateRequest {
|
||||
enabled: Some(form.bool_value("screen_share_delivery_enabled")),
|
||||
rollout_basis_points: parse_form_number(
|
||||
form,
|
||||
"message_hover_rollout_basis_points",
|
||||
"Rollout basis points",
|
||||
0,
|
||||
EXPERIMENT_ROLLOUT_BASIS_POINTS_MAX,
|
||||
)?,
|
||||
rollout_salt: parse_experiment_rollout_salt(form, "message_hover_rollout_salt")?,
|
||||
included_user_ids: Some(parse_experiment_user_ids(
|
||||
form.first("message_hover_included_user_ids")
|
||||
.unwrap_or_default(),
|
||||
"Included user IDs",
|
||||
)?),
|
||||
excluded_user_ids: Some(parse_experiment_user_ids(
|
||||
form.first("message_hover_excluded_user_ids")
|
||||
.unwrap_or_default(),
|
||||
"Excluded user IDs",
|
||||
)?),
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn build_message_keyboard_focus_update(
|
||||
form: &MultiValueForm,
|
||||
) -> Result<InstanceConfigUpdateRequest, String> {
|
||||
Ok(InstanceConfigUpdateRequest {
|
||||
message_keyboard_focus: Some(MessageKeyboardFocusConfigUpdateRequest {
|
||||
enabled: Some(form.bool_value("message_keyboard_focus_enabled")),
|
||||
rollout_basis_points: parse_form_number(
|
||||
form,
|
||||
"message_keyboard_focus_rollout_basis_points",
|
||||
"screen_share_delivery_rollout_basis_points",
|
||||
"Rollout basis points",
|
||||
0,
|
||||
EXPERIMENT_ROLLOUT_BASIS_POINTS_MAX,
|
||||
)?,
|
||||
rollout_salt: parse_experiment_rollout_salt(
|
||||
form,
|
||||
"message_keyboard_focus_rollout_salt",
|
||||
"screen_share_delivery_rollout_salt",
|
||||
)?,
|
||||
included_user_ids: Some(parse_experiment_user_ids(
|
||||
form.first("message_keyboard_focus_included_user_ids")
|
||||
form.first("screen_share_delivery_included_user_ids")
|
||||
.unwrap_or_default(),
|
||||
"Included user IDs",
|
||||
)?),
|
||||
excluded_user_ids: Some(parse_experiment_user_ids(
|
||||
form.first("message_keyboard_focus_excluded_user_ids")
|
||||
.unwrap_or_default(),
|
||||
"Excluded user IDs",
|
||||
)?),
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn build_blocked_message_groups_update(
|
||||
form: &MultiValueForm,
|
||||
) -> Result<InstanceConfigUpdateRequest, String> {
|
||||
Ok(InstanceConfigUpdateRequest {
|
||||
blocked_message_groups: Some(BlockedMessageGroupsConfigUpdateRequest {
|
||||
enabled: Some(form.bool_value("blocked_groups_enabled")),
|
||||
rollout_basis_points: parse_form_number(
|
||||
form,
|
||||
"blocked_groups_rollout_basis_points",
|
||||
"Rollout basis points",
|
||||
0,
|
||||
EXPERIMENT_ROLLOUT_BASIS_POINTS_MAX,
|
||||
)?,
|
||||
rollout_salt: parse_experiment_rollout_salt(form, "blocked_groups_rollout_salt")?,
|
||||
included_user_ids: Some(parse_experiment_user_ids(
|
||||
form.first("blocked_groups_included_user_ids")
|
||||
.unwrap_or_default(),
|
||||
"Included user IDs",
|
||||
)?),
|
||||
excluded_user_ids: Some(parse_experiment_user_ids(
|
||||
form.first("blocked_groups_excluded_user_ids")
|
||||
.unwrap_or_default(),
|
||||
"Excluded user IDs",
|
||||
)?),
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn build_guild_activity_log_presentation_update(
|
||||
form: &MultiValueForm,
|
||||
) -> Result<InstanceConfigUpdateRequest, String> {
|
||||
Ok(InstanceConfigUpdateRequest {
|
||||
guild_activity_log_presentation: Some(GuildActivityLogPresentationConfigUpdateRequest {
|
||||
enabled: Some(form.bool_value("guild_activity_log_presentation_enabled")),
|
||||
rollout_basis_points: parse_form_number(
|
||||
form,
|
||||
"guild_activity_log_presentation_rollout_basis_points",
|
||||
"Rollout basis points",
|
||||
0,
|
||||
EXPERIMENT_ROLLOUT_BASIS_POINTS_MAX,
|
||||
)?,
|
||||
rollout_salt: parse_experiment_rollout_salt(
|
||||
form,
|
||||
"guild_activity_log_presentation_rollout_salt",
|
||||
)?,
|
||||
included_user_ids: Some(parse_experiment_user_ids(
|
||||
form.first("guild_activity_log_presentation_included_user_ids")
|
||||
.unwrap_or_default(),
|
||||
"Included user IDs",
|
||||
)?),
|
||||
excluded_user_ids: Some(parse_experiment_user_ids(
|
||||
form.first("guild_activity_log_presentation_excluded_user_ids")
|
||||
.unwrap_or_default(),
|
||||
"Excluded user IDs",
|
||||
)?),
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn build_expression_info_card_update(
|
||||
form: &MultiValueForm,
|
||||
) -> Result<InstanceConfigUpdateRequest, String> {
|
||||
Ok(InstanceConfigUpdateRequest {
|
||||
expression_info_card: Some(ExpressionInfoCardConfigUpdateRequest {
|
||||
enabled: Some(form.bool_value("expression_card_enabled")),
|
||||
rollout_basis_points: parse_form_number(
|
||||
form,
|
||||
"expression_card_rollout_basis_points",
|
||||
"Rollout basis points",
|
||||
0,
|
||||
EXPERIMENT_ROLLOUT_BASIS_POINTS_MAX,
|
||||
)?,
|
||||
rollout_salt: parse_experiment_rollout_salt(form, "expression_card_rollout_salt")?,
|
||||
included_user_ids: Some(parse_experiment_user_ids(
|
||||
form.first("expression_card_included_user_ids")
|
||||
.unwrap_or_default(),
|
||||
"Included user IDs",
|
||||
)?),
|
||||
excluded_user_ids: Some(parse_experiment_user_ids(
|
||||
form.first("expression_card_excluded_user_ids")
|
||||
.unwrap_or_default(),
|
||||
"Excluded user IDs",
|
||||
)?),
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn build_guild_header_collapse_update(
|
||||
form: &MultiValueForm,
|
||||
) -> Result<InstanceConfigUpdateRequest, String> {
|
||||
Ok(InstanceConfigUpdateRequest {
|
||||
guild_header_collapse: Some(GuildHeaderCollapseConfigUpdateRequest {
|
||||
enabled: Some(form.bool_value("guild_header_collapse_enabled")),
|
||||
rollout_basis_points: parse_form_number(
|
||||
form,
|
||||
"guild_header_collapse_rollout_basis_points",
|
||||
"Rollout basis points",
|
||||
0,
|
||||
EXPERIMENT_ROLLOUT_BASIS_POINTS_MAX,
|
||||
)?,
|
||||
rollout_salt: parse_experiment_rollout_salt(
|
||||
form,
|
||||
"guild_header_collapse_rollout_salt",
|
||||
)?,
|
||||
included_user_ids: Some(parse_experiment_user_ids(
|
||||
form.first("guild_header_collapse_included_user_ids")
|
||||
.unwrap_or_default(),
|
||||
"Included user IDs",
|
||||
)?),
|
||||
excluded_user_ids: Some(parse_experiment_user_ids(
|
||||
form.first("guild_header_collapse_excluded_user_ids")
|
||||
.unwrap_or_default(),
|
||||
"Excluded user IDs",
|
||||
)?),
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn build_typing_indicator_rework_update(
|
||||
form: &MultiValueForm,
|
||||
) -> Result<InstanceConfigUpdateRequest, String> {
|
||||
Ok(InstanceConfigUpdateRequest {
|
||||
typing_indicator_rework: Some(TypingIndicatorReworkConfigUpdateRequest {
|
||||
enabled: Some(form.bool_value("typing_indicator_rework_enabled")),
|
||||
rollout_basis_points: parse_form_number(
|
||||
form,
|
||||
"typing_indicator_rework_rollout_basis_points",
|
||||
"Rollout basis points",
|
||||
0,
|
||||
EXPERIMENT_ROLLOUT_BASIS_POINTS_MAX,
|
||||
)?,
|
||||
rollout_salt: parse_experiment_rollout_salt(
|
||||
form,
|
||||
"typing_indicator_rework_rollout_salt",
|
||||
)?,
|
||||
included_user_ids: Some(parse_experiment_user_ids(
|
||||
form.first("typing_indicator_rework_included_user_ids")
|
||||
.unwrap_or_default(),
|
||||
"Included user IDs",
|
||||
)?),
|
||||
excluded_user_ids: Some(parse_experiment_user_ids(
|
||||
form.first("typing_indicator_rework_excluded_user_ids")
|
||||
form.first("screen_share_delivery_excluded_user_ids")
|
||||
.unwrap_or_default(),
|
||||
"Excluded user IDs",
|
||||
)?),
|
||||
@@ -933,6 +719,8 @@ fn build_app_public_update(form: &MultiValueForm) -> InstanceConfigUpdateRequest
|
||||
wordmark_url: optional("app_wordmark_url"),
|
||||
favicon_url: optional("app_favicon_url"),
|
||||
theme_color: optional("app_theme_color"),
|
||||
status_page_url: optional("app_status_page_url"),
|
||||
status_page_incident_history_url: optional("app_status_page_incident_history_url"),
|
||||
}),
|
||||
setup: Some(AppSetupConfigUpdateRequest {
|
||||
configured: Some(form.bool_value("app_setup_configured")),
|
||||
@@ -1494,7 +1282,6 @@ mod tests {
|
||||
.expect("voice noise suppression update");
|
||||
assert_eq!(update.enabled, Some(true));
|
||||
assert_eq!(update.allow_user_override, Some(true));
|
||||
assert_eq!(update.stereo_enabled, Some(false));
|
||||
assert_eq!(
|
||||
update.default_backend,
|
||||
Some(NoiseSuppressionBackend::Rnnoise)
|
||||
@@ -1520,7 +1307,6 @@ mod tests {
|
||||
serde_json::json!({"voice_noise_suppression": {
|
||||
"enabled": false,
|
||||
"allow_user_override": false,
|
||||
"stereo_enabled": false,
|
||||
"enabled_backends": [],
|
||||
"included_user_ids": [],
|
||||
"excluded_user_ids": [],
|
||||
@@ -1598,13 +1384,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parse_experiment_user_ids_rejects_exceeding_the_cap() {
|
||||
let value = (0..VOICE_NS_MAX_TARGETED_USERS)
|
||||
let value = (0..EXPERIMENT_MAX_TARGETED_USERS)
|
||||
.map(|index| index.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let ids = parse_experiment_user_ids(&format!("{value}\n999"), "Included user IDs")
|
||||
.expect("valid IDs at cap");
|
||||
assert_eq!(ids.len(), VOICE_NS_MAX_TARGETED_USERS);
|
||||
assert_eq!(ids.len(), EXPERIMENT_MAX_TARGETED_USERS);
|
||||
assert_eq!(ids.last(), Some(&"999".to_owned()));
|
||||
assert_eq!(
|
||||
parse_experiment_user_ids(&format!("{value}\n1000"), "Included user IDs")
|
||||
@@ -1801,40 +1587,43 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_guild_header_collapse_update_reads_the_whole_form() {
|
||||
fn build_screen_share_delivery_update_reads_the_rollout_fields() {
|
||||
let form = MultiValueForm::parse(
|
||||
b"guild_header_collapse_enabled=true&guild_header_collapse_rollout_basis_points=2500&guild_header_collapse_rollout_salt=%20guild-header-collapse-v2%20&guild_header_collapse_included_user_ids=1500000000000000001%0A1500000000000000001&guild_header_collapse_excluded_user_ids=1500000000000000002%2C%201500000000000000003",
|
||||
b"screen_share_delivery_enabled=true&screen_share_delivery_rollout_basis_points=%20250%20&screen_share_delivery_rollout_salt=%20screen-share-delivery-v2%20&screen_share_delivery_included_user_ids=1500000000000000001%0A1500000000000000002&screen_share_delivery_excluded_user_ids=1500000000000000003%2C%201500000000000000004",
|
||||
);
|
||||
let update = build_guild_header_collapse_update(&form)
|
||||
let update = build_screen_share_delivery_update(&form)
|
||||
.expect("valid form")
|
||||
.guild_header_collapse
|
||||
.expect("guild header collapse update");
|
||||
.screen_share_delivery
|
||||
.expect("screen share delivery update");
|
||||
assert_eq!(update.enabled, Some(true));
|
||||
assert_eq!(update.rollout_basis_points, Some(2_500));
|
||||
assert_eq!(update.rollout_basis_points, Some(250));
|
||||
assert_eq!(
|
||||
update.rollout_salt,
|
||||
Some("guild-header-collapse-v2".to_owned())
|
||||
Some("screen-share-delivery-v2".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
update.included_user_ids,
|
||||
Some(vec!["1500000000000000001".to_owned()])
|
||||
Some(vec![
|
||||
"1500000000000000001".to_owned(),
|
||||
"1500000000000000002".to_owned()
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
update.excluded_user_ids,
|
||||
Some(vec![
|
||||
"1500000000000000002".to_owned(),
|
||||
"1500000000000000003".to_owned()
|
||||
"1500000000000000003".to_owned(),
|
||||
"1500000000000000004".to_owned()
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_guild_header_collapse_update_leaves_the_rollout_inert_when_nothing_is_submitted() {
|
||||
fn build_screen_share_delivery_update_leaves_the_feature_inert_when_nothing_is_submitted() {
|
||||
let form = MultiValueForm::parse(b"_csrf=token");
|
||||
let request = build_guild_header_collapse_update(&form).expect("valid form");
|
||||
let request = build_screen_share_delivery_update(&form).expect("valid form");
|
||||
assert_eq!(
|
||||
serde_json::to_value(request).expect("serializable update"),
|
||||
serde_json::json!({"guild_header_collapse": {
|
||||
serde_json::json!({"screen_share_delivery": {
|
||||
"enabled": false,
|
||||
"included_user_ids": [],
|
||||
"excluded_user_ids": [],
|
||||
@@ -1843,107 +1632,35 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_guild_header_collapse_update_rejects_a_rollout_above_the_maximum() {
|
||||
let form = MultiValueForm::parse(b"guild_header_collapse_rollout_basis_points=10001");
|
||||
assert_eq!(
|
||||
build_guild_header_collapse_update(&form).expect_err("invalid rollout"),
|
||||
"Rollout basis points must be a whole number between 0 and 10000"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_guild_header_collapse_update_reads_only_its_own_prefix() {
|
||||
let form = MultiValueForm::parse(
|
||||
b"guild_header_collapse_enabled=true&guild_header_collapse_rollout_basis_points=2500&guild_header_collapse_rollout_salt=guild-header-collapse-v2&guild_header_collapse_included_user_ids=1500000000000000001&expression_card_rollout_basis_points=750&expression_card_rollout_salt=expression-info-card-v2&expression_card_excluded_user_ids=1500000000000000009",
|
||||
);
|
||||
let update = build_guild_header_collapse_update(&form)
|
||||
.expect("valid form")
|
||||
.guild_header_collapse
|
||||
.expect("guild header collapse update");
|
||||
assert_eq!(update.enabled, Some(true));
|
||||
assert_eq!(update.rollout_basis_points, Some(2_500));
|
||||
assert_eq!(
|
||||
update.rollout_salt,
|
||||
Some("guild-header-collapse-v2".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
update.included_user_ids,
|
||||
Some(vec!["1500000000000000001".to_owned()])
|
||||
);
|
||||
assert_eq!(update.excluded_user_ids, Some(Vec::new()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_typing_indicator_rework_update_reads_the_whole_form() {
|
||||
let form = MultiValueForm::parse(
|
||||
b"typing_indicator_rework_enabled=true&typing_indicator_rework_rollout_basis_points=2500&typing_indicator_rework_rollout_salt=%20typing-indicator-rework-v2%20&typing_indicator_rework_included_user_ids=1500000000000000001%0A1500000000000000001&typing_indicator_rework_excluded_user_ids=1500000000000000002%2C%201500000000000000003",
|
||||
);
|
||||
let update = build_typing_indicator_rework_update(&form)
|
||||
.expect("valid form")
|
||||
.typing_indicator_rework
|
||||
.expect("typing indicator rework update");
|
||||
assert_eq!(update.enabled, Some(true));
|
||||
assert_eq!(update.rollout_basis_points, Some(2_500));
|
||||
assert_eq!(
|
||||
update.rollout_salt,
|
||||
Some("typing-indicator-rework-v2".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
update.included_user_ids,
|
||||
Some(vec!["1500000000000000001".to_owned()])
|
||||
);
|
||||
assert_eq!(
|
||||
update.excluded_user_ids,
|
||||
Some(vec![
|
||||
"1500000000000000002".to_owned(),
|
||||
"1500000000000000003".to_owned()
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_typing_indicator_rework_update_leaves_the_rollout_inert_when_nothing_is_submitted() {
|
||||
let form = MultiValueForm::parse(b"_csrf=token");
|
||||
let request = build_typing_indicator_rework_update(&form).expect("valid form");
|
||||
assert_eq!(
|
||||
serde_json::to_value(request).expect("serializable update"),
|
||||
serde_json::json!({"typing_indicator_rework": {
|
||||
"enabled": false,
|
||||
"included_user_ids": [],
|
||||
"excluded_user_ids": [],
|
||||
}})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_typing_indicator_rework_update_rejects_a_rollout_above_the_maximum() {
|
||||
let form = MultiValueForm::parse(b"typing_indicator_rework_rollout_basis_points=10001");
|
||||
assert_eq!(
|
||||
build_typing_indicator_rework_update(&form).expect_err("invalid rollout"),
|
||||
"Rollout basis points must be a whole number between 0 and 10000"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_typing_indicator_rework_update_reads_only_its_own_prefix() {
|
||||
let form = MultiValueForm::parse(
|
||||
b"expression_card_enabled=true&expression_card_rollout_basis_points=2500&expression_card_rollout_salt=expression-info-card-v2&expression_card_included_user_ids=1500000000000000001&typing_indicator_rework_rollout_basis_points=750&typing_indicator_rework_rollout_salt=typing-indicator-rework-v2&typing_indicator_rework_excluded_user_ids=1500000000000000009",
|
||||
);
|
||||
let update = build_typing_indicator_rework_update(&form)
|
||||
.expect("valid form")
|
||||
.typing_indicator_rework
|
||||
.expect("typing indicator rework update");
|
||||
assert_eq!(update.enabled, Some(false));
|
||||
assert_eq!(update.rollout_basis_points, Some(750));
|
||||
assert_eq!(
|
||||
update.rollout_salt,
|
||||
Some("typing-indicator-rework-v2".to_owned())
|
||||
);
|
||||
assert_eq!(update.included_user_ids, Some(Vec::new()));
|
||||
assert_eq!(
|
||||
update.excluded_user_ids,
|
||||
Some(vec!["1500000000000000009".to_owned()])
|
||||
);
|
||||
fn build_screen_share_delivery_update_rejects_invalid_rollout_fields() {
|
||||
for (form, message) in [
|
||||
(
|
||||
"screen_share_delivery_rollout_basis_points=10001",
|
||||
"Rollout basis points must be a whole number between 0 and 10000",
|
||||
),
|
||||
(
|
||||
"screen_share_delivery_rollout_basis_points=abc",
|
||||
"Rollout basis points must be a whole number between 0 and 10000",
|
||||
),
|
||||
(
|
||||
"screen_share_delivery_rollout_salt=%20%20",
|
||||
"Rollout salt must be between 1 and 64 characters",
|
||||
),
|
||||
(
|
||||
"screen_share_delivery_included_user_ids=123%2Cinvalid",
|
||||
"Included user IDs entry 2 must contain 1 to 20 decimal digits",
|
||||
),
|
||||
(
|
||||
"screen_share_delivery_excluded_user_ids=123%2Cinvalid",
|
||||
"Excluded user IDs entry 2 must contain 1 to 20 decimal digits",
|
||||
),
|
||||
] {
|
||||
let form = MultiValueForm::parse(form.as_bytes());
|
||||
assert_eq!(
|
||||
build_screen_share_delivery_update(&form).expect_err("invalid rollout field"),
|
||||
message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -73,15 +73,11 @@ pub async fn render(
|
||||
.map(|r| r.sessions)
|
||||
.map_err(|error| tracing::warn!(%error, user_id, "admin API request failed: list user sessions"))
|
||||
.unwrap_or_default();
|
||||
let webauthn_credentials = if u.authenticator_types.contains(&2) {
|
||||
client
|
||||
.list_webauthn_credentials(user_id)
|
||||
.await
|
||||
.map_err(|error| tracing::warn!(%error, user_id, "admin API request failed: list webauthn credentials"))
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let webauthn_credentials = client
|
||||
.list_webauthn_credentials(user_id)
|
||||
.await
|
||||
.map_err(|error| tracing::warn!(%error, user_id, "admin API request failed: list webauthn credentials"))
|
||||
.unwrap_or_default();
|
||||
Some(tabs::account::account_tab(
|
||||
config,
|
||||
&u,
|
||||
@@ -245,6 +241,7 @@ pub async fn render(
|
||||
admin_user_id: None,
|
||||
target_id: Some(user_id.to_owned()),
|
||||
target_type: None,
|
||||
access: Some("write".to_owned()),
|
||||
sort_by: Some("created_at".to_owned()),
|
||||
sort_order: Some("desc".to_owned()),
|
||||
limit,
|
||||
|
||||
@@ -72,7 +72,7 @@ pub fn audit_logs_for_target(
|
||||
let total_pages = total.div_ceil(u64::from(PAGE_SIZE)).max(1);
|
||||
let page_number = u64::from(current_page) + 1;
|
||||
let all_logs_href = format!(
|
||||
"{base_path}/audit-logs?target_id={}",
|
||||
"{base_path}/audit-logs?target_id={}&access=write",
|
||||
urlencoding::encode(target_id)
|
||||
);
|
||||
|
||||
@@ -94,7 +94,7 @@ pub fn audit_logs_for_target(
|
||||
}
|
||||
}
|
||||
@if entries.is_empty() {
|
||||
(empty_state("No admin actions have been recorded against this entity."))
|
||||
(empty_state("No admin write actions have been recorded against this entity."))
|
||||
} @else {
|
||||
(table_container(html! {
|
||||
(table(html! {
|
||||
|
||||
@@ -22,6 +22,7 @@ pub struct AuditLogsParams<'a> {
|
||||
pub admin_user_id: &'a str,
|
||||
pub target_id: &'a str,
|
||||
pub target_type: &'a str,
|
||||
pub access: &'a str,
|
||||
pub sort_by: &'a str,
|
||||
pub sort_order: &'a str,
|
||||
pub limit: u32,
|
||||
@@ -42,6 +43,11 @@ fn filters_section(base: &str, params: &AuditLogsParams<'_>) -> Markup {
|
||||
("file_sha", "File SHA"),
|
||||
("email", "Email"),
|
||||
];
|
||||
let access_options: &[(&str, &str)] = &[
|
||||
("", "All entries"),
|
||||
("write", "Writes only"),
|
||||
("read", "Reads only"),
|
||||
];
|
||||
let sort_options: &[(&str, &str)] = &[("createdAt", "Created at"), ("relevance", "Relevance")];
|
||||
let order_options: &[(&str, &str)] = &[("desc", "Newest first"), ("asc", "Oldest first")];
|
||||
let limit_options: &[(&str, &str)] =
|
||||
@@ -60,6 +66,7 @@ fn filters_section(base: &str, params: &AuditLogsParams<'_>) -> Markup {
|
||||
"Filter by admin user ID..."))
|
||||
(select_input("target_type", "Target type",
|
||||
target_type_options, params.target_type))
|
||||
(select_input("access", "Access", access_options, params.access))
|
||||
(select_input("sort_by", "Sort by", sort_options, params.sort_by))
|
||||
(select_input("sort_order", "Order", order_options, params.sort_order))
|
||||
(select_input("limit", "Page size", limit_options, &limit_str))
|
||||
@@ -81,6 +88,7 @@ fn build_pagination_url(base: &str, page: u32, params: &AuditLogsParams<'_>) ->
|
||||
("admin_user_id", params.admin_user_id),
|
||||
("target_id", params.target_id),
|
||||
("target_type", params.target_type),
|
||||
("access", params.access),
|
||||
("sort_by", params.sort_by),
|
||||
("sort_order", params.sort_order),
|
||||
]
|
||||
@@ -169,6 +177,7 @@ mod tests {
|
||||
admin_user_id: "",
|
||||
target_id: "",
|
||||
target_type: "bulk_job",
|
||||
access: "",
|
||||
sort_by: "createdAt",
|
||||
sort_order: "desc",
|
||||
limit: 50,
|
||||
@@ -176,5 +185,28 @@ mod tests {
|
||||
};
|
||||
let markup = filters_section("/admin", ¶ms).into_string();
|
||||
assert!(markup.contains(r#"<option value="bulk_job" selected>Bulk job</option>"#));
|
||||
assert!(markup.contains(r#"<option value="" selected>All entries</option>"#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn access_filter_survives_form_and_pagination() {
|
||||
let params = AuditLogsParams {
|
||||
query: "",
|
||||
admin_user_id: "",
|
||||
target_id: "1500000000000000001",
|
||||
target_type: "",
|
||||
access: "read",
|
||||
sort_by: "createdAt",
|
||||
sort_order: "desc",
|
||||
limit: 50,
|
||||
current_page: 0,
|
||||
};
|
||||
let markup = filters_section("/admin", ¶ms).into_string();
|
||||
assert!(markup.contains(r#"<select id="access" name="access""#));
|
||||
assert!(markup.contains(r#"<option value="read" selected>Reads only</option>"#));
|
||||
assert_eq!(
|
||||
build_pagination_url("/admin", 1, ¶ms),
|
||||
"/admin/audit-logs?page=1&target_id=1500000000000000001&access=read&sort_by=createdAt&sort_order=desc&limit=50"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,12 @@
|
||||
|
||||
use crate::{
|
||||
api::types::{
|
||||
AppPublicConfigResponse, BlockedMessageGroupsConfigResponse,
|
||||
ExperimentDeliveryConfigResponse, ExpressionInfoCardConfigResponse,
|
||||
GatewayRolloutConfigResponse, GuildActivityLogPresentationConfigResponse,
|
||||
GuildHeaderCollapseConfigResponse, InstanceConfigResponse, InstanceIntegrationsResponse,
|
||||
AppPublicConfigResponse, EXPERIMENT_MAX_TARGETED_USERS, ExperimentDeliveryConfigResponse,
|
||||
GatewayRolloutConfigResponse, InstanceConfigResponse, InstanceIntegrationsResponse,
|
||||
InstanceMediaResponse, InstancePolicyResponse, InstanceRegistrationResponse,
|
||||
LimitConfigResponse, MessageHoverTrackingConfigResponse,
|
||||
MessageKeyboardFocusConfigResponse, NoiseSuppressionBackend, PendingRegistrationResponse,
|
||||
RegistrationUrlResponse, SsoConfigResponse, TypingIndicatorReworkConfigResponse,
|
||||
VOICE_NS_MAX_GUILD_OVERRIDES, VOICE_NS_MAX_TARGETED_USERS,
|
||||
LimitConfigResponse, NoiseSuppressionBackend, PendingRegistrationResponse,
|
||||
RegistrationUrlResponse, SCREEN_SHARE_DELIVERY_DEFAULT_SALT,
|
||||
ScreenShareDeliveryConfigResponse, SsoConfigResponse, VOICE_NS_MAX_GUILD_OVERRIDES,
|
||||
VoiceNoiseSuppressionConfigResponse,
|
||||
},
|
||||
config::AdminConfig,
|
||||
@@ -152,13 +149,7 @@ pub fn instance_config_page(
|
||||
html! {
|
||||
(gateway_rollout_section(base, csrf_token, &instance_config.gateway_rollout))
|
||||
(voice_noise_suppression_section(base, csrf_token, &instance_config.voice_noise_suppression))
|
||||
(message_hover_tracking_section(base, csrf_token, &instance_config.message_hover_tracking))
|
||||
(message_keyboard_focus_section(base, csrf_token, &instance_config.message_keyboard_focus))
|
||||
(blocked_message_groups_section(base, csrf_token, &instance_config.blocked_message_groups))
|
||||
(guild_activity_log_presentation_section(base, csrf_token, &instance_config.guild_activity_log_presentation))
|
||||
(expression_info_card_section(base, csrf_token, &instance_config.expression_info_card))
|
||||
(guild_header_collapse_section(base, csrf_token, &instance_config.guild_header_collapse))
|
||||
(typing_indicator_rework_section(base, csrf_token, &instance_config.typing_indicator_rework))
|
||||
(screen_share_delivery_section(base, csrf_token, &instance_config.screen_share_delivery))
|
||||
(experiment_delivery_section(base, csrf_token, &instance_config.experiment_delivery))
|
||||
@if let Some(limit_config) = limit_config {
|
||||
(limit_config_section(base, limit_config))
|
||||
@@ -859,6 +850,18 @@ fn app_public_config_section(
|
||||
app_public.branding.favicon_url.as_deref().unwrap_or(""),
|
||||
"https://example.com/favicon.ico",
|
||||
))
|
||||
(text_input(
|
||||
"app_status_page_url",
|
||||
"Status page URL",
|
||||
app_public.branding.status_page_url.as_deref().unwrap_or(""),
|
||||
"https://fluxerstatus.com",
|
||||
))
|
||||
(text_input(
|
||||
"app_status_page_incident_history_url",
|
||||
"Status page history URL",
|
||||
app_public.branding.status_page_incident_history_url.as_deref().unwrap_or(""),
|
||||
"https://fluxerstatus.com/history",
|
||||
))
|
||||
}
|
||||
div class="space-y-2" {
|
||||
(checkbox(
|
||||
@@ -1104,7 +1107,7 @@ fn voice_noise_suppression_section(
|
||||
))
|
||||
(entry_count_hint(
|
||||
voice_noise_suppression.included_user_ids.len(),
|
||||
VOICE_NS_MAX_TARGETED_USERS,
|
||||
EXPERIMENT_MAX_TARGETED_USERS,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"One snowflake per line, or comma separated. These users are targeted \
|
||||
@@ -1124,7 +1127,7 @@ fn voice_noise_suppression_section(
|
||||
))
|
||||
(entry_count_hint(
|
||||
voice_noise_suppression.excluded_user_ids.len(),
|
||||
VOICE_NS_MAX_TARGETED_USERS,
|
||||
EXPERIMENT_MAX_TARGETED_USERS,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Same format. Exclusion wins over both the always-on list and the \
|
||||
@@ -1156,17 +1159,6 @@ fn voice_noise_suppression_section(
|
||||
}
|
||||
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Processing" }
|
||||
(checkbox(
|
||||
"voice_ns_stereo_enabled",
|
||||
"true",
|
||||
"Process stereo input instead of downmixing to mono",
|
||||
voice_noise_suppression.stereo_enabled,
|
||||
true,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Costs more CPU on the client. Leave off unless you are testing stereo \
|
||||
capture."
|
||||
}
|
||||
div class="grid grid-cols-1 gap-4 sm:grid-cols-2" {
|
||||
(number_field(
|
||||
"voice_ns_suppression_strength",
|
||||
@@ -1186,63 +1178,62 @@ fn voice_noise_suppression_section(
|
||||
)
|
||||
}
|
||||
|
||||
fn message_hover_tracking_section(
|
||||
fn screen_share_delivery_section(
|
||||
base: &str,
|
||||
csrf_token: &str,
|
||||
message_hover_tracking: &MessageHoverTrackingConfigResponse,
|
||||
screen_share_delivery: &ScreenShareDeliveryConfigResponse,
|
||||
) -> Markup {
|
||||
let status = if message_hover_tracking.enabled {
|
||||
let status = if screen_share_delivery.enabled {
|
||||
("Live", BadgeVariant::Success)
|
||||
} else {
|
||||
("Inert", BadgeVariant::Default)
|
||||
};
|
||||
let included_user_ids = message_hover_tracking.included_user_ids.join("\n");
|
||||
let excluded_user_ids = message_hover_tracking.excluded_user_ids.join("\n");
|
||||
let included_user_ids = screen_share_delivery.included_user_ids.join("\n");
|
||||
let excluded_user_ids = screen_share_delivery.excluded_user_ids.join("\n");
|
||||
section_card_with_description(
|
||||
"Message Hover Tracking",
|
||||
"Picks which message hover implementation targeted clients run in the message list. A \
|
||||
targeted client resolves the hovered message from one shared pointer oracle and drives \
|
||||
the message action bar from that state. While the master switch below is off every \
|
||||
client keeps the per-row implementation it ships with, whatever the rest of these \
|
||||
fields say.",
|
||||
"Screen Share Delivery",
|
||||
"Pick how many clients publish screen shares through the reworked delivery path. While \
|
||||
the master switch below is off nothing on this form reaches any client: every user \
|
||||
keeps the screen share pipeline they have today, whatever the rest of these fields say. \
|
||||
A client that is already sharing keeps the path it started on until the share ends.",
|
||||
html! {
|
||||
form method="post" action={(base) "/instance-config?action=update_message_hover_tracking"} {
|
||||
form method="post" action={(base) "/instance-config?action=update_screen_share_delivery"} {
|
||||
(csrf_input(csrf_token))
|
||||
div class="space-y-6" {
|
||||
div class="flex flex-wrap items-center gap-2" {
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Master switch" }
|
||||
(badge(status.0, status.1))
|
||||
span class="text-xs text-neutral-500" {
|
||||
"Config version " (message_hover_tracking.config_version)
|
||||
"Config version " (screen_share_delivery.config_version)
|
||||
}
|
||||
}
|
||||
(checkbox(
|
||||
"message_hover_enabled",
|
||||
"screen_share_delivery_enabled",
|
||||
"true",
|
||||
"Serve message hover tracking assignments to clients",
|
||||
message_hover_tracking.enabled,
|
||||
"Serve screen share delivery assignments to clients",
|
||||
screen_share_delivery.enabled,
|
||||
true,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Off is the safe state. With this unchecked every client is told the \
|
||||
rollout is inert and keeps its current hover behavior, so the rollout \
|
||||
and targeting fields below have no effect at all."
|
||||
feature is inert and keeps its current behavior, so the rollout and \
|
||||
targeting fields below have no effect at all."
|
||||
}
|
||||
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Rollout" }
|
||||
(number_field(
|
||||
"message_hover_rollout_basis_points",
|
||||
"screen_share_delivery_rollout_basis_points",
|
||||
"Rollout (basis points)",
|
||||
&message_hover_tracking.rollout_basis_points.to_string(),
|
||||
&screen_share_delivery.rollout_basis_points.to_string(),
|
||||
Some(0), Some(10000), "1",
|
||||
Some("Share of users bucketed into the canary, in basis points: 0 is nobody, 100 is 1%, 10000 is everybody."),
|
||||
))
|
||||
div class="flex flex-col gap-2" {
|
||||
(text_input(
|
||||
"message_hover_rollout_salt",
|
||||
"screen_share_delivery_rollout_salt",
|
||||
"Rollout Salt",
|
||||
&message_hover_tracking.rollout_salt,
|
||||
"message-hover-tracking-v1",
|
||||
&screen_share_delivery.rollout_salt,
|
||||
SCREEN_SHARE_DELIVERY_DEFAULT_SALT,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Seeds the bucketing hash. Changing it reshuffles which users fall \
|
||||
@@ -1252,13 +1243,17 @@ fn message_hover_tracking_section(
|
||||
}
|
||||
div class="flex flex-col gap-2" {
|
||||
(textarea_input(
|
||||
"message_hover_included_user_ids",
|
||||
"screen_share_delivery_included_user_ids",
|
||||
"Always-on User IDs",
|
||||
"1500000000000000001\n1500000000000000002",
|
||||
&included_user_ids,
|
||||
4,
|
||||
false,
|
||||
))
|
||||
(entry_count_hint(
|
||||
screen_share_delivery.included_user_ids.len(),
|
||||
EXPERIMENT_MAX_TARGETED_USERS,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"One snowflake per line, or comma separated. These users are targeted \
|
||||
regardless of the percentage above. IDs must contain 1 to 20 decimal \
|
||||
@@ -1268,13 +1263,17 @@ fn message_hover_tracking_section(
|
||||
}
|
||||
div class="flex flex-col gap-2" {
|
||||
(textarea_input(
|
||||
"message_hover_excluded_user_ids",
|
||||
"screen_share_delivery_excluded_user_ids",
|
||||
"Never-on User IDs",
|
||||
"1500000000000000003\n1500000000000000004",
|
||||
&excluded_user_ids,
|
||||
4,
|
||||
false,
|
||||
))
|
||||
(entry_count_hint(
|
||||
screen_share_delivery.excluded_user_ids.len(),
|
||||
EXPERIMENT_MAX_TARGETED_USERS,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Same format. Exclusion wins over both the always-on list and the \
|
||||
percentage, so this is the per-user kill switch."
|
||||
@@ -1282,630 +1281,7 @@ fn message_hover_tracking_section(
|
||||
}
|
||||
|
||||
(form_actions(html! {
|
||||
(submit_button("Save Message Hover Tracking Configuration"))
|
||||
}))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn message_keyboard_focus_section(
|
||||
base: &str,
|
||||
csrf_token: &str,
|
||||
message_keyboard_focus: &MessageKeyboardFocusConfigResponse,
|
||||
) -> Markup {
|
||||
let status = if message_keyboard_focus.enabled {
|
||||
("Live", BadgeVariant::Success)
|
||||
} else {
|
||||
("Inert", BadgeVariant::Default)
|
||||
};
|
||||
let included_user_ids = message_keyboard_focus.included_user_ids.join("\n");
|
||||
let excluded_user_ids = message_keyboard_focus.excluded_user_ids.join("\n");
|
||||
section_card_with_description(
|
||||
"Message Keyboard Focus",
|
||||
"Picks whether targeted clients run the keyboard navigation rework in the message list. \
|
||||
A targeted client reaches the message list from the composer with one Tab, walks \
|
||||
messages with the arrow keys through revealed blocked groups, and draws the focus ring \
|
||||
inside each row. While the master switch below is off every client keeps the keyboard \
|
||||
navigation it ships with, whatever the rest of these fields say.",
|
||||
html! {
|
||||
form method="post" action={(base) "/instance-config?action=update_message_keyboard_focus"} {
|
||||
(csrf_input(csrf_token))
|
||||
div class="space-y-6" {
|
||||
div class="flex flex-wrap items-center gap-2" {
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Master switch" }
|
||||
(badge(status.0, status.1))
|
||||
span class="text-xs text-neutral-500" {
|
||||
"Config version " (message_keyboard_focus.config_version)
|
||||
}
|
||||
}
|
||||
(checkbox(
|
||||
"message_keyboard_focus_enabled",
|
||||
"true",
|
||||
"Serve message keyboard focus assignments to clients",
|
||||
message_keyboard_focus.enabled,
|
||||
true,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Off is the safe state. With this unchecked every client is told the \
|
||||
rollout is inert and keeps its current keyboard navigation, so the rollout \
|
||||
and targeting fields below have no effect at all."
|
||||
}
|
||||
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Rollout" }
|
||||
(number_field(
|
||||
"message_keyboard_focus_rollout_basis_points",
|
||||
"Rollout (basis points)",
|
||||
&message_keyboard_focus.rollout_basis_points.to_string(),
|
||||
Some(0), Some(10000), "1",
|
||||
Some("Share of users bucketed into the canary, in basis points: 0 is nobody, 100 is 1%, 10000 is everybody."),
|
||||
))
|
||||
div class="flex flex-col gap-2" {
|
||||
(text_input(
|
||||
"message_keyboard_focus_rollout_salt",
|
||||
"Rollout Salt",
|
||||
&message_keyboard_focus.rollout_salt,
|
||||
"message-keyboard-focus-v1",
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Seeds the bucketing hash. Changing it reshuffles which users fall \
|
||||
inside the percentage above. Leave it alone to keep the current \
|
||||
cohort stable."
|
||||
}
|
||||
}
|
||||
div class="flex flex-col gap-2" {
|
||||
(textarea_input(
|
||||
"message_keyboard_focus_included_user_ids",
|
||||
"Always-on User IDs",
|
||||
"1500000000000000001\n1500000000000000002",
|
||||
&included_user_ids,
|
||||
4,
|
||||
false,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"One snowflake per line, or comma separated. These users are targeted \
|
||||
regardless of the percentage above. IDs must contain 1 to 20 decimal \
|
||||
digits. Invalid entries prevent the save; blank entries and duplicate \
|
||||
IDs are ignored."
|
||||
}
|
||||
}
|
||||
div class="flex flex-col gap-2" {
|
||||
(textarea_input(
|
||||
"message_keyboard_focus_excluded_user_ids",
|
||||
"Never-on User IDs",
|
||||
"1500000000000000003\n1500000000000000004",
|
||||
&excluded_user_ids,
|
||||
4,
|
||||
false,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Same format. Exclusion wins over both the always-on list and the \
|
||||
percentage, so this is the per-user kill switch."
|
||||
}
|
||||
}
|
||||
|
||||
(form_actions(html! {
|
||||
(submit_button("Save Message Keyboard Focus Configuration"))
|
||||
}))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn blocked_message_groups_section(
|
||||
base: &str,
|
||||
csrf_token: &str,
|
||||
blocked_message_groups: &BlockedMessageGroupsConfigResponse,
|
||||
) -> Markup {
|
||||
let status = if blocked_message_groups.enabled {
|
||||
("Live", BadgeVariant::Success)
|
||||
} else {
|
||||
("Inert", BadgeVariant::Default)
|
||||
};
|
||||
let included_user_ids = blocked_message_groups.included_user_ids.join("\n");
|
||||
let excluded_user_ids = blocked_message_groups.excluded_user_ids.join("\n");
|
||||
section_card_with_description(
|
||||
"Blocked Message Groups",
|
||||
"Picks how targeted clients render a revealed block of blocked or suspected spam \
|
||||
messages. A targeted client draws the block full width, spaces consecutive message \
|
||||
groups inside it, and keys an unread divider apart from the group below it. While the \
|
||||
master switch below is off every client keeps the rendering it ships with, whatever the \
|
||||
rest of these fields say.",
|
||||
html! {
|
||||
form method="post" action={(base) "/instance-config?action=update_blocked_message_groups"} {
|
||||
(csrf_input(csrf_token))
|
||||
div class="space-y-6" {
|
||||
div class="flex flex-wrap items-center gap-2" {
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Master switch" }
|
||||
(badge(status.0, status.1))
|
||||
span class="text-xs text-neutral-500" {
|
||||
"Config version " (blocked_message_groups.config_version)
|
||||
}
|
||||
}
|
||||
(checkbox(
|
||||
"blocked_groups_enabled",
|
||||
"true",
|
||||
"Serve blocked message groups assignments to clients",
|
||||
blocked_message_groups.enabled,
|
||||
true,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Off is the safe state. With this unchecked every client is told the \
|
||||
rollout is inert and keeps its current rendering, so the rollout \
|
||||
and targeting fields below have no effect at all."
|
||||
}
|
||||
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Rollout" }
|
||||
(number_field(
|
||||
"blocked_groups_rollout_basis_points",
|
||||
"Rollout (basis points)",
|
||||
&blocked_message_groups.rollout_basis_points.to_string(),
|
||||
Some(0), Some(10000), "1",
|
||||
Some("Share of users bucketed into the canary, in basis points: 0 is nobody, 100 is 1%, 10000 is everybody."),
|
||||
))
|
||||
div class="flex flex-col gap-2" {
|
||||
(text_input(
|
||||
"blocked_groups_rollout_salt",
|
||||
"Rollout Salt",
|
||||
&blocked_message_groups.rollout_salt,
|
||||
"blocked-message-groups-v1",
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Seeds the bucketing hash. Changing it reshuffles which users fall \
|
||||
inside the percentage above. Leave it alone to keep the current \
|
||||
cohort stable."
|
||||
}
|
||||
}
|
||||
div class="flex flex-col gap-2" {
|
||||
(textarea_input(
|
||||
"blocked_groups_included_user_ids",
|
||||
"Always-on User IDs",
|
||||
"1500000000000000001\n1500000000000000002",
|
||||
&included_user_ids,
|
||||
4,
|
||||
false,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"One snowflake per line, or comma separated. These users are targeted \
|
||||
regardless of the percentage above. IDs must contain 1 to 20 decimal \
|
||||
digits. Invalid entries prevent the save; blank entries and duplicate \
|
||||
IDs are ignored."
|
||||
}
|
||||
}
|
||||
div class="flex flex-col gap-2" {
|
||||
(textarea_input(
|
||||
"blocked_groups_excluded_user_ids",
|
||||
"Never-on User IDs",
|
||||
"1500000000000000003\n1500000000000000004",
|
||||
&excluded_user_ids,
|
||||
4,
|
||||
false,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Same format. Exclusion wins over both the always-on list and the \
|
||||
percentage, so this is the per-user kill switch."
|
||||
}
|
||||
}
|
||||
|
||||
(form_actions(html! {
|
||||
(submit_button("Save Blocked Message Groups Configuration"))
|
||||
}))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn guild_activity_log_presentation_section(
|
||||
base: &str,
|
||||
csrf_token: &str,
|
||||
guild_activity_log_presentation: &GuildActivityLogPresentationConfigResponse,
|
||||
) -> Markup {
|
||||
let status = if guild_activity_log_presentation.enabled {
|
||||
("Live", BadgeVariant::Success)
|
||||
} else {
|
||||
("Inert", BadgeVariant::Default)
|
||||
};
|
||||
let included_user_ids = guild_activity_log_presentation.included_user_ids.join("\n");
|
||||
let excluded_user_ids = guild_activity_log_presentation.excluded_user_ids.join("\n");
|
||||
section_card_with_description(
|
||||
"Guild Activity Log Presentation",
|
||||
"Picks which activity log rendering targeted clients run in community settings. A \
|
||||
targeted client renders each activity log entry through the rewritten presenters. \
|
||||
While the master switch below is off every client keeps the previous activity log \
|
||||
rendering, whatever the rest of these fields say.",
|
||||
html! {
|
||||
form method="post" action={(base) "/instance-config?action=update_guild_activity_log_presentation"} {
|
||||
(csrf_input(csrf_token))
|
||||
div class="space-y-6" {
|
||||
div class="flex flex-wrap items-center gap-2" {
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Master switch" }
|
||||
(badge(status.0, status.1))
|
||||
span class="text-xs text-neutral-500" {
|
||||
"Config version " (guild_activity_log_presentation.config_version)
|
||||
}
|
||||
}
|
||||
(checkbox(
|
||||
"guild_activity_log_presentation_enabled",
|
||||
"true",
|
||||
"Serve guild activity log presentation assignments to clients",
|
||||
guild_activity_log_presentation.enabled,
|
||||
true,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Off is the safe state. With this unchecked every client is told the \
|
||||
rollout is inert and keeps the previous activity log rendering, so the \
|
||||
rollout and targeting fields below have no effect at all."
|
||||
}
|
||||
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Rollout" }
|
||||
(number_field(
|
||||
"guild_activity_log_presentation_rollout_basis_points",
|
||||
"Rollout (basis points)",
|
||||
&guild_activity_log_presentation.rollout_basis_points.to_string(),
|
||||
Some(0), Some(10000), "1",
|
||||
Some("Share of users bucketed into the canary, in basis points: 0 is nobody, 100 is 1%, 10000 is everybody."),
|
||||
))
|
||||
div class="flex flex-col gap-2" {
|
||||
(text_input(
|
||||
"guild_activity_log_presentation_rollout_salt",
|
||||
"Rollout Salt",
|
||||
&guild_activity_log_presentation.rollout_salt,
|
||||
"guild-activity-log-presentation-v1",
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Seeds the bucketing hash. Changing it reshuffles which users fall \
|
||||
inside the percentage above. Leave it alone to keep the current \
|
||||
cohort stable."
|
||||
}
|
||||
}
|
||||
div class="flex flex-col gap-2" {
|
||||
(textarea_input(
|
||||
"guild_activity_log_presentation_included_user_ids",
|
||||
"Always-on User IDs",
|
||||
"1500000000000000001\n1500000000000000002",
|
||||
&included_user_ids,
|
||||
4,
|
||||
false,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"One snowflake per line, or comma separated. These users are targeted \
|
||||
regardless of the percentage above. IDs must contain 1 to 20 decimal \
|
||||
digits. Invalid entries prevent the save. Blank entries and duplicate \
|
||||
IDs are ignored."
|
||||
}
|
||||
}
|
||||
div class="flex flex-col gap-2" {
|
||||
(textarea_input(
|
||||
"guild_activity_log_presentation_excluded_user_ids",
|
||||
"Never-on User IDs",
|
||||
"1500000000000000003\n1500000000000000004",
|
||||
&excluded_user_ids,
|
||||
4,
|
||||
false,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Same format. Exclusion wins over both the always-on list and the \
|
||||
percentage, so this is the per-user kill switch."
|
||||
}
|
||||
}
|
||||
|
||||
(form_actions(html! {
|
||||
(submit_button("Save Guild Activity Log Presentation Configuration"))
|
||||
}))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn expression_info_card_section(
|
||||
base: &str,
|
||||
csrf_token: &str,
|
||||
expression_info_card: &ExpressionInfoCardConfigResponse,
|
||||
) -> Markup {
|
||||
let status = if expression_info_card.enabled {
|
||||
("Live", BadgeVariant::Success)
|
||||
} else {
|
||||
("Inert", BadgeVariant::Default)
|
||||
};
|
||||
let included_user_ids = expression_info_card.included_user_ids.join("\n");
|
||||
let excluded_user_ids = expression_info_card.excluded_user_ids.join("\n");
|
||||
section_card_with_description(
|
||||
"Expression Info Card",
|
||||
"Picks what a targeted client shows for an emoji or a sticker in a message. A targeted \
|
||||
client opens a click-triggered info card that names the expression, says where it comes \
|
||||
from, and offers a row for the source community the reader can open. While the master \
|
||||
switch below is off every client keeps the hover tooltip it ships with, whatever the \
|
||||
rest of these fields say.",
|
||||
html! {
|
||||
form method="post" action={(base) "/instance-config?action=update_expression_info_card"} {
|
||||
(csrf_input(csrf_token))
|
||||
div class="space-y-6" {
|
||||
div class="flex flex-wrap items-center gap-2" {
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Master switch" }
|
||||
(badge(status.0, status.1))
|
||||
span class="text-xs text-neutral-500" {
|
||||
"Config version " (expression_info_card.config_version)
|
||||
}
|
||||
}
|
||||
(checkbox(
|
||||
"expression_card_enabled",
|
||||
"true",
|
||||
"Serve expression info card assignments to clients",
|
||||
expression_info_card.enabled,
|
||||
true,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Off is the safe state. With this unchecked every client is told the \
|
||||
rollout is inert and keeps its current tooltip, so the rollout \
|
||||
and targeting fields below have no effect at all."
|
||||
}
|
||||
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Rollout" }
|
||||
(number_field(
|
||||
"expression_card_rollout_basis_points",
|
||||
"Rollout (basis points)",
|
||||
&expression_info_card.rollout_basis_points.to_string(),
|
||||
Some(0), Some(10000), "1",
|
||||
Some("Share of users bucketed into the canary, in basis points: 0 is nobody, 100 is 1%, 10000 is everybody."),
|
||||
))
|
||||
div class="flex flex-col gap-2" {
|
||||
(text_input(
|
||||
"expression_card_rollout_salt",
|
||||
"Rollout Salt",
|
||||
&expression_info_card.rollout_salt,
|
||||
"expression-info-card-v1",
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Seeds the bucketing hash. Changing it reshuffles which users fall \
|
||||
inside the percentage above. Leave it alone to keep the current \
|
||||
cohort stable."
|
||||
}
|
||||
}
|
||||
div class="flex flex-col gap-2" {
|
||||
(textarea_input(
|
||||
"expression_card_included_user_ids",
|
||||
"Always-on User IDs",
|
||||
"1500000000000000001\n1500000000000000002",
|
||||
&included_user_ids,
|
||||
4,
|
||||
false,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"One snowflake per line, or comma separated. These users are targeted \
|
||||
regardless of the percentage above. IDs must contain 1 to 20 decimal \
|
||||
digits. Invalid entries prevent the save; blank entries and duplicate \
|
||||
IDs are ignored."
|
||||
}
|
||||
}
|
||||
div class="flex flex-col gap-2" {
|
||||
(textarea_input(
|
||||
"expression_card_excluded_user_ids",
|
||||
"Never-on User IDs",
|
||||
"1500000000000000003\n1500000000000000004",
|
||||
&excluded_user_ids,
|
||||
4,
|
||||
false,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Same format. Exclusion wins over both the always-on list and the \
|
||||
percentage, so this is the per-user kill switch."
|
||||
}
|
||||
}
|
||||
|
||||
(form_actions(html! {
|
||||
(submit_button("Save Expression Info Card Configuration"))
|
||||
}))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn guild_header_collapse_section(
|
||||
base: &str,
|
||||
csrf_token: &str,
|
||||
guild_header_collapse: &GuildHeaderCollapseConfigResponse,
|
||||
) -> Markup {
|
||||
let status = if guild_header_collapse.enabled {
|
||||
("Live", BadgeVariant::Success)
|
||||
} else {
|
||||
("Inert", BadgeVariant::Default)
|
||||
};
|
||||
let included_user_ids = guild_header_collapse.included_user_ids.join("\n");
|
||||
let excluded_user_ids = guild_header_collapse.excluded_user_ids.join("\n");
|
||||
section_card_with_description(
|
||||
"Guild Header Collapse",
|
||||
"Picks how a targeted client draws the guild banner above the channel list. A targeted \
|
||||
client reduces that banner to the height of the header as the channel list scrolls down \
|
||||
and returns it to full height as the list scrolls back up. While the master switch below \
|
||||
is off every client keeps the fixed banner height it ships with, whatever the rest of \
|
||||
these fields say.",
|
||||
html! {
|
||||
form method="post" action={(base) "/instance-config?action=update_guild_header_collapse"} {
|
||||
(csrf_input(csrf_token))
|
||||
div class="space-y-6" {
|
||||
div class="flex flex-wrap items-center gap-2" {
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Master switch" }
|
||||
(badge(status.0, status.1))
|
||||
span class="text-xs text-neutral-500" {
|
||||
"Config version " (guild_header_collapse.config_version)
|
||||
}
|
||||
}
|
||||
(checkbox(
|
||||
"guild_header_collapse_enabled",
|
||||
"true",
|
||||
"Serve guild header collapse assignments to clients",
|
||||
guild_header_collapse.enabled,
|
||||
true,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Off is the safe state. With this unchecked every client is told the \
|
||||
rollout is inert and keeps its current banner height, so the rollout \
|
||||
and targeting fields below have no effect at all."
|
||||
}
|
||||
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Rollout" }
|
||||
(number_field(
|
||||
"guild_header_collapse_rollout_basis_points",
|
||||
"Rollout (basis points)",
|
||||
&guild_header_collapse.rollout_basis_points.to_string(),
|
||||
Some(0), Some(10000), "1",
|
||||
Some("Share of users bucketed into the canary, in basis points: 0 is nobody, 100 is 1%, 10000 is everybody."),
|
||||
))
|
||||
div class="flex flex-col gap-2" {
|
||||
(text_input(
|
||||
"guild_header_collapse_rollout_salt",
|
||||
"Rollout Salt",
|
||||
&guild_header_collapse.rollout_salt,
|
||||
"guild-header-collapse-v1",
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Seeds the bucketing hash. Changing it reshuffles which users fall \
|
||||
inside the percentage above. Leave it alone to keep the current \
|
||||
cohort stable."
|
||||
}
|
||||
}
|
||||
div class="flex flex-col gap-2" {
|
||||
(textarea_input(
|
||||
"guild_header_collapse_included_user_ids",
|
||||
"Always-on User IDs",
|
||||
"1500000000000000001\n1500000000000000002",
|
||||
&included_user_ids,
|
||||
4,
|
||||
false,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"One snowflake per line, or comma separated. These users are targeted \
|
||||
regardless of the percentage above. IDs must contain 1 to 20 decimal \
|
||||
digits. Invalid entries prevent the save. Blank entries and duplicate \
|
||||
IDs are ignored."
|
||||
}
|
||||
}
|
||||
div class="flex flex-col gap-2" {
|
||||
(textarea_input(
|
||||
"guild_header_collapse_excluded_user_ids",
|
||||
"Never-on User IDs",
|
||||
"1500000000000000003\n1500000000000000004",
|
||||
&excluded_user_ids,
|
||||
4,
|
||||
false,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Same format. Exclusion wins over both the always-on list and the \
|
||||
percentage, so this is the per-user kill switch."
|
||||
}
|
||||
}
|
||||
|
||||
(form_actions(html! {
|
||||
(submit_button("Save Guild Header Collapse Configuration"))
|
||||
}))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn typing_indicator_rework_section(
|
||||
base: &str,
|
||||
csrf_token: &str,
|
||||
typing_indicator_rework: &TypingIndicatorReworkConfigResponse,
|
||||
) -> Markup {
|
||||
let status = if typing_indicator_rework.enabled {
|
||||
("Live", BadgeVariant::Success)
|
||||
} else {
|
||||
("Inert", BadgeVariant::Default)
|
||||
};
|
||||
let included_user_ids = typing_indicator_rework.included_user_ids.join("\n");
|
||||
let excluded_user_ids = typing_indicator_rework.excluded_user_ids.join("\n");
|
||||
section_card_with_description(
|
||||
"Typing Indicator Rework",
|
||||
"Picks how a targeted client sends and shows typing indicators. A targeted client sends a \
|
||||
typing signal 1.5 seconds after someone starts typing and then at most once every 8 \
|
||||
seconds, names up to three typists, and announces those names to screen readers even \
|
||||
where the visible row collapses them. While the master switch below is off every client \
|
||||
keeps the typing behaviour it ships with, whatever the rest of these fields say.",
|
||||
html! {
|
||||
form method="post" action={(base) "/instance-config?action=update_typing_indicator_rework"} {
|
||||
(csrf_input(csrf_token))
|
||||
div class="space-y-6" {
|
||||
div class="flex flex-wrap items-center gap-2" {
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Master switch" }
|
||||
(badge(status.0, status.1))
|
||||
span class="text-xs text-neutral-500" {
|
||||
"Config version " (typing_indicator_rework.config_version)
|
||||
}
|
||||
}
|
||||
(checkbox(
|
||||
"typing_indicator_rework_enabled",
|
||||
"true",
|
||||
"Serve typing indicator rework assignments to clients",
|
||||
typing_indicator_rework.enabled,
|
||||
true,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Off is the safe state. With this unchecked every client is told the \
|
||||
rollout is inert and keeps its current typing behaviour, so the rollout \
|
||||
and targeting fields below have no effect at all."
|
||||
}
|
||||
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Rollout" }
|
||||
(number_field(
|
||||
"typing_indicator_rework_rollout_basis_points",
|
||||
"Rollout (basis points)",
|
||||
&typing_indicator_rework.rollout_basis_points.to_string(),
|
||||
Some(0), Some(10000), "1",
|
||||
Some("Share of users bucketed into the canary, in basis points: 0 is nobody, 100 is 1%, 10000 is everybody."),
|
||||
))
|
||||
div class="flex flex-col gap-2" {
|
||||
(text_input(
|
||||
"typing_indicator_rework_rollout_salt",
|
||||
"Rollout Salt",
|
||||
&typing_indicator_rework.rollout_salt,
|
||||
"typing-indicator-rework-v1",
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Seeds the bucketing hash. Changing it reshuffles which users fall \
|
||||
inside the percentage above. Leave it alone to keep the current \
|
||||
cohort stable."
|
||||
}
|
||||
}
|
||||
div class="flex flex-col gap-2" {
|
||||
(textarea_input(
|
||||
"typing_indicator_rework_included_user_ids",
|
||||
"Always-on User IDs",
|
||||
"1500000000000000001\n1500000000000000002",
|
||||
&included_user_ids,
|
||||
4,
|
||||
false,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"One snowflake per line, or comma separated. These users are targeted \
|
||||
regardless of the percentage above. IDs must contain 1 to 20 decimal \
|
||||
digits. Invalid entries prevent the save. Blank entries and duplicate \
|
||||
IDs are ignored."
|
||||
}
|
||||
}
|
||||
div class="flex flex-col gap-2" {
|
||||
(textarea_input(
|
||||
"typing_indicator_rework_excluded_user_ids",
|
||||
"Never-on User IDs",
|
||||
"1500000000000000003\n1500000000000000004",
|
||||
&excluded_user_ids,
|
||||
4,
|
||||
false,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Same format. Exclusion wins over both the always-on list and the \
|
||||
percentage, so this is the per-user kill switch."
|
||||
}
|
||||
}
|
||||
|
||||
(form_actions(html! {
|
||||
(submit_button("Save Typing Indicator Rework Configuration"))
|
||||
(submit_button("Save Screen Share Delivery Configuration"))
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -1921,7 +1297,7 @@ fn experiment_delivery_section(
|
||||
section_card_with_description(
|
||||
"Experiment Delivery",
|
||||
"How often every client revalidates its experiment assignments. This is instance-wide \
|
||||
and covers every experiment, not just the one above. Raising the interval sheds \
|
||||
and covers every experiment, not just the ones above. Raising the interval sheds \
|
||||
request volume and makes a change take longer to reach a client. Raising the jitter \
|
||||
spreads a fleet that has synchronised on one tick back out across the interval.",
|
||||
html! {
|
||||
@@ -2556,10 +1932,29 @@ mod tests {
|
||||
assert!(!markup.contains("at the cap"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn screen_share_delivery_section_shows_list_counts_and_the_master_switch() {
|
||||
let screen_share_delivery = ScreenShareDeliveryConfigResponse {
|
||||
included_user_ids: vec!["1500000000000000001".to_owned()],
|
||||
excluded_user_ids: vec![
|
||||
"1500000000000000002".to_owned(),
|
||||
"1500000000000000003".to_owned(),
|
||||
],
|
||||
..ScreenShareDeliveryConfigResponse::default()
|
||||
};
|
||||
let markup =
|
||||
screen_share_delivery_section("/admin", "csrf", &screen_share_delivery).into_string();
|
||||
assert!(markup.contains("action=update_screen_share_delivery"));
|
||||
assert!(markup.contains("screen_share_delivery_enabled"));
|
||||
assert!(markup.contains("1 of 1000 stored"));
|
||||
assert!(markup.contains("2 of 1000 stored"));
|
||||
assert!(!markup.contains("at the cap"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn voice_noise_suppression_section_flags_a_list_at_its_cap() {
|
||||
let voice_noise_suppression = VoiceNoiseSuppressionConfigResponse {
|
||||
included_user_ids: (0..VOICE_NS_MAX_TARGETED_USERS)
|
||||
included_user_ids: (0..EXPERIMENT_MAX_TARGETED_USERS)
|
||||
.map(|index| index.to_string())
|
||||
.collect(),
|
||||
..VoiceNoiseSuppressionConfigResponse::default()
|
||||
@@ -2568,44 +1963,4 @@ mod tests {
|
||||
assert!(markup.contains("1000 of 1000 stored"));
|
||||
assert!(markup.contains("at the cap"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guild_header_collapse_section_posts_its_own_action_and_fields() {
|
||||
let guild_header_collapse = GuildHeaderCollapseConfigResponse {
|
||||
included_user_ids: vec!["1500000000000000001".to_owned()],
|
||||
excluded_user_ids: vec!["1500000000000000002".to_owned()],
|
||||
..GuildHeaderCollapseConfigResponse::default()
|
||||
};
|
||||
let markup =
|
||||
guild_header_collapse_section("/admin", "csrf", &guild_header_collapse).into_string();
|
||||
assert!(markup.contains("/instance-config?action=update_guild_header_collapse"));
|
||||
assert!(markup.contains("Guild Header Collapse"));
|
||||
assert!(markup.contains("guild-header-collapse-v1"));
|
||||
assert!(markup.contains("guild_header_collapse_enabled"));
|
||||
assert!(markup.contains("guild_header_collapse_rollout_basis_points"));
|
||||
assert!(markup.contains("guild_header_collapse_rollout_salt"));
|
||||
assert!(markup.contains("guild_header_collapse_included_user_ids"));
|
||||
assert!(markup.contains("guild_header_collapse_excluded_user_ids"));
|
||||
assert!(!markup.contains("expression_card_"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typing_indicator_rework_section_posts_its_own_action_and_fields() {
|
||||
let typing_indicator_rework = TypingIndicatorReworkConfigResponse {
|
||||
included_user_ids: vec!["1500000000000000001".to_owned()],
|
||||
excluded_user_ids: vec!["1500000000000000002".to_owned()],
|
||||
..TypingIndicatorReworkConfigResponse::default()
|
||||
};
|
||||
let markup = typing_indicator_rework_section("/admin", "csrf", &typing_indicator_rework)
|
||||
.into_string();
|
||||
assert!(markup.contains("/instance-config?action=update_typing_indicator_rework"));
|
||||
assert!(markup.contains("Typing Indicator Rework"));
|
||||
assert!(markup.contains("typing-indicator-rework-v1"));
|
||||
assert!(markup.contains("typing_indicator_rework_enabled"));
|
||||
assert!(markup.contains("typing_indicator_rework_rollout_basis_points"));
|
||||
assert!(markup.contains("typing_indicator_rework_rollout_salt"));
|
||||
assert!(markup.contains("typing_indicator_rework_included_user_ids"));
|
||||
assert!(markup.contains("typing_indicator_rework_excluded_user_ids"));
|
||||
assert!(!markup.contains("expression_card_"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +230,7 @@ fn deserialize_audit_logs_response() {
|
||||
"target_type": "user",
|
||||
"target_id": "1130958221824557056",
|
||||
"action": "list_user_sessions",
|
||||
"access": "read",
|
||||
"audit_log_reason": null,
|
||||
"metadata": {"session_count": "3"},
|
||||
"created_at": "2026-05-26T13:21:47.138Z"
|
||||
@@ -243,6 +244,7 @@ fn deserialize_audit_logs_response() {
|
||||
assert_eq!(resp.logs.len(), 1);
|
||||
assert_eq!(resp.logs[0].log_id, "1508822460457747580");
|
||||
assert_eq!(resp.logs[0].action, "list_user_sessions");
|
||||
assert_eq!(resp.logs[0].access.as_deref(), Some("read"));
|
||||
assert_eq!(resp.logs[0].target_type, "user");
|
||||
assert!(resp.logs[0].audit_log_reason.is_none());
|
||||
assert_eq!(resp.logs[0].metadata.get("session_count").unwrap(), "3");
|
||||
@@ -401,70 +403,22 @@ fn deserialize_instance_config_response_with_unknown_keys() {
|
||||
"included_user_ids": [],
|
||||
"excluded_user_ids": [],
|
||||
"guild_overrides": [],
|
||||
"stereo_enabled": false,
|
||||
"suppression_strength": 80
|
||||
},
|
||||
"guild_activity_log_presentation": {
|
||||
"enabled": true,
|
||||
"config_version": 2,
|
||||
"rollout_basis_points": 5000,
|
||||
"rollout_salt": "guild-activity-log-presentation-v1",
|
||||
"included_user_ids": ["1130650140672000000"],
|
||||
"excluded_user_ids": [],
|
||||
"future_presentation_knob": "verbose"
|
||||
},
|
||||
"experiment_delivery": {"poll_interval_seconds": 300, "poll_jitter_percent": 15},
|
||||
"message_hover_tracking": {
|
||||
"enabled": false,
|
||||
"config_version": 0,
|
||||
"rollout_basis_points": 0,
|
||||
"rollout_salt": "message-hover-tracking-v1",
|
||||
"included_user_ids": [],
|
||||
"excluded_user_ids": []
|
||||
},
|
||||
"message_keyboard_focus": {
|
||||
"enabled": false,
|
||||
"config_version": 0,
|
||||
"rollout_basis_points": 0,
|
||||
"rollout_salt": "message-keyboard-focus-v1",
|
||||
"included_user_ids": [],
|
||||
"excluded_user_ids": []
|
||||
},
|
||||
"blocked_message_groups": {
|
||||
"enabled": false,
|
||||
"config_version": 0,
|
||||
"rollout_basis_points": 0,
|
||||
"rollout_salt": "blocked-message-groups-v1",
|
||||
"included_user_ids": [],
|
||||
"excluded_user_ids": []
|
||||
},
|
||||
"expression_info_card": {
|
||||
"enabled": true,
|
||||
"config_version": 3,
|
||||
"rollout_basis_points": 2500,
|
||||
"rollout_salt": "expression-info-card-v1",
|
||||
"included_user_ids": ["1130650140672000000"],
|
||||
"excluded_user_ids": ["1130958221824557056"],
|
||||
"suppression_strength": 80,
|
||||
"future_presentation_knob": "verbose",
|
||||
"future_knob": 7,
|
||||
"future_object_knob": {"nested": true},
|
||||
"future_list_knob": ["a", "b"]
|
||||
},
|
||||
"guild_header_collapse": {
|
||||
"enabled": false,
|
||||
"config_version": 0,
|
||||
"rollout_basis_points": 0,
|
||||
"rollout_salt": "guild-header-collapse-v1",
|
||||
"included_user_ids": [],
|
||||
"excluded_user_ids": []
|
||||
},
|
||||
"typing_indicator_rework": {
|
||||
"enabled": false,
|
||||
"config_version": 0,
|
||||
"rollout_basis_points": 0,
|
||||
"rollout_salt": "typing-indicator-rework-v1",
|
||||
"included_user_ids": [],
|
||||
"screen_share_delivery": {
|
||||
"enabled": true,
|
||||
"config_version": 2,
|
||||
"rollout_basis_points": 2500,
|
||||
"rollout_salt": "screen-share-delivery-v1",
|
||||
"included_user_ids": ["1500000000000000001"],
|
||||
"future_delivery_knob": 9,
|
||||
"excluded_user_ids": []
|
||||
},
|
||||
"experiment_delivery": {"poll_interval_seconds": 300, "poll_jitter_percent": 15},
|
||||
"registration": {
|
||||
"mode": "open",
|
||||
"admin_registration_urls_enabled": false,
|
||||
@@ -588,28 +542,19 @@ fn deserialize_instance_config_response_with_unknown_keys() {
|
||||
);
|
||||
|
||||
assert!(!resp.self_hosted);
|
||||
assert!(resp.expression_info_card.enabled);
|
||||
assert_eq!(resp.expression_info_card.config_version, 3);
|
||||
assert_eq!(resp.expression_info_card.rollout_basis_points, 2500);
|
||||
assert!(resp.voice_noise_suppression.enabled);
|
||||
assert_eq!(resp.voice_noise_suppression.config_version, 4);
|
||||
assert_eq!(resp.voice_noise_suppression.rollout_basis_points, 10000);
|
||||
assert_eq!(*resp.voice_noise_suppression.rollout_salt, "voice-ns-v1");
|
||||
assert_eq!(resp.voice_noise_suppression.enabled_backends.len(), 3);
|
||||
assert!(resp.screen_share_delivery.enabled);
|
||||
assert_eq!(resp.screen_share_delivery.config_version, 2);
|
||||
assert_eq!(resp.screen_share_delivery.rollout_basis_points, 2500);
|
||||
assert_eq!(
|
||||
*resp.expression_info_card.rollout_salt,
|
||||
"expression-info-card-v1"
|
||||
*resp.screen_share_delivery.rollout_salt,
|
||||
"screen-share-delivery-v1"
|
||||
);
|
||||
assert_eq!(resp.expression_info_card.included_user_ids.len(), 1);
|
||||
assert_eq!(
|
||||
*resp.expression_info_card.excluded_user_ids[0],
|
||||
"1130958221824557056"
|
||||
);
|
||||
assert!(resp.guild_activity_log_presentation.enabled);
|
||||
assert_eq!(
|
||||
*resp.guild_activity_log_presentation.rollout_salt,
|
||||
"guild-activity-log-presentation-v1"
|
||||
);
|
||||
assert!(!resp.message_hover_tracking.enabled);
|
||||
assert!(!resp.message_keyboard_focus.enabled);
|
||||
assert!(!resp.blocked_message_groups.enabled);
|
||||
assert!(!resp.guild_header_collapse.enabled);
|
||||
assert!(!resp.typing_indicator_rework.enabled);
|
||||
assert_eq!(resp.screen_share_delivery.included_user_ids.len(), 1);
|
||||
assert_eq!(resp.experiment_delivery.poll_interval_seconds, 300);
|
||||
assert!(resp.policy.single_community_guild_id.is_none());
|
||||
assert_eq!(resp.policy.services.gif_enabled, Some(true));
|
||||
@@ -620,6 +565,7 @@ fn deserialize_instance_config_response_with_unknown_keys() {
|
||||
.replace("\"future_rollout_knob\": 3,", "")
|
||||
.replace("\"future_presentation_knob\": \"verbose\",", "")
|
||||
.replace("\"future_knob\": 7,", "")
|
||||
.replace("\"future_delivery_knob\": 9,", "")
|
||||
.replace("\"future_object_knob\": {\"nested\": true},", "")
|
||||
.replace("\"future_list_knob\": [\"a\", \"b\"],", "")
|
||||
.replace(
|
||||
|
||||
@@ -168,6 +168,39 @@ async fn detail_tab_routes_return_layout_or_fragments_by_route_shape() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn target_audit_log_tabs_request_write_entries_only() {
|
||||
let app = setup().await;
|
||||
for path in [
|
||||
"/users/1500000000000000001/tabs/audit_logs",
|
||||
"/guilds/1600000000000000001/tabs/audit_logs",
|
||||
] {
|
||||
let fragment = get(&app, path, &[]).await;
|
||||
assert!(fragment.contains("Temp ban"), "{path}\n{fragment}");
|
||||
assert!(!fragment.contains("Get user"), "{path}\n{fragment}");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn audit_log_page_forwards_the_access_filter() {
|
||||
let app = setup().await;
|
||||
let all = get(&app, "/audit-logs", &[]).await;
|
||||
assert!(all.contains("Temp ban"), "{all}");
|
||||
assert!(all.contains("Get user"), "{all}");
|
||||
assert!(
|
||||
all.contains(r#"<option value="" selected>All entries</option>"#),
|
||||
"{all}"
|
||||
);
|
||||
|
||||
let reads = get(&app, "/audit-logs?access=read", &[]).await;
|
||||
assert!(reads.contains("Get user"), "{reads}");
|
||||
assert!(!reads.contains("Temp ban"), "{reads}");
|
||||
assert!(
|
||||
reads.contains(r#"<option value="read" selected>Reads only</option>"#),
|
||||
"{reads}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn report_routes_keep_layout_and_fragment_contract() {
|
||||
let app = setup().await;
|
||||
@@ -432,8 +465,7 @@ async fn mutating_admin_pages_render_usable_csrf_tokens() {
|
||||
"/instance-config?action=update_gateway_rollout",
|
||||
"/instance-config?action=update_sso",
|
||||
"/instance-config?action=update_voice_noise_suppression",
|
||||
"/instance-config?action=update_guild_header_collapse",
|
||||
"/instance-config?action=update_typing_indicator_rework",
|
||||
"/instance-config?action=update_screen_share_delivery",
|
||||
"/instance-config?action=update_experiment_delivery",
|
||||
][..],
|
||||
),
|
||||
@@ -846,6 +878,25 @@ async fn mock_api(method: Method, uri: Uri) -> Response {
|
||||
json_response(instance_config_without_pending_registrations())
|
||||
}
|
||||
(Method::GET, "/admin/limit-config") => json_response(limit_config()),
|
||||
(Method::GET, "/admin/audit-logs") => {
|
||||
let access = uri.query().and_then(|query| {
|
||||
url::form_urlencoded::parse(query.as_bytes())
|
||||
.find(|(key, _)| key == "access")
|
||||
.map(|(_, value)| value.into_owned())
|
||||
});
|
||||
let logs = [
|
||||
audit_log_entry("1900000000000000101", "get_user", "read"),
|
||||
audit_log_entry("1900000000000000102", "temp_ban", "write"),
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|entry| {
|
||||
access
|
||||
.as_deref()
|
||||
.is_none_or(|access| entry.access.to_string() == access)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
json_response(json!({ "total": logs.len(), "logs": logs }))
|
||||
}
|
||||
_ => (StatusCode::NOT_FOUND, Json(json!({ "error": "not found" }))).into_response(),
|
||||
}
|
||||
}
|
||||
@@ -977,6 +1028,32 @@ fn guild_fixtures_match_generated_response_contracts() {
|
||||
assert_eq!(detail.member_count, 12);
|
||||
}
|
||||
|
||||
fn audit_log_entry(
|
||||
log_id: &str,
|
||||
action: &str,
|
||||
access: &str,
|
||||
) -> generated_types::AdminAuditLogResponseSchema {
|
||||
serde_json::from_value(json!({
|
||||
"log_id": log_id,
|
||||
"admin_user_id": "1500000000000000000",
|
||||
"admin_user": null,
|
||||
"target_type": "user",
|
||||
"target_id": "1500000000000000001",
|
||||
"target_user": null,
|
||||
"target_guild": null,
|
||||
"target_channel": null,
|
||||
"related_users": {},
|
||||
"related_guilds": {},
|
||||
"related_channels": {},
|
||||
"action": action,
|
||||
"access": access,
|
||||
"audit_log_reason": null,
|
||||
"metadata": {},
|
||||
"created_at": "2026-09-16T12:00:00.000Z"
|
||||
}))
|
||||
.expect("audit log fixture must match the generated response contract")
|
||||
}
|
||||
|
||||
fn searched_application() -> Value {
|
||||
json!({
|
||||
"id": "1700000000000000001",
|
||||
@@ -1099,22 +1176,6 @@ fn instance_config() -> Value {
|
||||
"max_concurrent_guild_starts": 16,
|
||||
"voice_e2ee_scope": "guild_feature_only"
|
||||
},
|
||||
"guild_header_collapse": {
|
||||
"enabled": false,
|
||||
"config_version": 0,
|
||||
"rollout_basis_points": 0,
|
||||
"rollout_salt": "guild-header-collapse-v1",
|
||||
"included_user_ids": [],
|
||||
"excluded_user_ids": []
|
||||
},
|
||||
"typing_indicator_rework": {
|
||||
"enabled": false,
|
||||
"config_version": 0,
|
||||
"rollout_basis_points": 0,
|
||||
"rollout_salt": "typing-indicator-rework-v1",
|
||||
"included_user_ids": [],
|
||||
"excluded_user_ids": []
|
||||
},
|
||||
"voice_noise_suppression": {
|
||||
"enabled": false,
|
||||
"config_version": 0,
|
||||
@@ -1134,9 +1195,16 @@ fn instance_config() -> Value {
|
||||
"included_user_ids": [],
|
||||
"excluded_user_ids": [],
|
||||
"guild_overrides": [],
|
||||
"stereo_enabled": false,
|
||||
"suppression_strength": 80
|
||||
},
|
||||
"screen_share_delivery": {
|
||||
"enabled": false,
|
||||
"config_version": 0,
|
||||
"rollout_basis_points": 0,
|
||||
"rollout_salt": "screen-share-delivery-v1",
|
||||
"included_user_ids": [],
|
||||
"excluded_user_ids": []
|
||||
},
|
||||
"experiment_delivery": {
|
||||
"poll_interval_seconds": 300,
|
||||
"poll_jitter_percent": 15
|
||||
|
||||
+8
-12
@@ -1,11 +1,11 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
ARG BUILD_VERSION
|
||||
|
||||
FROM node:24-bookworm-slim AS base
|
||||
FROM node:26-trixie-slim AS base
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@10.29.3 --activate
|
||||
RUN npm install -g pnpm@11.27.0
|
||||
|
||||
FROM base AS deploy
|
||||
|
||||
@@ -23,9 +23,9 @@ COPY . .
|
||||
|
||||
RUN pnpm install --frozen-lockfile
|
||||
RUN pnpm --filter fluxer_api run build
|
||||
RUN pnpm deploy --legacy --filter=fluxer_api --prod --config.allowUnusedPatches=true /out
|
||||
RUN pnpm deploy --legacy --filter=fluxer_api --prod --config.allow-unused-patches=true /out
|
||||
|
||||
FROM node:24-bookworm-slim
|
||||
FROM node:26-trixie-slim
|
||||
|
||||
ARG BUILD_VERSION
|
||||
ARG SOURCE_SHA
|
||||
@@ -45,32 +45,28 @@ LABEL app.fluxer.build-version="${BUILD_VERSION}"
|
||||
|
||||
WORKDIR /usr/src/app/fluxer_api
|
||||
|
||||
RUN echo 'deb http://deb.debian.org/debian bookworm-backports main' > /etc/apt/sources.list.d/backports.list && \
|
||||
apt-get update && apt-get install -y --no-install-recommends \
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
ffmpeg \
|
||||
libimage-exiftool-perl \
|
||||
libwebp7 \
|
||||
libwebpmux3 \
|
||||
libheif1 \
|
||||
libvips42 && \
|
||||
apt-get install -y --no-install-recommends -t bookworm-backports \
|
||||
libheif-plugin-libde265 \
|
||||
libheif-plugin-dav1d && \
|
||||
libheif-plugin-dav1d \
|
||||
libvips42t64 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@10.29.3 --activate
|
||||
RUN npm install -g pnpm@11.27.0
|
||||
|
||||
COPY --from=deploy /out ./
|
||||
COPY --from=deploy /usr/src/app/fluxer_api/dist ./dist
|
||||
COPY --from=deploy /usr/src/app/tsconfigs /usr/src/app/tsconfigs
|
||||
|
||||
RUN rm -rf pkgs && \
|
||||
mkdir -p /usr/src/app/.cache/corepack && \
|
||||
chown -R 65532:65532 /usr/src/app
|
||||
|
||||
ENV HOME=/usr/src/app
|
||||
ENV COREPACK_HOME=/usr/src/app/.cache/corepack
|
||||
ENV NODE_ENV=production
|
||||
ENV NODE_OPTIONS="--enable-source-maps"
|
||||
ENV NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
@@ -5,19 +5,19 @@
|
||||
"scripts": {
|
||||
"build": "node scripts/build.mjs",
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsgo --noEmit",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "tsx watch --clear-screen=false src/AppEntrypoint.ts",
|
||||
"start": "tsx src/AppEntrypoint.ts",
|
||||
"start:worker": "tsx src/WorkerEntrypoint.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "catalog:",
|
||||
"@atproto/jwk-jose": "catalog:",
|
||||
"@atproto/oauth-client-node": "catalog:",
|
||||
"@aws-sdk/client-s3": "catalog:",
|
||||
"@aws-sdk/lib-storage": "catalog:",
|
||||
"@aws-sdk/s3-request-presigner": "catalog:",
|
||||
"@bluesky-social/jwk-jose": "catalog:",
|
||||
"@bluesky-social/oauth-client-node": "catalog:",
|
||||
"@bufbuild/protobuf": "^2.12.0",
|
||||
"@bufbuild/protobuf": "^2.15.0",
|
||||
"@elastic/elasticsearch": "catalog:",
|
||||
"@fluxer/config": "workspace:*",
|
||||
"@fluxer/constants": "workspace:*",
|
||||
@@ -34,6 +34,8 @@
|
||||
"@hono/node-server": "catalog:",
|
||||
"@messageformat/core": "catalog:",
|
||||
"@messageformat/parser": "catalog:",
|
||||
"@nats-io/jetstream": "catalog:",
|
||||
"@nats-io/transport-node": "catalog:",
|
||||
"@pkgs/cache": "workspace:*",
|
||||
"@pkgs/captcha": "workspace:*",
|
||||
"@pkgs/cassandra": "workspace:*",
|
||||
@@ -71,7 +73,6 @@
|
||||
"lodash": "catalog:",
|
||||
"maxmind": "catalog:",
|
||||
"mime": "catalog:",
|
||||
"nats": "catalog:",
|
||||
"nodemailer": "catalog:",
|
||||
"pg": "catalog:",
|
||||
"pino": "catalog:",
|
||||
@@ -88,10 +89,10 @@
|
||||
"devDependencies": {
|
||||
"@types/archiver": "catalog:",
|
||||
"@types/lodash": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"esbuild": "catalog:",
|
||||
"msw": "catalog:",
|
||||
"typescript": "catalog:ts7",
|
||||
"vitest": "catalog:"
|
||||
},
|
||||
"packageManager": "pnpm@10.29.3"
|
||||
"packageManager": "pnpm@11.27.0"
|
||||
}
|
||||
|
||||
Vendored
+2
-2
@@ -9,14 +9,14 @@
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pkgs/kv_client": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:ts7",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"./*": "./*"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluxer/logger": "workspace:*",
|
||||
@@ -15,6 +15,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
"typescript": "catalog:ts7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
"./*": "./*"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"cassandra-driver": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
"typescript": "catalog:ts7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"./*": "./*"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elastic/elasticsearch": "catalog:",
|
||||
@@ -15,6 +15,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
"typescript": "catalog:ts7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {AuditLogSearchFilters, SearchableAuditLog} from '@fluxer/schema/src
|
||||
import type {ElasticsearchDistributedLock} from '@pkgs/elasticsearch_search/src/adapters/ElasticsearchIndexAdapter';
|
||||
import {ElasticsearchIndexAdapter} from '@pkgs/elasticsearch_search/src/adapters/ElasticsearchIndexAdapter';
|
||||
import type {ElasticsearchFilter} from '@pkgs/elasticsearch_search/src/ElasticsearchFilterUtils';
|
||||
import {compactFilters, esTermFilter} from '@pkgs/elasticsearch_search/src/ElasticsearchFilterUtils';
|
||||
import {compactFilters, esTermFilter, esTermsFilter} from '@pkgs/elasticsearch_search/src/ElasticsearchFilterUtils';
|
||||
import {ELASTICSEARCH_INDEX_DEFINITIONS} from '@pkgs/elasticsearch_search/src/ElasticsearchIndexDefinitions';
|
||||
|
||||
function buildAuditLogFilters(filters: AuditLogSearchFilters): Array<ElasticsearchFilter | undefined> {
|
||||
@@ -15,6 +15,10 @@ function buildAuditLogFilters(filters: AuditLogSearchFilters): Array<Elasticsear
|
||||
if (filters.targetType) clauses.push(esTermFilter('targetType', filters.targetType));
|
||||
if (filters.targetId) clauses.push(esTermFilter('targetId', filters.targetId));
|
||||
if (filters.action) clauses.push(esTermFilter('action', filters.action));
|
||||
if (filters.actions && filters.actions.length > 0) clauses.push(esTermsFilter('action.keyword', filters.actions));
|
||||
if (filters.excludeActions && filters.excludeActions.length > 0) {
|
||||
clauses.push({bool: {must_not: [esTermsFilter('action.keyword', filters.excludeActions)]}});
|
||||
}
|
||||
return compactFilters(clauses);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluxer/i18n": "workspace:*",
|
||||
@@ -19,8 +19,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@types/nodemailer": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:ts7",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import {createLogger} from '@fluxer/logger/src/Logger';
|
||||
import type {EmailMessage, IEmailProvider} from '@pkgs/email/src/EmailProviderTypes';
|
||||
import nodemailer from 'nodemailer';
|
||||
import nodemailer, {type Transporter} from 'nodemailer';
|
||||
|
||||
const logger = createLogger('@pkgs/email/src/SmtpEmailProvider');
|
||||
|
||||
@@ -18,7 +18,7 @@ interface SmtpEmailConfig {
|
||||
}
|
||||
|
||||
export class SmtpEmailProvider implements IEmailProvider {
|
||||
private readonly transporter: nodemailer.Transporter;
|
||||
private readonly transporter: Transporter;
|
||||
|
||||
constructor(config: SmtpEmailConfig) {
|
||||
this.transporter = nodemailer.createTransport({
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"./*": "./*"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "catalog:",
|
||||
@@ -21,6 +21,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
"typescript": "catalog:ts7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,6 +285,5 @@ export function formatGeoipLocation(result: GeoipResult, locale?: string | null)
|
||||
const localizedCountry = locale && result.countryCode ? countryDisplayName(result.countryCode, locale) : null;
|
||||
const countryLabel = localizedCountry ?? result.countryName ?? result.countryCode;
|
||||
if (countryLabel) parts.push(countryLabel);
|
||||
if (parts.length === 0) return null;
|
||||
return new Intl.ListFormat(locale ?? 'en', {style: 'narrow', type: 'unit'}).format(parts);
|
||||
return parts.length > 0 ? parts.join(', ') : null;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ interface GeoipRuntimePathOptions {
|
||||
}
|
||||
|
||||
export function parseGeoipSourceConfig(rawValue: string | undefined): GeoipSourceConfig {
|
||||
if (!rawValue || !rawValue.startsWith('s3://')) {
|
||||
if (!rawValue?.startsWith('s3://')) {
|
||||
return createGeoipFilesystemSourceConfig(rawValue);
|
||||
}
|
||||
return parseGeoipS3SourceConfig(rawValue);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluxer/constants": "workspace:*",
|
||||
@@ -17,8 +17,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"undici-types": "catalog:",
|
||||
"typescript": "catalog:ts7",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from '@pkgs/http_client/src/HttpClientRequestInternals';
|
||||
import type {HttpClientMetrics, HttpClientTelemetry} from '@pkgs/http_client/src/HttpClientTelemetryTypes';
|
||||
import type {
|
||||
FetchDispatcher,
|
||||
HttpClient,
|
||||
HttpClientFactoryOptions,
|
||||
HttpMethod,
|
||||
@@ -26,7 +27,6 @@ import type {
|
||||
StreamResponse,
|
||||
} from '@pkgs/http_client/src/HttpClientTypes';
|
||||
import {HttpError} from '@pkgs/http_client/src/HttpError';
|
||||
import type {Dispatcher} from 'undici-types';
|
||||
|
||||
const DEFAULT_SERVICE_NAME = 'unknown';
|
||||
|
||||
@@ -79,7 +79,7 @@ function createFetchInit(
|
||||
headers: Headers,
|
||||
body: string | undefined,
|
||||
signal: AbortSignal,
|
||||
dispatcher: Dispatcher | undefined,
|
||||
dispatcher: FetchDispatcher | undefined,
|
||||
): RequestInit {
|
||||
return {
|
||||
method,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {HttpClientTelemetry} from '@pkgs/http_client/src/HttpClientTelemetryTypes';
|
||||
import type {Dispatcher} from 'undici-types';
|
||||
|
||||
export type ResponseStream = ReadableStream<Uint8Array> | null;
|
||||
export type FetchDispatcher = NonNullable<RequestInit['dispatcher']>;
|
||||
export type HttpMethod = 'GET' | 'POST' | 'HEAD' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS';
|
||||
export type RequestUrlValidationPhase = 'initial' | 'redirect';
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface RequestUrlValidationContext {
|
||||
}
|
||||
|
||||
export interface RequestUrlPolicy {
|
||||
readonly dispatcher?: Dispatcher;
|
||||
readonly dispatcher?: FetchDispatcher;
|
||||
validate(url: URL, context: RequestUrlValidationContext): Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,13 @@ import dns from 'node:dns';
|
||||
import type {LookupFunction} from 'node:net';
|
||||
import {BlockList, isIP} from 'node:net';
|
||||
import {formatUrlForDiagnostics} from '@pkgs/http_client/src/HttpClientDiagnostics';
|
||||
import type {RequestUrlPolicy, RequestUrlValidationContext} from '@pkgs/http_client/src/HttpClientTypes';
|
||||
import type {
|
||||
FetchDispatcher,
|
||||
RequestUrlPolicy,
|
||||
RequestUrlValidationContext,
|
||||
} from '@pkgs/http_client/src/HttpClientTypes';
|
||||
import {HttpError} from '@pkgs/http_client/src/HttpError';
|
||||
import {Agent} from 'undici';
|
||||
import type {Dispatcher} from 'undici-types';
|
||||
import {Agent, Dispatcher1Wrapper} from 'undici';
|
||||
|
||||
const DEFAULT_DNS_CACHE_TTL_MS = 60000;
|
||||
const DNS_CACHE_MAX_ENTRIES = 10000;
|
||||
@@ -223,7 +226,7 @@ async function defaultLookupHost(hostname: string): Promise<Array<string>> {
|
||||
return addresses.map((addressEntry) => addressEntry.address);
|
||||
}
|
||||
|
||||
function createBlocklistDispatcher(allowPrivateAddresses: boolean): Dispatcher {
|
||||
function createBlocklistDispatcher(allowPrivateAddresses: boolean): FetchDispatcher {
|
||||
const lookup: LookupFunction = (hostname, options, callback) => {
|
||||
dns.lookup(hostname, {...options, all: true, order: options.order ?? 'verbatim'}, (error, addresses) => {
|
||||
if (error) {
|
||||
@@ -246,15 +249,18 @@ function createBlocklistDispatcher(allowPrivateAddresses: boolean): Dispatcher {
|
||||
callback(null, primary.address, primary.family);
|
||||
});
|
||||
};
|
||||
return new Agent({
|
||||
connect: {
|
||||
lookup,
|
||||
},
|
||||
}) as unknown as Dispatcher;
|
||||
return new Dispatcher1Wrapper(
|
||||
new Agent({
|
||||
allowH2: false,
|
||||
connect: {
|
||||
lookup,
|
||||
},
|
||||
}),
|
||||
) as unknown as FetchDispatcher;
|
||||
}
|
||||
|
||||
interface PublicInternetRequestUrlPolicy extends RequestUrlPolicy {
|
||||
readonly dispatcher: Dispatcher;
|
||||
readonly dispatcher: FetchDispatcher;
|
||||
}
|
||||
|
||||
export function createPublicInternetRequestUrlPolicy(
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluxer/constants": "workspace:*",
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:ts7",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface IKVSubscription {
|
||||
}
|
||||
|
||||
export interface KVPurgeBatchResult {
|
||||
urls: Array<string>;
|
||||
entries: Array<string>;
|
||||
tokensConsumed: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -217,7 +217,7 @@ local refillIntervalMs = tonumber(ARGV[5])
|
||||
|
||||
local queueSize = redis.call('SCARD', queueKey)
|
||||
if queueSize == 0 then
|
||||
return '{"urls":[],"tokens":0}'
|
||||
return '{"entries":[],"tokens":0}'
|
||||
end
|
||||
|
||||
local tokens = maxTokens
|
||||
@@ -237,14 +237,14 @@ end
|
||||
local toPop = math.min(maxItems, math.floor(tokens), queueSize)
|
||||
if toPop <= 0 then
|
||||
redis.call('SET', bucketKey, cjson.encode({tokens = tokens, lastRefill = lastRefill}), 'EX', 3600)
|
||||
return '{"urls":[],"tokens":0}'
|
||||
return '{"entries":[],"tokens":0}'
|
||||
end
|
||||
|
||||
local urls = redis.call('SPOP', queueKey, toPop)
|
||||
tokens = tokens - #urls
|
||||
local entries = redis.call('SPOP', queueKey, toPop)
|
||||
tokens = tokens - #entries
|
||||
|
||||
redis.call('SET', bucketKey, cjson.encode({tokens = tokens, lastRefill = lastRefill}), 'EX', 3600)
|
||||
return cjson.encode({urls = urls, tokens = #urls})
|
||||
return cjson.encode({entries = entries, tokens = #entries})
|
||||
`;
|
||||
const CLAIM_BULK_DELETION_SCRIPT = `
|
||||
local score = redis.call('ZSCORE', KEYS[1], ARGV[1])
|
||||
@@ -295,6 +295,7 @@ export class KVClient implements IKVProvider {
|
||||
connectTimeout: this.timeoutMs,
|
||||
commandTimeout: this.timeoutMs,
|
||||
maxRetriesPerRequest: 1,
|
||||
protocol: 2,
|
||||
retryStrategy: createRetryStrategy(),
|
||||
});
|
||||
}
|
||||
@@ -311,6 +312,7 @@ export class KVClient implements IKVProvider {
|
||||
connectTimeout: clusterConfig.timeoutMs,
|
||||
commandTimeout: clusterConfig.timeoutMs,
|
||||
maxRetriesPerRequest: 1,
|
||||
protocol: 2,
|
||||
},
|
||||
scaleReads: 'master',
|
||||
...(hasNatMap ? {natMap} : {}),
|
||||
@@ -902,17 +904,17 @@ function parseRateLimitResult(value: unknown): KVRateLimitResult {
|
||||
function parsePurgeBatchResult(value: unknown, maxItems: number): KVPurgeBatchResult {
|
||||
const command = 'dequeuePurgeBatch';
|
||||
if (!isJsonObject(value)) throw createInvalidResponseError(command, 'a purge batch object');
|
||||
const {urls, tokens} = value;
|
||||
const {entries, tokens} = value;
|
||||
if (
|
||||
!Array.isArray(urls) ||
|
||||
!urls.every((url): url is string => typeof url === 'string') ||
|
||||
!Array.isArray(entries) ||
|
||||
!entries.every((entry): entry is string => typeof entry === 'string') ||
|
||||
!isNonNegativeSafeInteger(tokens) ||
|
||||
tokens !== urls.length ||
|
||||
urls.length > maxItems
|
||||
tokens !== entries.length ||
|
||||
entries.length > maxItems
|
||||
) {
|
||||
throw createInvalidResponseError(command, 'a bounded string array and matching token count');
|
||||
}
|
||||
return {urls, tokensConsumed: tokens};
|
||||
return {entries, tokensConsumed: tokens};
|
||||
}
|
||||
|
||||
function isJsonObject(value: unknown): value is Record<string, unknown> {
|
||||
|
||||
@@ -72,6 +72,7 @@ export class KVSubscription implements IKVSubscription {
|
||||
connectTimeout: this.timeoutMs,
|
||||
commandTimeout: this.timeoutMs,
|
||||
maxRetriesPerRequest: 1,
|
||||
protocol: 2,
|
||||
retryStrategy: createRetryStrategy(),
|
||||
};
|
||||
const connection = this.mode === 'cluster' ? resolveKVClusterConnection(this.url, this.clusterNodes) : null;
|
||||
|
||||
@@ -153,7 +153,7 @@ describe('KVClient script execution', () => {
|
||||
},
|
||||
{
|
||||
name: 'dequeuePurgeBatch',
|
||||
reply: JSON.stringify({urls: ['https://fluxer.test/a.png'], tokens: 1}),
|
||||
reply: JSON.stringify({entries: ['/attachments/1/2/a'], tokens: 1}),
|
||||
keyCount: 2,
|
||||
run: async (client) => client.dequeuePurgeBatch('queue:key', 'bucket:key', 10, 10, 1, 1000),
|
||||
},
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluxer/constants": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:ts7",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
"import": "./src/MediaProxySigner.ts",
|
||||
"types": "./src/MediaProxySigner.ts"
|
||||
},
|
||||
"./src/AttachmentUrlSignature": {
|
||||
"import": "./src/AttachmentUrlSignature.ts",
|
||||
"types": "./src/AttachmentUrlSignature.ts"
|
||||
},
|
||||
"./*": "./*"
|
||||
},
|
||||
"main": "./src/MediaProxyUtils.ts",
|
||||
@@ -31,13 +35,13 @@
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:ts7",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
export const ATTACHMENT_URL_TTL_SECS = 86_400;
|
||||
export const ATTACHMENT_URL_BUCKET_SECS = 43_200;
|
||||
|
||||
export const ORDINARY_USAGE = '';
|
||||
export const DATA_PACKAGE_USAGE = 'dp';
|
||||
|
||||
export type AttachmentUrlUsage = typeof ORDINARY_USAGE | typeof DATA_PACKAGE_USAGE;
|
||||
export type SignatureParameterName = 'ex' | 'is' | 'hm' | 'uc';
|
||||
|
||||
const SIGNATURE_DOMAIN = 'fluxer-attachment-url-v1';
|
||||
const ATTACHMENT_PATH_PREFIX = '/attachments/';
|
||||
const SIGNATURE_PARAMETER_NAMES: ReadonlyArray<SignatureParameterName> = ['ex', 'is', 'hm', 'uc'];
|
||||
const DATA_PACKAGE_EXPIRES = '0';
|
||||
const WINDOW_HEX_LENGTH = 8;
|
||||
const MAX_WINDOW_SECS = 0xff_ff_ff_ff;
|
||||
const LEADING_SLASHES_REGEX = /^\/+/u;
|
||||
const TRAILING_SLASHES_REGEX = /\/+$/u;
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
const strictTextDecoder = new TextDecoder('utf-8', {fatal: true, ignoreBOM: true});
|
||||
|
||||
export interface AttachmentUrlWindow {
|
||||
issued: number;
|
||||
expires: number;
|
||||
}
|
||||
|
||||
export interface SignAttachmentUrlOptions {
|
||||
mediaEndpoint: string;
|
||||
secret: Uint8Array;
|
||||
nowSecs: number;
|
||||
anchorSecs: number;
|
||||
}
|
||||
|
||||
function hexNibble(byte: number): number {
|
||||
if (byte >= 0x30 && byte <= 0x39) return byte - 0x30;
|
||||
if (byte >= 0x41 && byte <= 0x46) return byte - 0x41 + 10;
|
||||
if (byte >= 0x61 && byte <= 0x66) return byte - 0x61 + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
function percentDecodeBytes(value: string, plusAsSpace: boolean): Uint8Array {
|
||||
const bytes = textEncoder.encode(value);
|
||||
const decoded = new Uint8Array(bytes.length);
|
||||
let length = 0;
|
||||
let index = 0;
|
||||
while (index < bytes.length) {
|
||||
const byte = bytes[index] as number;
|
||||
if (byte === 0x25 && index + 2 < bytes.length) {
|
||||
const high = hexNibble(bytes[index + 1] as number);
|
||||
const low = hexNibble(bytes[index + 2] as number);
|
||||
if (high >= 0 && low >= 0) {
|
||||
decoded[length] = (high << 4) | low;
|
||||
length += 1;
|
||||
index += 3;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
decoded[length] = plusAsSpace && byte === 0x2b ? 0x20 : byte;
|
||||
length += 1;
|
||||
index += 1;
|
||||
}
|
||||
return decoded.subarray(0, length);
|
||||
}
|
||||
|
||||
export function percentDecodeStorageKey(path: string): string | null {
|
||||
try {
|
||||
return strictTextDecoder.decode(percentDecodeBytes(path.replace(LEADING_SLASHES_REGEX, ''), false));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function signatureParameterName(name: string): SignatureParameterName | null {
|
||||
const decoded = percentDecodeBytes(name, true);
|
||||
if (decoded.length !== 2) return null;
|
||||
const candidate = String.fromCharCode(decoded[0] as number, decoded[1] as number);
|
||||
return SIGNATURE_PARAMETER_NAMES.find((entry) => entry === candidate) ?? null;
|
||||
}
|
||||
|
||||
export function isSignatureParameterName(name: string): boolean {
|
||||
return signatureParameterName(name) !== null;
|
||||
}
|
||||
|
||||
function isSafeStorageKey(key: string): boolean {
|
||||
if (key.length === 0 || key.startsWith('/')) return false;
|
||||
return key
|
||||
.split('/')
|
||||
.every((component) => component.length > 0 && component !== '.' && component !== '..' && !component.includes('\0'));
|
||||
}
|
||||
|
||||
function firstIndexOf(value: string, characters: ReadonlyArray<string>): number {
|
||||
let found = -1;
|
||||
for (const character of characters) {
|
||||
const index = value.indexOf(character);
|
||||
if (index >= 0 && (found < 0 || index < found)) {
|
||||
found = index;
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
function rawPathFromUrl(url: string): string | null {
|
||||
const schemeIndex = url.indexOf('://');
|
||||
if (schemeIndex < 0) return null;
|
||||
const afterAuthority = url.slice(schemeIndex + 3);
|
||||
const boundary = firstIndexOf(afterAuthority, ['/', '?', '#']);
|
||||
if (boundary < 0 || afterAuthority[boundary] !== '/') return '';
|
||||
const path = afterAuthority.slice(boundary);
|
||||
const queryIndex = firstIndexOf(path, ['?', '#']);
|
||||
return queryIndex < 0 ? path : path.slice(0, queryIndex);
|
||||
}
|
||||
|
||||
function parseWebUrl(value: string): URL | null {
|
||||
try {
|
||||
const parsed = new URL(value);
|
||||
return parsed.protocol === 'http:' || parsed.protocol === 'https:' ? parsed : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function attachmentStorageKeyFromUrl(url: string, mediaEndpoint: string): string | null {
|
||||
const target = parseWebUrl(url);
|
||||
const endpoint = parseWebUrl(mediaEndpoint);
|
||||
if (!target || !endpoint || target.origin !== endpoint.origin) return null;
|
||||
const path = rawPathFromUrl(url);
|
||||
if (path === null) return null;
|
||||
const endpointPath = (rawPathFromUrl(mediaEndpoint) ?? '').replace(TRAILING_SLASHES_REGEX, '');
|
||||
if (!path.startsWith(`${endpointPath}${ATTACHMENT_PATH_PREFIX}`)) return null;
|
||||
const storageKey = percentDecodeStorageKey(path.slice(endpointPath.length));
|
||||
if (storageKey === null || !isSafeStorageKey(storageKey)) return null;
|
||||
return storageKey;
|
||||
}
|
||||
|
||||
interface SplitUrl {
|
||||
base: string;
|
||||
query: string;
|
||||
fragment: string;
|
||||
}
|
||||
|
||||
function splitUrl(url: string): SplitUrl {
|
||||
const fragmentIndex = url.indexOf('#');
|
||||
const head = fragmentIndex < 0 ? url : url.slice(0, fragmentIndex);
|
||||
const fragment = fragmentIndex < 0 ? '' : url.slice(fragmentIndex);
|
||||
const queryIndex = head.indexOf('?');
|
||||
if (queryIndex < 0) return {base: head, query: '', fragment};
|
||||
return {base: head.slice(0, queryIndex), query: head.slice(queryIndex + 1), fragment};
|
||||
}
|
||||
|
||||
function preservedFields(query: string): Array<string> {
|
||||
if (query.length === 0) return [];
|
||||
return query.split('&').filter((field) => {
|
||||
if (field.length === 0 || field === '=') return false;
|
||||
const separator = field.indexOf('=');
|
||||
return !isSignatureParameterName(separator < 0 ? field : field.slice(0, separator));
|
||||
});
|
||||
}
|
||||
|
||||
export function stripAttachmentSignature(url: string): string {
|
||||
const {base, query, fragment} = splitUrl(url);
|
||||
const preserved = preservedFields(query);
|
||||
if (preserved.length === 0) return `${base}${fragment}`;
|
||||
return `${base}?${preserved.join('&')}${fragment}`;
|
||||
}
|
||||
|
||||
export function issueWindow(anchorSecs: number, nowSecs: number): AttachmentUrlWindow {
|
||||
const elapsed = Math.max(0, nowSecs - anchorSecs);
|
||||
const issued = anchorSecs + Math.floor(elapsed / ATTACHMENT_URL_BUCKET_SECS) * ATTACHMENT_URL_BUCKET_SECS;
|
||||
return {issued, expires: issued + ATTACHMENT_URL_TTL_SECS};
|
||||
}
|
||||
|
||||
export function canonicalInput(storageKey: string, exHex: string, isHex: string, usage: AttachmentUrlUsage): string {
|
||||
return `${SIGNATURE_DOMAIN}\n${exHex}\n${isHex}\n${usage}\n${storageKey}`;
|
||||
}
|
||||
|
||||
function windowHex(value: number): string {
|
||||
return value.toString(16).padStart(WINDOW_HEX_LENGTH, '0');
|
||||
}
|
||||
|
||||
function signUsage(url: string, options: SignAttachmentUrlOptions, usage: AttachmentUrlUsage): string {
|
||||
const storageKey = attachmentStorageKeyFromUrl(url, options.mediaEndpoint);
|
||||
if (storageKey === null) return url;
|
||||
const {issued, expires} = issueWindow(options.anchorSecs, options.nowSecs);
|
||||
if (!Number.isSafeInteger(issued) || issued < 0 || expires > MAX_WINDOW_SECS) return url;
|
||||
const isDataPackage = usage === DATA_PACKAGE_USAGE;
|
||||
const exHex = windowHex(isDataPackage ? 0 : expires);
|
||||
const isHex = windowHex(issued);
|
||||
const signature = crypto
|
||||
.createHmac('sha256', options.secret)
|
||||
.update(canonicalInput(storageKey, exHex, isHex, usage))
|
||||
.digest('hex');
|
||||
const signatureFields = isDataPackage
|
||||
? `ex=${DATA_PACKAGE_EXPIRES}&is=${isHex}&hm=${signature}&uc=${DATA_PACKAGE_USAGE}`
|
||||
: `ex=${exHex}&is=${isHex}&hm=${signature}`;
|
||||
const {base, query, fragment} = splitUrl(url);
|
||||
const preserved = preservedFields(query);
|
||||
const fields = preserved.length === 0 ? signatureFields : `${signatureFields}&${preserved.join('&')}`;
|
||||
return `${base}?${fields}${fragment}`;
|
||||
}
|
||||
|
||||
export function signAttachmentUrl(url: string, options: SignAttachmentUrlOptions): string {
|
||||
return signUsage(url, options, ORDINARY_USAGE);
|
||||
}
|
||||
|
||||
export function signDataPackageAttachmentUrl(url: string, options: SignAttachmentUrlOptions): string {
|
||||
return signUsage(url, options, DATA_PACKAGE_USAGE);
|
||||
}
|
||||
@@ -10,13 +10,13 @@
|
||||
"./*": "./*"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"mime": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
"typescript": "catalog:ts7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,14 @@
|
||||
"./*": "./*"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"nats": "catalog:"
|
||||
"@nats-io/jetstream": "catalog:",
|
||||
"@nats-io/transport-node": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
"typescript": "catalog:ts7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {NatsConnection} from 'nats';
|
||||
import type {NatsConnection} from '@nats-io/transport-node';
|
||||
|
||||
export interface INatsConnectionManager {
|
||||
connect(): Promise<void>;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {type JetStreamClient, type JetStreamManager, jetstream, jetstreamManager} from '@nats-io/jetstream';
|
||||
import {NatsConnectionManager} from '@pkgs/nats/src/NatsConnectionManager';
|
||||
import type {JetStreamClient, JetStreamManager} from 'nats';
|
||||
|
||||
export class JetStreamConnectionManager extends NatsConnectionManager {
|
||||
getJetStreamClient(): JetStreamClient {
|
||||
return this.getConnection().jetstream();
|
||||
return jetstream(this.getConnection());
|
||||
}
|
||||
|
||||
async getJetStreamManager(): Promise<JetStreamManager> {
|
||||
return this.getConnection().jetstreamManager();
|
||||
return jetstreamManager(this.getConnection());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {connect, DrainingConnectionError, type NatsConnection} from '@nats-io/transport-node';
|
||||
import type {INatsConnectionManager} from '@pkgs/nats/src/INatsConnectionManager';
|
||||
import type {NatsConnectionOptions} from '@pkgs/nats/src/NatsConnectionOptions';
|
||||
import {connect, ErrorCode, type NatsConnection, NatsError} from 'nats';
|
||||
|
||||
const DEFAULT_MAX_RECONNECT_ATTEMPTS = -1;
|
||||
const DEFAULT_RECONNECT_TIME_WAIT_MS = 500;
|
||||
@@ -46,7 +46,7 @@ export class NatsConnectionManager implements INatsConnectionManager {
|
||||
});
|
||||
await this.connectPromise;
|
||||
if (generation !== this.drainGeneration) {
|
||||
throw NatsError.errorForCode(ErrorCode.ConnectionDraining);
|
||||
throw new DrainingConnectionError();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ export class NatsConnectionManager implements INatsConnectionManager {
|
||||
|
||||
private assertNotDraining(): void {
|
||||
if (this.drainPromise !== null) {
|
||||
throw NatsError.errorForCode(ErrorCode.ConnectionDraining);
|
||||
throw new DrainingConnectionError();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"./*": "./*"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"pg": "catalog:"
|
||||
@@ -15,6 +15,6 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@types/pg": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
"typescript": "catalog:ts7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,16 +7,13 @@
|
||||
"./*": "./*"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pkgs/kv_client": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
"typescript": "catalog:ts7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
interface KVRequiredErrorOptions {
|
||||
serviceName: string;
|
||||
configPath: string;
|
||||
}
|
||||
|
||||
export function throwKVRequiredError(options: KVRequiredErrorOptions): never {
|
||||
const {serviceName, configPath} = options;
|
||||
throw new Error(
|
||||
`${serviceName} requires KV-backed rate limiting. ${configPath} is not set. ` +
|
||||
`internal.kv must be configured for distributed rate limiting.`,
|
||||
);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {throwKVRequiredError} from '@pkgs/rate_limit/src/KVRequiredError';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
describe('throwKVRequiredError', () => {
|
||||
it('should throw an error with the service name', () => {
|
||||
expect(() =>
|
||||
throwKVRequiredError({
|
||||
serviceName: 'fluxer_api',
|
||||
configPath: 'internal.kv.url',
|
||||
}),
|
||||
).toThrow('fluxer_api requires KV-backed rate limiting');
|
||||
});
|
||||
it('should include the config path in the error message', () => {
|
||||
expect(() =>
|
||||
throwKVRequiredError({
|
||||
serviceName: 'TestService',
|
||||
configPath: 'config.kv.connection_string',
|
||||
}),
|
||||
).toThrow('config.kv.connection_string is not set');
|
||||
});
|
||||
it('should construct complete error message with all parts', () => {
|
||||
let errorMessage = '';
|
||||
try {
|
||||
throwKVRequiredError({
|
||||
serviceName: 'fluxer_admin',
|
||||
configPath: 'admin.kv.endpoint',
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
}
|
||||
expect(errorMessage).toContain('fluxer_admin requires KV-backed rate limiting');
|
||||
expect(errorMessage).toContain('admin.kv.endpoint is not set');
|
||||
expect(errorMessage).toContain('internal.kv must be configured for distributed rate limiting');
|
||||
});
|
||||
it('should always throw (never return)', () => {
|
||||
const fn = () =>
|
||||
throwKVRequiredError({
|
||||
serviceName: 'test',
|
||||
configPath: 'test.path',
|
||||
});
|
||||
expect(fn).toThrow(Error);
|
||||
});
|
||||
it('should handle empty service name', () => {
|
||||
expect(() =>
|
||||
throwKVRequiredError({
|
||||
serviceName: '',
|
||||
configPath: 'internal.kv',
|
||||
}),
|
||||
).toThrow('requires KV-backed rate limiting');
|
||||
});
|
||||
it('should handle empty config path', () => {
|
||||
expect(() =>
|
||||
throwKVRequiredError({
|
||||
serviceName: 'TestService',
|
||||
configPath: '',
|
||||
}),
|
||||
).toThrow('is not set');
|
||||
});
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {defineConfig} from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {tsconfigPaths: true},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['**/*.{test,spec}.{ts,tsx}'],
|
||||
exclude: ['node_modules', 'dist'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: ['**/*.test.tsx', '**/*.spec.tsx', 'node_modules/'],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -9,7 +9,7 @@
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluxer/constants": "workspace:*",
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:ts7",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {SmsVerificationUnavailableError} from '@fluxer/errors/src/domains/auth/SmsVerificationUnavailableError';
|
||||
import {createMockLogger} from '@fluxer/logger/src/mock';
|
||||
import {createSmsProvider} from '@pkgs/sms/src/providers/SmsProviderFactory';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
describe('createSmsProvider', () => {
|
||||
it('creates a test provider that accepts the configured code', async () => {
|
||||
const provider = createSmsProvider({
|
||||
mode: 'test',
|
||||
logger: createMockLogger(),
|
||||
verificationCode: '654321',
|
||||
});
|
||||
await expect(provider.startVerification('+15551234567')).resolves.toBeUndefined();
|
||||
await expect(provider.checkVerification('+15551234567', '654321')).resolves.toBe(true);
|
||||
await expect(provider.checkVerification('+15551234567', '123456')).resolves.toBe(false);
|
||||
});
|
||||
it('creates an unavailable provider that throws on verification checks', async () => {
|
||||
const provider = createSmsProvider({
|
||||
mode: 'unavailable',
|
||||
logger: createMockLogger(),
|
||||
});
|
||||
await expect(provider.startVerification('+15551234567')).resolves.toBeUndefined();
|
||||
await expect(provider.checkVerification('+15551234567', '123456')).rejects.toThrow(SmsVerificationUnavailableError);
|
||||
});
|
||||
it('creates a Twilio provider in twilio mode', async () => {
|
||||
const provider = createSmsProvider({
|
||||
mode: 'twilio',
|
||||
config: {
|
||||
accountSid: 'AC123',
|
||||
authToken: 'twilio-secret',
|
||||
verifyServiceSid: 'VA123',
|
||||
},
|
||||
logger: createMockLogger(),
|
||||
fetchFn: async () => new Response(JSON.stringify({status: 'pending'}), {status: 200}),
|
||||
});
|
||||
await expect(provider.startVerification('+15551234567')).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {createMockLogger} from '@fluxer/logger/src/mock';
|
||||
import {TestSmsProvider} from '@pkgs/sms/src/providers/TestSmsProvider';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
describe('TestSmsProvider', () => {
|
||||
describe('startVerification', () => {
|
||||
it('completes without error', async () => {
|
||||
const logger = createMockLogger();
|
||||
const provider = new TestSmsProvider({logger});
|
||||
await expect(provider.startVerification('+15551234567')).resolves.toBeUndefined();
|
||||
});
|
||||
it('supports different phone number formats', async () => {
|
||||
const logger = createMockLogger();
|
||||
const provider = new TestSmsProvider({logger});
|
||||
await expect(provider.startVerification('+14155552671')).resolves.toBeUndefined();
|
||||
await expect(provider.startVerification('+447911123456')).resolves.toBeUndefined();
|
||||
await expect(provider.startVerification('+81312345678')).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
describe('checkVerification', () => {
|
||||
it('returns true for the default valid code', async () => {
|
||||
const logger = createMockLogger();
|
||||
const provider = new TestSmsProvider({logger});
|
||||
await provider.startVerification('+15551234567');
|
||||
const result = await provider.checkVerification('+15551234567', '123456');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
it('returns false for invalid codes', async () => {
|
||||
const logger = createMockLogger();
|
||||
const provider = new TestSmsProvider({logger});
|
||||
await provider.startVerification('+15551234567');
|
||||
expect(await provider.checkVerification('+15551234567', '000000')).toBe(false);
|
||||
expect(await provider.checkVerification('+15551234567', '654321')).toBe(false);
|
||||
expect(await provider.checkVerification('+15551234567', 'abcdef')).toBe(false);
|
||||
expect(await provider.checkVerification('+15551234567', '')).toBe(false);
|
||||
});
|
||||
it('supports custom verification code overrides', async () => {
|
||||
const logger = createMockLogger();
|
||||
const provider = new TestSmsProvider({logger, verificationCode: '654321'});
|
||||
expect(await provider.checkVerification('+15551111111', '123456')).toBe(false);
|
||||
expect(await provider.checkVerification('+15551111111', '654321')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -38,7 +38,7 @@ describe('TwilioSmsProvider', () => {
|
||||
const fetchStub: typeof fetch = async (_input, init) => {
|
||||
capturedRequest = {
|
||||
url: String(_input),
|
||||
authHeader: (init?.headers as Record<string, string>).Authorization,
|
||||
authHeader: (init?.headers as Record<string, string>)?.Authorization,
|
||||
body: init?.body as string,
|
||||
};
|
||||
return new Response(JSON.stringify({success: true}), {status: 200});
|
||||
@@ -65,7 +65,7 @@ describe('TwilioSmsProvider', () => {
|
||||
const fetchStub: typeof fetch = async (_input, init) => {
|
||||
capturedRequest = {
|
||||
url: String(_input),
|
||||
authHeader: (init?.headers as Record<string, string>).Authorization,
|
||||
authHeader: (init?.headers as Record<string, string>)?.Authorization,
|
||||
body: init?.body as string,
|
||||
};
|
||||
return new Response(JSON.stringify({channel: 'auto'}), {status: 200});
|
||||
@@ -232,7 +232,7 @@ describe('TwilioSmsProvider', () => {
|
||||
capturedRequest = {
|
||||
url: String(input),
|
||||
method: init?.method,
|
||||
authHeader: (init?.headers as Record<string, string>).Authorization,
|
||||
authHeader: (init?.headers as Record<string, string>)?.Authorization,
|
||||
};
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"extends": "../../../tsconfigs/package.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node"],
|
||||
"paths": {
|
||||
"@fluxer/*": ["../../../packages/*", "../../../packages/*/src/index.ts"],
|
||||
"@pkgs/*": ["../*"]
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"./*": "./*"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluxer/logger": "workspace:*",
|
||||
@@ -15,6 +15,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
"typescript": "catalog:ts7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"./*": "./*"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluxer/constants": "workspace:*",
|
||||
@@ -60,6 +60,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
"typescript": "catalog:ts7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ export async function createAPIApp(options: CreateAPIAppOptions): Promise<APIApp
|
||||
trustClientIpHeader: config.proxy.trust_client_ip_header,
|
||||
clientIpHeaderName: config.proxy.client_ip_header,
|
||||
maxInflightRequests: config.maxInflightRequests,
|
||||
torExitBlockingEnabled: config.torExitList.enabled,
|
||||
});
|
||||
routes.onError(AbuseAwareAppErrorHandler);
|
||||
routes.notFound(AppNotFoundHandler);
|
||||
|
||||
@@ -166,3 +166,57 @@ describe('buildAPIConfigFromMaster stripe legacy prices', () => {
|
||||
expect(buildAPIConfigFromMaster(withStripeLegacyPrices(master, undefined)).stripe.legacyPrices).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
function withOptionalOutboundLookups(
|
||||
master: MasterConfig,
|
||||
selfHosted: boolean,
|
||||
overrides: {torExitList?: boolean; breachedPasswordCheck?: boolean} = {},
|
||||
): MasterConfig {
|
||||
return {
|
||||
...master,
|
||||
integrations: {
|
||||
...master.integrations,
|
||||
tor_exit_list: {enabled: overrides.torExitList},
|
||||
breached_password_check: {enabled: overrides.breachedPasswordCheck},
|
||||
},
|
||||
instance: {
|
||||
...master.instance,
|
||||
self_hosted: selfHosted,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('buildAPIConfigFromMaster optional outbound lookups', () => {
|
||||
let master: MasterConfig;
|
||||
beforeAll(async () => {
|
||||
master = await loadConfig();
|
||||
});
|
||||
|
||||
it('keeps both lookups on when the instance is not self-hosted', () => {
|
||||
const config = buildAPIConfigFromMaster(withOptionalOutboundLookups(master, false));
|
||||
expect(config.torExitList.enabled).toBe(true);
|
||||
expect(config.breachedPasswordCheck.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('leaves both lookups off on a self-hosted instance', () => {
|
||||
const config = buildAPIConfigFromMaster(withOptionalOutboundLookups(master, true));
|
||||
expect(config.torExitList.enabled).toBe(false);
|
||||
expect(config.breachedPasswordCheck.enabled).toBe(false);
|
||||
});
|
||||
|
||||
it('lets a self-hosted operator switch each lookup on', () => {
|
||||
const config = buildAPIConfigFromMaster(
|
||||
withOptionalOutboundLookups(master, true, {torExitList: true, breachedPasswordCheck: true}),
|
||||
);
|
||||
expect(config.torExitList.enabled).toBe(true);
|
||||
expect(config.breachedPasswordCheck.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('lets an operator switch each lookup off when the instance is not self-hosted', () => {
|
||||
const config = buildAPIConfigFromMaster(
|
||||
withOptionalOutboundLookups(master, false, {torExitList: false, breachedPasswordCheck: false}),
|
||||
);
|
||||
expect(config.torExitList.enabled).toBe(false);
|
||||
expect(config.breachedPasswordCheck.enabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import type {APIConfig, BlueskyOAuthConfig} from '@app/api/config/APIConfig';
|
||||
import type {WorkerTaskName} from '@app/api/worker/WorkerLaneConfig';
|
||||
import type {MasterConfig} from '@fluxer/config/src/MasterConfig';
|
||||
import {resolveDownloadsProvider} from '@fluxer/config/src/S3DownloadsProvider';
|
||||
import {parseIpAddress} from '@fluxer/ip_utils/src/IpAddress';
|
||||
import {parseGeoipSourceConfig, resolveGeoipRuntimeSourceConfig} from '@pkgs/geoip/src/GeoipStartup';
|
||||
|
||||
@@ -92,18 +91,6 @@ function normalizeIpBanExemptIps(values: Array<string>): Array<string> {
|
||||
return Array.from(normalized);
|
||||
}
|
||||
|
||||
function normalizeCountryCodes(values: Array<string>, configName: string): ReadonlySet<string> {
|
||||
const normalized = new Set<string>();
|
||||
for (const value of values) {
|
||||
const countryCode = value.trim().toUpperCase();
|
||||
if (!/^[A-Z]{2}$/u.test(countryCode)) {
|
||||
throw new Error(`${configName} contains an invalid ISO 3166-1 alpha-2 country code: ${value}`);
|
||||
}
|
||||
normalized.add(countryCode);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function mapPushProviderApps(
|
||||
apps:
|
||||
| Array<{
|
||||
@@ -157,7 +144,6 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
|
||||
const s3Buckets = s3Config.buckets ?? {
|
||||
cdn: '',
|
||||
uploads: '',
|
||||
downloads: '',
|
||||
reports: '',
|
||||
harvests: '',
|
||||
};
|
||||
@@ -174,10 +160,6 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
|
||||
requestTimeoutMs: master.services.api.request_timeout_ms,
|
||||
maxInflightRequests: master.services.api.max_inflight_requests,
|
||||
ipBanExemptIps: normalizeIpBanExemptIps(master.services.api.ip_ban_exempt_ips),
|
||||
desktopGitHubRedirectCountries: normalizeCountryCodes(
|
||||
master.services.api.desktop_github_redirect_countries,
|
||||
'FLUXER_API_DESKTOP_GITHUB_REDIRECT_COUNTRIES',
|
||||
),
|
||||
cassandra: {
|
||||
hosts: cassandraSource?.hosts.join(',') ?? '',
|
||||
port: cassandraSource?.port ?? 9042,
|
||||
@@ -237,6 +219,11 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
|
||||
jetStreamUrl: master.services.nats?.jetstream_url ?? 'nats://127.0.0.1:4223',
|
||||
authToken: master.services.nats?.auth_token ?? '',
|
||||
},
|
||||
storageChangeFeed: {
|
||||
enabled: master.services.api.storage_change_feed?.enabled ?? false,
|
||||
stream: master.services.api.storage_change_feed?.stream ?? 'STORAGE_CHANGES',
|
||||
skipBuckets: master.services.api.storage_change_feed?.skip_buckets ?? [s3Buckets.uploads],
|
||||
},
|
||||
search: {
|
||||
engine: master.integrations.search?.engine ?? 'elasticsearch',
|
||||
url: master.integrations.search?.url ?? 'http://127.0.0.1:9200',
|
||||
@@ -258,6 +245,9 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
|
||||
tokenTtlSecs: uploadRelayConfig.token_ttl_secs,
|
||||
keepDirectCountries: uploadRelayConfig.keep_direct_countries,
|
||||
},
|
||||
attachmentUrls: {
|
||||
secretsBase64: master.services.media_proxy.attachment_urls.secrets_base64,
|
||||
},
|
||||
},
|
||||
geoip: geoipSourceConfig,
|
||||
proxy: {
|
||||
@@ -296,7 +286,6 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
|
||||
cacheMinTtlSeconds: master.services.api.embeds.cache_min_ttl_seconds,
|
||||
cacheRespectRemoteTtl: master.services.api.embeds.cache_respect_remote_ttl,
|
||||
},
|
||||
s3Downloads: resolveDownloadsProvider(master),
|
||||
s3: {
|
||||
endpoint: s3Config.endpoint,
|
||||
presignedUrlBase: s3Config.presigned_url_base,
|
||||
@@ -340,6 +329,12 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
|
||||
blocklistFeeds: {
|
||||
enabled: master.integrations.blocklist_feeds.enabled ?? !master.instance.self_hosted,
|
||||
},
|
||||
torExitList: {
|
||||
enabled: master.integrations.tor_exit_list.enabled ?? !master.instance.self_hosted,
|
||||
},
|
||||
breachedPasswordCheck: {
|
||||
enabled: master.integrations.breached_password_check.enabled ?? !master.instance.self_hosted,
|
||||
},
|
||||
captcha: {
|
||||
enabled: master.integrations.captcha.enabled,
|
||||
provider: master.integrations.captcha.provider,
|
||||
@@ -474,6 +469,8 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
|
||||
wordmarkUrl: master.instance.branding.wordmark_url,
|
||||
faviconUrl: master.instance.branding.favicon_url,
|
||||
themeColor: master.instance.branding.theme_color,
|
||||
statusPageUrl: master.instance.branding.status_page_url,
|
||||
statusPageIncidentHistoryUrl: master.instance.branding.status_page_incident_history_url,
|
||||
},
|
||||
setup: {
|
||||
configured: master.instance.setup.configured,
|
||||
@@ -507,7 +504,6 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
|
||||
validateResponses: resolveValidateResponses(master),
|
||||
},
|
||||
presignedAttachmentUploadsEnabled: master.services.api.presigned_attachment_uploads_enabled ?? false,
|
||||
presignedDownloadsEnabled: master.services.api.presigned_downloads_enabled ?? false,
|
||||
presignedHarvestDownloadsEnabled: master.services.api.presigned_harvest_downloads_enabled ?? true,
|
||||
attachmentDecayEnabled: master.attachment_decay_enabled,
|
||||
deletionGracePeriodHours: master.dev.test_mode_enabled ? 0.01 : master.deletion_grace_period_hours,
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {ValueOf} from '@fluxer/constants/src/ValueOf';
|
||||
import type {AdminAuditAccess} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
|
||||
export const AdminAuditReadActions = {
|
||||
CHECK_BLOCKLIST_ENTRY: 'check_blocklist_entry',
|
||||
GET_ADMIN_API_KEY: 'get_admin_api_key',
|
||||
GET_APPLICATION: 'get_application',
|
||||
GET_ARCHIVE: 'get_archive',
|
||||
GET_ARCHIVE_DOWNLOAD_URL: 'get_archive_download_url',
|
||||
GET_AUDIT_LOG: 'get_audit_log',
|
||||
GET_GATEWAY_STATS: 'get_gateway_stats',
|
||||
GET_GUILD: 'get_guild',
|
||||
GET_INSTANCE_CONFIG: 'get_instance_config',
|
||||
GET_LIMIT_CONFIG: 'get_limit_config',
|
||||
GET_MESSAGE: 'get_message',
|
||||
GET_MESSAGE_SHRED_STATUS: 'get_message_shred_status',
|
||||
GET_REPORT: 'get_report',
|
||||
GET_USER: 'get_user',
|
||||
GET_VOICE_REGION: 'get_voice_region',
|
||||
GET_VOICE_SERVER: 'get_voice_server',
|
||||
GET_VOICE_STATE_COUNTS: 'get_voice_state_counts',
|
||||
LIST_ADMIN_ACLS: 'list_admin_acls',
|
||||
LIST_ADMIN_API_KEYS: 'list_admin_api_keys',
|
||||
LIST_ARCHIVES: 'list_archives',
|
||||
LIST_AUDIT_LOGS: 'list_audit_logs',
|
||||
LIST_BLOCKLIST_ENTRIES: 'list_blocklist_entries',
|
||||
LIST_BLOCKLISTS: 'list_blocklists',
|
||||
LIST_CHANNEL_MESSAGES: 'list_channel_messages',
|
||||
LIST_DISCOVERY_APPLICATIONS: 'list_discovery_applications',
|
||||
LIST_DISCOVERY_CATEGORIES: 'list_discovery_categories',
|
||||
LIST_DISCOVERY_CATEGORY_LISTINGS: 'list_discovery_category_listings',
|
||||
LIST_DISCOVERY_LISTINGS: 'list_discovery_listings',
|
||||
LIST_GUILD_APPLICATIONS: 'list_guild_applications',
|
||||
LIST_GUILD_AUDIT_LOGS: 'list_guild_audit_logs',
|
||||
LIST_GUILD_EMOJIS: 'list_guild_emojis',
|
||||
LIST_GUILD_MEMBERS: 'list_guild_members',
|
||||
LIST_GUILD_MEMORY_STATS: 'list_guild_memory_stats',
|
||||
LIST_GUILD_STICKERS: 'list_guild_stickers',
|
||||
LIST_USER_APPLICATIONS: 'list_user_applications',
|
||||
LIST_USER_CHANGE_LOG: 'list_user_change_log',
|
||||
LIST_USER_DM_CHANNELS: 'list_user_dm_channels',
|
||||
LIST_USER_GUILDS: 'list_user_guilds',
|
||||
LIST_USER_RELATIONSHIPS: 'list_user_relationships',
|
||||
LIST_USER_SESSIONS: 'list_user_sessions',
|
||||
LIST_VOICE_REGIONS: 'list_voice_regions',
|
||||
LIST_VOICE_SERVERS: 'list_voice_servers',
|
||||
LIST_WEBAUTHN_CREDENTIALS: 'list_webauthn_credentials',
|
||||
SEARCH_AUDIT_LOGS: 'search_audit_logs',
|
||||
SEARCH_GUILDS: 'search_guilds',
|
||||
SEARCH_MESSAGES: 'search_messages',
|
||||
SEARCH_REPORTS: 'search_reports',
|
||||
SEARCH_USERS: 'search_users',
|
||||
} as const;
|
||||
|
||||
export type AdminAuditReadAction = ValueOf<typeof AdminAuditReadActions>;
|
||||
|
||||
export const ADMIN_AUDIT_READ_ACTIONS: ReadonlyArray<AdminAuditReadAction> = Object.values(AdminAuditReadActions);
|
||||
|
||||
const READ_ACTION_SET: ReadonlySet<string> = new Set(ADMIN_AUDIT_READ_ACTIONS);
|
||||
|
||||
export function getAdminAuditAccess(action: string): AdminAuditAccess {
|
||||
return READ_ACTION_SET.has(action) ? 'read' : 'write';
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {AdminAuditReadAction} from '@app/api/admin/AdminAuditActions';
|
||||
import type {HonoEnv} from '@app/api/types/HonoEnv';
|
||||
import type {Context} from 'hono';
|
||||
|
||||
type AdminAuditMetadataValue = string | number | bigint | boolean | null | undefined;
|
||||
|
||||
type AdminAuditMetadataInput = Readonly<Record<string, AdminAuditMetadataValue>>;
|
||||
|
||||
interface AdminAuditEntryInput<TAction extends string> {
|
||||
targetType: string;
|
||||
targetId: bigint;
|
||||
action: TAction;
|
||||
metadata?: AdminAuditMetadataInput;
|
||||
}
|
||||
|
||||
const SNOWFLAKE_PATTERN = /^(0|[1-9][0-9]{0,19})$/;
|
||||
|
||||
export function snowflakeOrUndefined(value: string | undefined): string | undefined {
|
||||
return value !== undefined && SNOWFLAKE_PATTERN.test(value) ? value : undefined;
|
||||
}
|
||||
|
||||
function toAdminAuditMetadata(input: AdminAuditMetadataInput = {}): Map<string, string> {
|
||||
const metadata = new Map<string, string>();
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
if (value === undefined || value === null) continue;
|
||||
metadata.set(key, String(value));
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
async function recordAdminAuditEntry(ctx: Context<HonoEnv>, entry: AdminAuditEntryInput<string>): Promise<void> {
|
||||
await ctx.get('adminService').auditService.createAuditLog({
|
||||
adminUserId: ctx.get('adminUserId'),
|
||||
targetType: entry.targetType,
|
||||
targetId: entry.targetId,
|
||||
action: entry.action,
|
||||
auditLogReason: ctx.get('auditLogReason'),
|
||||
metadata: toAdminAuditMetadata(entry.metadata),
|
||||
});
|
||||
}
|
||||
|
||||
export async function recordAdminRead(
|
||||
ctx: Context<HonoEnv>,
|
||||
entry: AdminAuditEntryInput<AdminAuditReadAction>,
|
||||
): Promise<void> {
|
||||
await recordAdminAuditEntry(ctx, entry);
|
||||
}
|
||||
|
||||
export async function recordAdminWrite(ctx: Context<HonoEnv>, entry: AdminAuditEntryInput<string>): Promise<void> {
|
||||
await recordAdminAuditEntry(ctx, entry);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {AdminAuditReadActions} from '@app/api/admin/AdminAuditActions';
|
||||
import {recordAdminRead, recordAdminWrite} from '@app/api/admin/AdminAuditRecorder';
|
||||
import type {AdminApiKeyView} from '@app/api/admin/services/AdminApiKeyService';
|
||||
import {requireAdminACL} from '@app/api/middleware/AdminMiddleware';
|
||||
import {RateLimitMiddleware} from '@app/api/middleware/RateLimitMiddleware';
|
||||
@@ -62,6 +64,15 @@ export function AdminApiKeyAdminController(app: HonoApp) {
|
||||
expires_at: result.apiKey.expiresAt?.toISOString() ?? null,
|
||||
acls: Array.from(result.apiKey.acls),
|
||||
};
|
||||
await recordAdminWrite(ctx, {
|
||||
targetType: 'admin_api_key',
|
||||
targetId: BigInt(result.apiKey.keyId),
|
||||
action: 'create_admin_api_key',
|
||||
metadata: {
|
||||
acls: response.acls.join(','),
|
||||
expires_in_days: request.expires_in_days,
|
||||
},
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
@@ -84,6 +95,12 @@ export function AdminApiKeyAdminController(app: HonoApp) {
|
||||
const user = ctx.get('user');
|
||||
const keys = await adminApiKeyService.listKeys(user.id);
|
||||
const response: Array<ListAdminApiKeyResponseType> = keys.map(toApiKeyResponse);
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'admin_api_key',
|
||||
targetId: 0n,
|
||||
action: AdminAuditReadActions.LIST_ADMIN_API_KEYS,
|
||||
metadata: {result_count: response.length},
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
@@ -107,6 +124,11 @@ export function AdminApiKeyAdminController(app: HonoApp) {
|
||||
const user = ctx.get('user');
|
||||
const keyId = ctx.req.valid('param').key_id;
|
||||
const key = await adminApiKeyService.getKey(keyId, user.id);
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'admin_api_key',
|
||||
targetId: keyId,
|
||||
action: AdminAuditReadActions.GET_ADMIN_API_KEY,
|
||||
});
|
||||
return ctx.json(toApiKeyResponse(key));
|
||||
},
|
||||
);
|
||||
@@ -131,8 +153,19 @@ export function AdminApiKeyAdminController(app: HonoApp) {
|
||||
const user = ctx.get('user');
|
||||
const adminUserAcls = ctx.get('adminUserAcls');
|
||||
const keyId = ctx.req.valid('param').key_id;
|
||||
const key = await adminApiKeyService.updateKey(keyId, user.id, ctx.req.valid('json'), adminUserAcls);
|
||||
return ctx.json(toApiKeyResponse(key));
|
||||
const request = ctx.req.valid('json');
|
||||
const key = await adminApiKeyService.updateKey(keyId, user.id, request, adminUserAcls);
|
||||
const response = toApiKeyResponse(key);
|
||||
await recordAdminWrite(ctx, {
|
||||
targetType: 'admin_api_key',
|
||||
targetId: keyId,
|
||||
action: 'update_admin_api_key',
|
||||
metadata: {
|
||||
fields: (['name', 'acls'] as const).filter((field) => request[field] !== undefined).join(','),
|
||||
acls: request.acls !== undefined ? response.acls.join(',') : undefined,
|
||||
},
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
app.delete(
|
||||
@@ -155,6 +188,11 @@ export function AdminApiKeyAdminController(app: HonoApp) {
|
||||
const user = ctx.get('user');
|
||||
const keyId = ctx.req.valid('param').key_id;
|
||||
await adminApiKeyService.revokeKey(keyId, user.id);
|
||||
await recordAdminWrite(ctx, {
|
||||
targetType: 'admin_api_key',
|
||||
targetId: keyId,
|
||||
action: 'revoke_admin_api_key',
|
||||
});
|
||||
return ctx.json({success: true}, 200);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {AdminAuditReadActions} from '@app/api/admin/AdminAuditActions';
|
||||
import {recordAdminRead} from '@app/api/admin/AdminAuditRecorder';
|
||||
import {createApplicationID, createGuildID, createUserID} from '@app/api/BrandedTypes';
|
||||
import {requireAdminACL, requireAnyAdminACL} from '@app/api/middleware/AdminMiddleware';
|
||||
import {RateLimitMiddleware} from '@app/api/middleware/RateLimitMiddleware';
|
||||
@@ -50,11 +52,25 @@ export function ApplicationAdminController(app: HonoApp) {
|
||||
}
|
||||
if (guildId != null) {
|
||||
requireRequestAdminACL(ctx.get('adminUserAcls'), AdminACLs.APPLICATION_LOOKUP);
|
||||
return ctx.json(await adminService.applicationService.listGuildApplications(createGuildID(guildId)));
|
||||
const response = await adminService.applicationService.listGuildApplications(createGuildID(guildId));
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'guild',
|
||||
targetId: guildId,
|
||||
action: AdminAuditReadActions.LIST_GUILD_APPLICATIONS,
|
||||
metadata: {application_count: response.applications.length},
|
||||
});
|
||||
return ctx.json(response);
|
||||
}
|
||||
if (ownerId != null) {
|
||||
requireRequestAdminACL(ctx.get('adminUserAcls'), AdminACLs.APPLICATION_LIST_BY_OWNER);
|
||||
return ctx.json(await adminService.applicationService.listUserApplications(createUserID(ownerId)));
|
||||
const response = await adminService.applicationService.listUserApplications(createUserID(ownerId));
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'user',
|
||||
targetId: ownerId,
|
||||
action: AdminAuditReadActions.LIST_USER_APPLICATIONS,
|
||||
metadata: {application_count: response.applications.length},
|
||||
});
|
||||
return ctx.json(response);
|
||||
}
|
||||
throw InputValidationError.create('owner_id', 'One of owner_id and guild_id is required');
|
||||
},
|
||||
@@ -76,7 +92,14 @@ export function ApplicationAdminController(app: HonoApp) {
|
||||
async (ctx) => {
|
||||
const adminService = ctx.get('adminService');
|
||||
const userId = createUserID(ctx.req.valid('param').user_id);
|
||||
return ctx.json(await adminService.applicationService.listUserApplications(userId));
|
||||
const response = await adminService.applicationService.listUserApplications(userId);
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'user',
|
||||
targetId: userId,
|
||||
action: AdminAuditReadActions.LIST_USER_APPLICATIONS,
|
||||
metadata: {application_count: response.applications.length},
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
app.get(
|
||||
@@ -97,7 +120,18 @@ export function ApplicationAdminController(app: HonoApp) {
|
||||
async (ctx) => {
|
||||
const adminService = ctx.get('adminService');
|
||||
const applicationId = createApplicationID(ctx.req.valid('param').application_id);
|
||||
return ctx.json(await adminService.applicationService.lookupApplication(applicationId));
|
||||
const response = await adminService.applicationService.lookupApplication(applicationId);
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'application',
|
||||
targetId: applicationId,
|
||||
action: AdminAuditReadActions.GET_APPLICATION,
|
||||
metadata: {
|
||||
found: response.application !== null,
|
||||
owner_user_id: response.application?.owner_user_id,
|
||||
bot_user_id: response.application?.bot_user_id,
|
||||
},
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
app.patch(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {AdminAuditReadActions} from '@app/api/admin/AdminAuditActions';
|
||||
import {recordAdminRead, recordAdminWrite} from '@app/api/admin/AdminAuditRecorder';
|
||||
import {createGuildID, createUserID} from '@app/api/BrandedTypes';
|
||||
import {requireAdminACL, requireAnyAdminACL} from '@app/api/middleware/AdminMiddleware';
|
||||
import {RateLimitMiddleware} from '@app/api/middleware/RateLimitMiddleware';
|
||||
@@ -66,11 +68,15 @@ export function ArchiveAdminController(app: HonoApp) {
|
||||
async (ctx) => {
|
||||
const adminArchiveService = ctx.get('adminArchiveService');
|
||||
const adminUserId = ctx.get('adminUserId');
|
||||
const result = await adminArchiveService.triggerUserArchive(
|
||||
createUserID(ctx.req.valid('param').user_id),
|
||||
adminUserId,
|
||||
ctx.req.valid('json').include_attachments,
|
||||
);
|
||||
const userId = createUserID(ctx.req.valid('param').user_id);
|
||||
const includeAttachments = ctx.req.valid('json').include_attachments;
|
||||
const result = await adminArchiveService.triggerUserArchive(userId, adminUserId, includeAttachments);
|
||||
await recordAdminWrite(ctx, {
|
||||
targetType: 'user',
|
||||
targetId: userId,
|
||||
action: 'trigger_user_archive',
|
||||
metadata: {archive_id: result.archive_id, include_attachments: includeAttachments},
|
||||
});
|
||||
return ctx.json(result, 200);
|
||||
},
|
||||
);
|
||||
@@ -93,11 +99,15 @@ export function ArchiveAdminController(app: HonoApp) {
|
||||
async (ctx) => {
|
||||
const adminArchiveService = ctx.get('adminArchiveService');
|
||||
const adminUserId = ctx.get('adminUserId');
|
||||
const result = await adminArchiveService.triggerGuildArchive(
|
||||
createGuildID(ctx.req.valid('param').guild_id),
|
||||
adminUserId,
|
||||
ctx.req.valid('json').include_attachments,
|
||||
);
|
||||
const guildId = createGuildID(ctx.req.valid('param').guild_id);
|
||||
const includeAttachments = ctx.req.valid('json').include_attachments;
|
||||
const result = await adminArchiveService.triggerGuildArchive(guildId, adminUserId, includeAttachments);
|
||||
await recordAdminWrite(ctx, {
|
||||
targetType: 'guild',
|
||||
targetId: guildId,
|
||||
action: 'trigger_guild_archive',
|
||||
metadata: {archive_id: result.archive_id, include_attachments: includeAttachments},
|
||||
});
|
||||
return ctx.json(result, 200);
|
||||
},
|
||||
);
|
||||
@@ -120,13 +130,28 @@ export function ArchiveAdminController(app: HonoApp) {
|
||||
const adminArchiveService = ctx.get('adminArchiveService');
|
||||
const adminAcls = ctx.get('adminUserAcls');
|
||||
const query = ctx.req.valid('query');
|
||||
const subjectType = resolveListSubjectType(adminAcls, query.subject_type);
|
||||
const result = await adminArchiveService.listArchives({
|
||||
subjectType: resolveListSubjectType(adminAcls, query.subject_type),
|
||||
subjectType,
|
||||
subjectId: query.subject_id ?? undefined,
|
||||
requestedBy: query.requested_by ?? undefined,
|
||||
limit: query.limit,
|
||||
includeExpired: query.include_expired,
|
||||
});
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'archive',
|
||||
targetId: 0n,
|
||||
action: AdminAuditReadActions.LIST_ARCHIVES,
|
||||
metadata: {
|
||||
subject_type: subjectType,
|
||||
subject_user_id: subjectType === 'user' ? query.subject_id : undefined,
|
||||
subject_guild_id: subjectType === 'guild' ? query.subject_id : undefined,
|
||||
requested_by_user_id: query.requested_by,
|
||||
limit: query.limit,
|
||||
include_expired: query.include_expired,
|
||||
result_count: result.length,
|
||||
},
|
||||
});
|
||||
return ctx.json({archives: result}, 200);
|
||||
},
|
||||
);
|
||||
@@ -151,6 +176,12 @@ export function ArchiveAdminController(app: HonoApp) {
|
||||
const params = ctx.req.valid('param');
|
||||
requireArchiveSubjectAccess(adminAcls, params.subject_type);
|
||||
const archive = await adminArchiveService.getArchive(params.subject_type, params.subject_id, params.archive_id);
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: params.subject_type,
|
||||
targetId: params.subject_id,
|
||||
action: AdminAuditReadActions.GET_ARCHIVE,
|
||||
metadata: {archive_id: params.archive_id, found: archive !== null},
|
||||
});
|
||||
return ctx.json({archive}, 200);
|
||||
},
|
||||
);
|
||||
@@ -179,6 +210,12 @@ export function ArchiveAdminController(app: HonoApp) {
|
||||
params.subject_id,
|
||||
params.archive_id,
|
||||
);
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: params.subject_type,
|
||||
targetId: params.subject_id,
|
||||
action: AdminAuditReadActions.GET_ARCHIVE_DOWNLOAD_URL,
|
||||
metadata: {archive_id: params.archive_id},
|
||||
});
|
||||
return ctx.json(result, 200);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {AdminAuditReadActions} from '@app/api/admin/AdminAuditActions';
|
||||
import {recordAdminRead, snowflakeOrUndefined} from '@app/api/admin/AdminAuditRecorder';
|
||||
import {requireAdminACL} from '@app/api/middleware/AdminMiddleware';
|
||||
import {RateLimitMiddleware} from '@app/api/middleware/RateLimitMiddleware';
|
||||
import {OpenAPI} from '@app/api/middleware/ResponseTypeMiddleware';
|
||||
@@ -16,6 +18,8 @@ import {
|
||||
ListAdminAuditLogsQuery,
|
||||
} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
|
||||
const AUDIT_TARGET_TYPE_PATTERN = /^[a-z][a-z_]{0,63}$/;
|
||||
|
||||
export function AuditLogAdminController(app: HonoApp) {
|
||||
app.get(
|
||||
'/admin/audit-logs',
|
||||
@@ -34,24 +38,50 @@ export function AuditLogAdminController(app: HonoApp) {
|
||||
}),
|
||||
async (ctx) => {
|
||||
const adminService = ctx.get('adminService');
|
||||
const {q, admin_user_id, target_type, target_id, sort_by, sort_order, limit, offset} = ctx.req.valid('query');
|
||||
if (q === undefined) {
|
||||
return ctx.json(
|
||||
await adminService.auditService.listAuditLogs({admin_user_id, target_type, target_id, limit, offset}),
|
||||
);
|
||||
}
|
||||
return ctx.json(
|
||||
await adminService.auditService.searchAuditLogs({
|
||||
query: q,
|
||||
admin_user_id,
|
||||
target_type,
|
||||
target_id,
|
||||
sort_by,
|
||||
sort_order,
|
||||
const {q, admin_user_id, target_type, target_id, access, sort_by, sort_order, limit, offset} =
|
||||
ctx.req.valid('query');
|
||||
const response =
|
||||
q === undefined
|
||||
? await adminService.auditService.listAuditLogs({
|
||||
admin_user_id,
|
||||
target_type,
|
||||
target_id,
|
||||
access,
|
||||
limit,
|
||||
offset,
|
||||
})
|
||||
: await adminService.auditService.searchAuditLogs({
|
||||
query: q,
|
||||
admin_user_id,
|
||||
target_type,
|
||||
target_id,
|
||||
access,
|
||||
sort_by,
|
||||
sort_order,
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'audit_log',
|
||||
targetId: 0n,
|
||||
action: q === undefined ? AdminAuditReadActions.LIST_AUDIT_LOGS : AdminAuditReadActions.SEARCH_AUDIT_LOGS,
|
||||
metadata: {
|
||||
filter_admin_user_id: admin_user_id,
|
||||
filter_target_type:
|
||||
target_type !== undefined && AUDIT_TARGET_TYPE_PATTERN.test(target_type) ? target_type : undefined,
|
||||
has_target_type_filter:
|
||||
target_type !== undefined && !AUDIT_TARGET_TYPE_PATTERN.test(target_type) ? true : undefined,
|
||||
filter_target_id: snowflakeOrUndefined(target_id),
|
||||
access,
|
||||
sort_by: q === undefined ? undefined : sort_by,
|
||||
sort_order: q === undefined ? undefined : sort_order,
|
||||
limit,
|
||||
offset,
|
||||
}),
|
||||
);
|
||||
result_count: response.logs.length,
|
||||
total: response.total,
|
||||
},
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
app.get(
|
||||
@@ -76,6 +106,11 @@ export function AuditLogAdminController(app: HonoApp) {
|
||||
if (!log) {
|
||||
throw new NotFoundError({code: APIErrorCodes.NOT_FOUND});
|
||||
}
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'audit_log',
|
||||
targetId: log_id,
|
||||
action: AdminAuditReadActions.GET_AUDIT_LOG,
|
||||
});
|
||||
return ctx.json(log);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {AdminAuditReadActions} from '@app/api/admin/AdminAuditActions';
|
||||
import {recordAdminRead, recordAdminWrite} from '@app/api/admin/AdminAuditRecorder';
|
||||
import type {AdminBanManagementService} from '@app/api/admin/services/AdminBanManagementService';
|
||||
import {requireAdminACL, requireAnyAdminACL} from '@app/api/middleware/AdminMiddleware';
|
||||
import {RateLimitMiddleware} from '@app/api/middleware/RateLimitMiddleware';
|
||||
import {OpenAPI} from '@app/api/middleware/ResponseTypeMiddleware';
|
||||
@@ -180,6 +183,18 @@ const BLOCKLIST_TYPE_ACLS: Record<AdminBlocklistListType, {add: string; check: s
|
||||
},
|
||||
};
|
||||
|
||||
const BLOCKLIST_AUDIT_TARGET_TYPES: Record<AdminBlocklistListType, string> = {
|
||||
ip: 'ip',
|
||||
email: 'email',
|
||||
'email-domain-suspicious': 'email_domain',
|
||||
phrase: 'phrase',
|
||||
url: 'url',
|
||||
'url-domain': 'url_domain',
|
||||
'file-sha': 'file_sha',
|
||||
'avatar-hash': 'avatar_hash',
|
||||
'profile-substring': 'profile_substring',
|
||||
};
|
||||
|
||||
type BlocklistVerb = 'add' | 'check' | 'remove';
|
||||
|
||||
const BLOCKLIST_ACLS_BY_VERB: Record<BlocklistVerb, Array<string>> = {
|
||||
@@ -229,6 +244,37 @@ async function parseBlocklistBody<T>(schema: ZodType<T>, value: unknown): Promis
|
||||
return result.data;
|
||||
}
|
||||
|
||||
async function checkBlocklistEntry(
|
||||
bans: AdminBanManagementService,
|
||||
listType: AdminBlocklistListType,
|
||||
entryValue: string,
|
||||
scope: ProfileSubstringScope | undefined,
|
||||
): Promise<{banned: boolean}> {
|
||||
switch (listType) {
|
||||
case 'ip':
|
||||
return bans.checkIpBan({ip: entryValue});
|
||||
case 'email':
|
||||
return bans.checkEmailBan({email: entryValue});
|
||||
case 'email-domain-suspicious':
|
||||
return bans.checkSuspiciousEmailDomain({domain: entryValue});
|
||||
case 'phrase':
|
||||
return bans.checkPhraseBan({phrase: entryValue});
|
||||
case 'url':
|
||||
return bans.checkUrlBan({url: entryValue});
|
||||
case 'url-domain':
|
||||
return bans.checkUrlDomainBan({domain: entryValue});
|
||||
case 'file-sha':
|
||||
return bans.checkFileShaBan({sha256_hex: entryValue});
|
||||
case 'avatar-hash':
|
||||
return bans.checkAvatarHashBan({hashes: [entryValue]});
|
||||
case 'profile-substring':
|
||||
return bans.checkProfileSubstringBan({
|
||||
scope: requireProfileSubstringScope(scope),
|
||||
substrings: [entryValue],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function BanAdminController(app: HonoApp) {
|
||||
app.get(
|
||||
'/admin/blocklists',
|
||||
@@ -245,6 +291,12 @@ export function BanAdminController(app: HonoApp) {
|
||||
'List every blocklist this instance maintains, the request field that carries an entry value, the extra fields its entries accept, and which of the bulk and update operations it supports.',
|
||||
}),
|
||||
async (ctx) => {
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'blocklist',
|
||||
targetId: 0n,
|
||||
action: AdminAuditReadActions.LIST_BLOCKLISTS,
|
||||
metadata: {result_count: BLOCKLIST_CATALOG.length},
|
||||
});
|
||||
return ctx.json({items: BLOCKLIST_CATALOG});
|
||||
},
|
||||
);
|
||||
@@ -270,14 +322,26 @@ export function BanAdminController(app: HonoApp) {
|
||||
requireBlocklistACL(ctx.get('adminUserAcls'), listType, 'check');
|
||||
const {limit, after, scope} = ctx.req.valid('query');
|
||||
assertBlocklistScopeAllowed(listType, scope);
|
||||
return ctx.json(
|
||||
await adminService.banManagementService.listBlocklistEntries({
|
||||
listType,
|
||||
const page = await adminService.banManagementService.listBlocklistEntries({
|
||||
listType,
|
||||
limit,
|
||||
after: after ?? null,
|
||||
scope: listType === 'profile-substring' ? requireProfileSubstringScope(scope) : null,
|
||||
});
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: BLOCKLIST_AUDIT_TARGET_TYPES[listType],
|
||||
targetId: 0n,
|
||||
action: AdminAuditReadActions.LIST_BLOCKLIST_ENTRIES,
|
||||
metadata: {
|
||||
list_type: listType,
|
||||
scope,
|
||||
limit,
|
||||
after: after ?? null,
|
||||
scope: listType === 'profile-substring' ? requireProfileSubstringScope(scope) : null,
|
||||
}),
|
||||
);
|
||||
has_after: after === undefined ? undefined : true,
|
||||
result_count: page.items.length,
|
||||
has_more: page.has_more,
|
||||
},
|
||||
});
|
||||
return ctx.json(page);
|
||||
},
|
||||
);
|
||||
app.post(
|
||||
@@ -379,6 +443,15 @@ export function BanAdminController(app: HonoApp) {
|
||||
},
|
||||
{requestedByUserId: adminUserId, requireLedger: true, ...(auditLogReason && {auditLogReason})},
|
||||
);
|
||||
await recordAdminWrite(ctx, {
|
||||
targetType: 'bulk_job',
|
||||
targetId: jobId,
|
||||
action: 'queue_bulk_job',
|
||||
metadata: {
|
||||
task: 'ban_file_shas',
|
||||
entity_count: body.sha256_list.length,
|
||||
},
|
||||
});
|
||||
return ctx.json({job_id: jobId.toString()});
|
||||
},
|
||||
);
|
||||
@@ -449,32 +522,18 @@ export function BanAdminController(app: HonoApp) {
|
||||
requireBlocklistACL(ctx.get('adminUserAcls'), listType, 'check');
|
||||
const {scope} = ctx.req.valid('query');
|
||||
assertBlocklistScopeAllowed(listType, scope);
|
||||
const bans = adminService.banManagementService;
|
||||
switch (listType) {
|
||||
case 'ip':
|
||||
return ctx.json(await bans.checkIpBan({ip: entryValue}));
|
||||
case 'email':
|
||||
return ctx.json(await bans.checkEmailBan({email: entryValue}));
|
||||
case 'email-domain-suspicious':
|
||||
return ctx.json(await bans.checkSuspiciousEmailDomain({domain: entryValue}));
|
||||
case 'phrase':
|
||||
return ctx.json(await bans.checkPhraseBan({phrase: entryValue}));
|
||||
case 'url':
|
||||
return ctx.json(await bans.checkUrlBan({url: entryValue}));
|
||||
case 'url-domain':
|
||||
return ctx.json(await bans.checkUrlDomainBan({domain: entryValue}));
|
||||
case 'file-sha':
|
||||
return ctx.json(await bans.checkFileShaBan({sha256_hex: entryValue}));
|
||||
case 'avatar-hash':
|
||||
return ctx.json(await bans.checkAvatarHashBan({hashes: [entryValue]}));
|
||||
case 'profile-substring':
|
||||
return ctx.json(
|
||||
await bans.checkProfileSubstringBan({
|
||||
scope: requireProfileSubstringScope(scope),
|
||||
substrings: [entryValue],
|
||||
}),
|
||||
);
|
||||
}
|
||||
const result = await checkBlocklistEntry(adminService.banManagementService, listType, entryValue, scope);
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: BLOCKLIST_AUDIT_TARGET_TYPES[listType],
|
||||
targetId: 0n,
|
||||
action: AdminAuditReadActions.CHECK_BLOCKLIST_ENTRY,
|
||||
metadata: {
|
||||
list_type: listType,
|
||||
scope,
|
||||
banned: result.banned,
|
||||
},
|
||||
});
|
||||
return ctx.json(result);
|
||||
},
|
||||
);
|
||||
app.patch(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {recordAdminWrite} from '@app/api/admin/AdminAuditRecorder';
|
||||
import type {UserID} from '@app/api/BrandedTypes';
|
||||
import {requireAnyAdminACL} from '@app/api/middleware/AdminMiddleware';
|
||||
import {RateLimitMiddleware} from '@app/api/middleware/RateLimitMiddleware';
|
||||
@@ -136,6 +137,16 @@ export function BulkAdminController(app: HonoApp) {
|
||||
throw new MissingACLError(requiredAcl);
|
||||
}
|
||||
const jobId = await queueBulkJob(body, adminUserId, auditLogReason);
|
||||
await recordAdminWrite(ctx, {
|
||||
targetType: 'bulk_job',
|
||||
targetId: jobId,
|
||||
action: 'queue_bulk_job',
|
||||
metadata: {
|
||||
task: body.task,
|
||||
entity_count: 'guild_ids' in body ? body.guild_ids.length : body.user_ids.length,
|
||||
guild_id: body.task === AdminBulkTaskType.ADD_GUILD_MEMBERS ? body.guild_id : undefined,
|
||||
},
|
||||
});
|
||||
return ctx.json({job_id: jobId.toString()});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {AdminAuditReadActions} from '@app/api/admin/AdminAuditActions';
|
||||
import {recordAdminRead, recordAdminWrite} from '@app/api/admin/AdminAuditRecorder';
|
||||
import {createGuildID} from '@app/api/BrandedTypes';
|
||||
import type {GuildDiscoveryRow} from '@app/api/database/types/GuildDiscoveryTypes';
|
||||
import {mapGuildFeatures} from '@app/api/guild/GuildFeatureUtils';
|
||||
@@ -30,6 +32,8 @@ import {
|
||||
DiscoveryCategoryListResponse,
|
||||
} from '@fluxer/schema/src/domains/guild/GuildDiscoverySchemas';
|
||||
|
||||
const DISCOVERY_LISTING_FIELDS = ['description', 'category_type', 'primary_language', 'custom_tags'] as const;
|
||||
|
||||
function mapRowToApplicationResponse(row: GuildDiscoveryRow) {
|
||||
return {
|
||||
guild_id: row.guild_id.toString(),
|
||||
@@ -149,6 +153,12 @@ export function DiscoveryAdminController(app: HonoApp) {
|
||||
const userRepository = ctx.get('userRepository');
|
||||
const rows = await discoveryService.listByStatus({status: DiscoveryApplicationStatus.PENDING});
|
||||
const enrichment = await enrichGuilds(rows, guildService, userRepository);
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'guild',
|
||||
targetId: 0n,
|
||||
action: AdminAuditReadActions.LIST_DISCOVERY_APPLICATIONS,
|
||||
metadata: {result_count: rows.length},
|
||||
});
|
||||
return ctx.json(rows.map((row) => mapPendingResponse(row, enrichment.get(row.guild_id.toString()))));
|
||||
},
|
||||
);
|
||||
@@ -173,10 +183,16 @@ export function DiscoveryAdminController(app: HonoApp) {
|
||||
const data = ctx.req.valid('json');
|
||||
const adminUserId = ctx.get('adminUserId');
|
||||
const discoveryService = ctx.get('discoveryService');
|
||||
const row =
|
||||
data.status === DiscoveryApplicationStatus.APPROVED
|
||||
? await discoveryService.approve({guildId, adminUserId, reason: data.reason})
|
||||
: await discoveryService.reject({guildId, adminUserId, reason: data.reason});
|
||||
const approved = data.status === DiscoveryApplicationStatus.APPROVED;
|
||||
const row = approved
|
||||
? await discoveryService.approve({guildId, adminUserId, reason: data.reason})
|
||||
: await discoveryService.reject({guildId, adminUserId, reason: data.reason});
|
||||
await recordAdminWrite(ctx, {
|
||||
targetType: 'guild',
|
||||
targetId: guildId,
|
||||
action: approved ? 'approve_discovery_application' : 'reject_discovery_application',
|
||||
metadata: {status: data.status},
|
||||
});
|
||||
return ctx.json(mapRowToApplicationResponse(row));
|
||||
},
|
||||
);
|
||||
@@ -195,12 +211,17 @@ export function DiscoveryAdminController(app: HonoApp) {
|
||||
tags: 'Admin',
|
||||
}),
|
||||
async (ctx) => {
|
||||
return ctx.json(
|
||||
Object.entries(DiscoveryCategoryLabels).map(([id, name]) => ({
|
||||
id: Number(id),
|
||||
name,
|
||||
})),
|
||||
);
|
||||
const categories = Object.entries(DiscoveryCategoryLabels).map(([id, name]) => ({
|
||||
id: Number(id),
|
||||
name,
|
||||
}));
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'discovery_category',
|
||||
targetId: 0n,
|
||||
action: AdminAuditReadActions.LIST_DISCOVERY_CATEGORIES,
|
||||
metadata: {result_count: categories.length},
|
||||
});
|
||||
return ctx.json(categories);
|
||||
},
|
||||
);
|
||||
app.get(
|
||||
@@ -233,11 +254,20 @@ export function DiscoveryAdminController(app: HonoApp) {
|
||||
(enrichment.get(right.guild_id.toString())?.member_count ?? 0) -
|
||||
(enrichment.get(left.guild_id.toString())?.member_count ?? 0),
|
||||
);
|
||||
return ctx.json(
|
||||
sorted
|
||||
.slice(offset, offset + limit)
|
||||
.map((row) => mapListedResponse(row, enrichment.get(row.guild_id.toString()))),
|
||||
);
|
||||
const page = sorted.slice(offset, offset + limit);
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'discovery_category',
|
||||
targetId: 0n,
|
||||
action: AdminAuditReadActions.LIST_DISCOVERY_CATEGORY_LISTINGS,
|
||||
metadata: {
|
||||
category_id,
|
||||
limit,
|
||||
offset,
|
||||
result_count: page.length,
|
||||
total: inCategory.length,
|
||||
},
|
||||
});
|
||||
return ctx.json(page.map((row) => mapListedResponse(row, enrichment.get(row.guild_id.toString()))));
|
||||
},
|
||||
);
|
||||
app.get(
|
||||
@@ -260,6 +290,12 @@ export function DiscoveryAdminController(app: HonoApp) {
|
||||
const userRepository = ctx.get('userRepository');
|
||||
const rows = await discoveryService.listByStatus({status: DiscoveryApplicationStatus.APPROVED});
|
||||
const enrichment = await enrichGuilds(rows, guildService, userRepository);
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'guild',
|
||||
targetId: 0n,
|
||||
action: AdminAuditReadActions.LIST_DISCOVERY_LISTINGS,
|
||||
metadata: {result_count: rows.length},
|
||||
});
|
||||
return ctx.json(rows.map((row) => mapListedResponse(row, enrichment.get(row.guild_id.toString()))));
|
||||
},
|
||||
);
|
||||
@@ -341,6 +377,18 @@ export function DiscoveryAdminController(app: HonoApp) {
|
||||
const adminUserId = ctx.get('adminUserId');
|
||||
const discoveryService = ctx.get('discoveryService');
|
||||
const row = await discoveryService.editApplication({guildId, userId: adminUserId, data});
|
||||
const fields = DISCOVERY_LISTING_FIELDS.filter((field) => data[field] !== undefined);
|
||||
await recordAdminWrite(ctx, {
|
||||
targetType: 'guild',
|
||||
targetId: guildId,
|
||||
action: 'update_discovery_listing',
|
||||
metadata: {
|
||||
fields: fields.length > 0 ? fields.join(',') : undefined,
|
||||
category_type: data.category_type,
|
||||
primary_language: data.primary_language,
|
||||
status: row.status,
|
||||
},
|
||||
});
|
||||
return ctx.json(mapRowToApplicationResponse(row));
|
||||
},
|
||||
);
|
||||
@@ -366,6 +414,11 @@ export function DiscoveryAdminController(app: HonoApp) {
|
||||
const adminUserId = ctx.get('adminUserId');
|
||||
const discoveryService = ctx.get('discoveryService');
|
||||
const row = await discoveryService.remove({guildId, adminUserId, reason: data.reason});
|
||||
await recordAdminWrite(ctx, {
|
||||
targetType: 'guild',
|
||||
targetId: guildId,
|
||||
action: 'remove_discovery_listing',
|
||||
});
|
||||
return ctx.json(mapRowToApplicationResponse(row));
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {AdminAuditReadActions} from '@app/api/admin/AdminAuditActions';
|
||||
import {recordAdminRead} from '@app/api/admin/AdminAuditRecorder';
|
||||
import {createGuildID} from '@app/api/BrandedTypes';
|
||||
import {requireAdminACL} from '@app/api/middleware/AdminMiddleware';
|
||||
import {RateLimitMiddleware} from '@app/api/middleware/RateLimitMiddleware';
|
||||
@@ -34,7 +36,14 @@ export function GatewayAdminController(app: HonoApp) {
|
||||
}),
|
||||
async (ctx) => {
|
||||
const adminService = ctx.get('adminService');
|
||||
return ctx.json(await adminService.guildServiceAggregate.managementService.getNodeStats());
|
||||
const stats = await adminService.guildServiceAggregate.managementService.getNodeStats();
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'gateway',
|
||||
targetId: 0n,
|
||||
action: AdminAuditReadActions.GET_GATEWAY_STATS,
|
||||
metadata: {node_count: stats.node_count},
|
||||
});
|
||||
return ctx.json(stats);
|
||||
},
|
||||
);
|
||||
app.get(
|
||||
@@ -54,7 +63,14 @@ export function GatewayAdminController(app: HonoApp) {
|
||||
async (ctx) => {
|
||||
const adminService = ctx.get('adminService');
|
||||
const {limit} = ctx.req.valid('query');
|
||||
return ctx.json(await adminService.guildServiceAggregate.managementService.getGuildMemoryStats(limit));
|
||||
const stats = await adminService.guildServiceAggregate.managementService.getGuildMemoryStats(limit);
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'guild',
|
||||
targetId: 0n,
|
||||
action: AdminAuditReadActions.LIST_GUILD_MEMORY_STATS,
|
||||
metadata: {limit, result_count: stats.guilds.length},
|
||||
});
|
||||
return ctx.json(stats);
|
||||
},
|
||||
);
|
||||
app.get(
|
||||
@@ -73,7 +89,18 @@ export function GatewayAdminController(app: HonoApp) {
|
||||
}),
|
||||
async (ctx) => {
|
||||
const adminService = ctx.get('adminService');
|
||||
return ctx.json(await adminService.guildServiceAggregate.managementService.getVoiceStateCounts());
|
||||
const counts = await adminService.guildServiceAggregate.managementService.getVoiceStateCounts();
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'gateway',
|
||||
targetId: 0n,
|
||||
action: AdminAuditReadActions.GET_VOICE_STATE_COUNTS,
|
||||
metadata: {
|
||||
total_voice_states: counts.total_voice_states,
|
||||
region_count: counts.regions.length,
|
||||
server_count: counts.servers.length,
|
||||
},
|
||||
});
|
||||
return ctx.json(counts);
|
||||
},
|
||||
);
|
||||
app.post(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {AdminAuditReadActions} from '@app/api/admin/AdminAuditActions';
|
||||
import {recordAdminRead, recordAdminWrite} from '@app/api/admin/AdminAuditRecorder';
|
||||
import {createGuildID} from '@app/api/BrandedTypes';
|
||||
import {requireAdminACL, requireAnyAdminACL} from '@app/api/middleware/AdminMiddleware';
|
||||
import {RateLimitMiddleware} from '@app/api/middleware/RateLimitMiddleware';
|
||||
@@ -107,13 +109,24 @@ export function GuildAdminController(app: HonoApp) {
|
||||
async (ctx) => {
|
||||
const adminService = ctx.get('adminService');
|
||||
const query = ctx.req.valid('query');
|
||||
return ctx.json(
|
||||
await adminService.searchService.searchGuilds({
|
||||
query: query.q,
|
||||
const response = await adminService.searchService.searchGuilds({
|
||||
query: query.q,
|
||||
limit: query.limit,
|
||||
offset: query.offset,
|
||||
});
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'guild',
|
||||
targetId: 0n,
|
||||
action: AdminAuditReadActions.SEARCH_GUILDS,
|
||||
metadata: {
|
||||
has_query: query.q === undefined ? undefined : true,
|
||||
limit: query.limit,
|
||||
offset: query.offset,
|
||||
}),
|
||||
);
|
||||
result_count: response.guilds.length,
|
||||
total: response.total,
|
||||
},
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
app.get(
|
||||
@@ -133,11 +146,15 @@ export function GuildAdminController(app: HonoApp) {
|
||||
}),
|
||||
async (ctx) => {
|
||||
const adminService = ctx.get('adminService');
|
||||
return ctx.json(
|
||||
await adminService.guildServiceAggregate.lookupService.lookupGuild({
|
||||
guild_id: ctx.req.valid('param').guild_id,
|
||||
}),
|
||||
);
|
||||
const {guild_id} = ctx.req.valid('param');
|
||||
const response = await adminService.guildServiceAggregate.lookupService.lookupGuild({guild_id});
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'guild',
|
||||
targetId: guild_id,
|
||||
action: AdminAuditReadActions.GET_GUILD,
|
||||
metadata: {found: response.guild !== null},
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
app.patch(
|
||||
@@ -215,6 +232,16 @@ export function GuildAdminController(app: HonoApp) {
|
||||
if (!guild) {
|
||||
throw new UnknownGuildError();
|
||||
}
|
||||
const appliedFieldGroup =
|
||||
body.fields !== undefined ||
|
||||
hasGuildSettingsUpdate(body) ||
|
||||
hasGuildFeatureUpdate(body) ||
|
||||
body.name !== undefined ||
|
||||
body.vanity_url_code !== undefined ||
|
||||
body.new_owner_id !== undefined;
|
||||
if (!appliedFieldGroup) {
|
||||
await recordAdminWrite(ctx, {targetType: 'guild', targetId: guildIdRaw, action: 'update_guild'});
|
||||
}
|
||||
return ctx.json({
|
||||
guild: {
|
||||
id: guild.id,
|
||||
@@ -275,14 +302,25 @@ export function GuildAdminController(app: HonoApp) {
|
||||
}),
|
||||
async (ctx) => {
|
||||
const adminService = ctx.get('adminService');
|
||||
const {guild_id} = ctx.req.valid('param');
|
||||
const query = ctx.req.valid('query');
|
||||
return ctx.json(
|
||||
await adminService.guildServiceAggregate.lookupService.listGuildMembers({
|
||||
guild_id: ctx.req.valid('param').guild_id,
|
||||
limit: query.limit,
|
||||
offset: query.offset,
|
||||
}),
|
||||
);
|
||||
const response = await adminService.guildServiceAggregate.lookupService.listGuildMembers({
|
||||
guild_id,
|
||||
limit: query.limit,
|
||||
offset: query.offset,
|
||||
});
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'guild',
|
||||
targetId: guild_id,
|
||||
action: AdminAuditReadActions.LIST_GUILD_MEMBERS,
|
||||
metadata: {
|
||||
limit: response.limit,
|
||||
offset: response.offset,
|
||||
result_count: response.members.length,
|
||||
total: response.total,
|
||||
},
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
app.put(
|
||||
@@ -391,7 +429,14 @@ export function GuildAdminController(app: HonoApp) {
|
||||
async (ctx) => {
|
||||
const adminService = ctx.get('adminService');
|
||||
const guildId = createGuildID(ctx.req.valid('param').guild_id);
|
||||
return ctx.json(await adminService.guildServiceAggregate.lookupService.listGuildEmojis(guildId));
|
||||
const response = await adminService.guildServiceAggregate.lookupService.listGuildEmojis(guildId);
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'guild',
|
||||
targetId: guildId,
|
||||
action: AdminAuditReadActions.LIST_GUILD_EMOJIS,
|
||||
metadata: {result_count: response.emojis.length},
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
app.get(
|
||||
@@ -412,7 +457,14 @@ export function GuildAdminController(app: HonoApp) {
|
||||
async (ctx) => {
|
||||
const adminService = ctx.get('adminService');
|
||||
const guildId = createGuildID(ctx.req.valid('param').guild_id);
|
||||
return ctx.json(await adminService.guildServiceAggregate.lookupService.listGuildStickers(guildId));
|
||||
const response = await adminService.guildServiceAggregate.lookupService.listGuildStickers(guildId);
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'guild',
|
||||
targetId: guildId,
|
||||
action: AdminAuditReadActions.LIST_GUILD_STICKERS,
|
||||
metadata: {result_count: response.stickers.length},
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
app.get(
|
||||
@@ -433,17 +485,30 @@ export function GuildAdminController(app: HonoApp) {
|
||||
}),
|
||||
async (ctx) => {
|
||||
const adminService = ctx.get('adminService');
|
||||
const {guild_id} = ctx.req.valid('param');
|
||||
const query = ctx.req.valid('query');
|
||||
return ctx.json(
|
||||
await adminService.guildServiceAggregate.listGuildAuditLogs({
|
||||
guild_id: ctx.req.valid('param').guild_id,
|
||||
const response = await adminService.guildServiceAggregate.listGuildAuditLogs({
|
||||
guild_id,
|
||||
limit: query.limit,
|
||||
before: query.before,
|
||||
after: query.after,
|
||||
user_id: query.user_id,
|
||||
action_type: query.action_type,
|
||||
});
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'guild',
|
||||
targetId: guild_id,
|
||||
action: AdminAuditReadActions.LIST_GUILD_AUDIT_LOGS,
|
||||
metadata: {
|
||||
limit: query.limit,
|
||||
before: query.before,
|
||||
after: query.after,
|
||||
user_id: query.user_id,
|
||||
filter_user_id: query.user_id,
|
||||
action_type: query.action_type,
|
||||
}),
|
||||
);
|
||||
result_count: response.audit_log_entries.length,
|
||||
},
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
app.post(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {AdminAuditReadActions} from '@app/api/admin/AdminAuditActions';
|
||||
import {recordAdminRead, recordAdminWrite} from '@app/api/admin/AdminAuditRecorder';
|
||||
import {createUserID} from '@app/api/BrandedTypes';
|
||||
import {Config} from '@app/api/Config';
|
||||
import {
|
||||
@@ -29,16 +31,10 @@ import {
|
||||
RegistrationUrlIdParam,
|
||||
} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
import {GatewayRolloutConfigSchema} from '@fluxer/schema/src/domains/admin/GatewayRolloutSchemas';
|
||||
import {ScreenShareDeliveryConfigSchema} from '@fluxer/schema/src/domains/admin/ScreenShareDeliverySchemas';
|
||||
import {VoiceNoiseSuppressionConfigSchema} from '@fluxer/schema/src/domains/admin/VoiceNoiseSuppressionSchemas';
|
||||
import {UserIdParam} from '@fluxer/schema/src/domains/common/CommonParamSchemas';
|
||||
import {BlockedMessageGroupsConfigSchema} from '@fluxer/schema/src/domains/experiment/BlockedMessageGroupsSchemas';
|
||||
import {ExperimentDeliveryConfigSchema} from '@fluxer/schema/src/domains/experiment/ExperimentSchemas';
|
||||
import {ExpressionInfoCardConfigSchema} from '@fluxer/schema/src/domains/experiment/ExpressionInfoCardSchemas';
|
||||
import {GuildActivityLogPresentationConfigSchema} from '@fluxer/schema/src/domains/experiment/GuildActivityLogPresentationSchemas';
|
||||
import {GuildHeaderCollapseConfigSchema} from '@fluxer/schema/src/domains/experiment/GuildHeaderCollapseSchemas';
|
||||
import {MessageHoverTrackingConfigSchema} from '@fluxer/schema/src/domains/experiment/MessageHoverTrackingSchemas';
|
||||
import {MessageKeyboardFocusConfigSchema} from '@fluxer/schema/src/domains/experiment/MessageKeyboardFocusSchemas';
|
||||
import {TypingIndicatorReworkConfigSchema} from '@fluxer/schema/src/domains/experiment/TypingIndicatorReworkSchemas';
|
||||
import type {InstanceBranding} from '@fluxer/schema/src/domains/instance/InstanceSchemas';
|
||||
import {SmtpEmailProvider} from '@pkgs/email/src/SmtpEmailProvider';
|
||||
import type {Context} from 'hono';
|
||||
@@ -64,14 +60,8 @@ async function buildInstanceConfigResponse(): Promise<InstanceConfigResponse> {
|
||||
ssoConfig,
|
||||
gatewayRollout,
|
||||
voiceNoiseSuppression,
|
||||
guildActivityLogPresentation,
|
||||
screenShareDelivery,
|
||||
experimentDelivery,
|
||||
messageHoverTracking,
|
||||
messageKeyboardFocus,
|
||||
blockedMessageGroups,
|
||||
expressionInfoCard,
|
||||
guildHeaderCollapse,
|
||||
typingIndicatorRework,
|
||||
registrationConfig,
|
||||
registrationUrls,
|
||||
pendingRegistrations,
|
||||
@@ -79,14 +69,8 @@ async function buildInstanceConfigResponse(): Promise<InstanceConfigResponse> {
|
||||
instanceConfigRepository.getSsoConfig(),
|
||||
instanceConfigRepository.getGatewayRolloutConfig(),
|
||||
instanceConfigRepository.getVoiceNoiseSuppressionConfig(),
|
||||
instanceConfigRepository.getGuildActivityLogPresentationConfig(),
|
||||
instanceConfigRepository.getScreenShareDeliveryConfig(),
|
||||
instanceConfigRepository.getExperimentDeliveryConfig(),
|
||||
instanceConfigRepository.getMessageHoverTrackingConfig(),
|
||||
instanceConfigRepository.getMessageKeyboardFocusConfig(),
|
||||
instanceConfigRepository.getBlockedMessageGroupsConfig(),
|
||||
instanceConfigRepository.getExpressionInfoCardConfig(),
|
||||
instanceConfigRepository.getGuildHeaderCollapseConfig(),
|
||||
instanceConfigRepository.getTypingIndicatorReworkConfig(),
|
||||
instanceConfigRepository.getRegistrationConfig(),
|
||||
instanceConfigRepository.getRegistrationUrlsForAdmin(),
|
||||
instanceConfigRepository.getPendingRegistrations(),
|
||||
@@ -117,14 +101,8 @@ async function buildInstanceConfigResponse(): Promise<InstanceConfigResponse> {
|
||||
},
|
||||
gateway_rollout: gatewayRollout,
|
||||
voice_noise_suppression: voiceNoiseSuppression,
|
||||
guild_activity_log_presentation: guildActivityLogPresentation,
|
||||
screen_share_delivery: screenShareDelivery,
|
||||
experiment_delivery: experimentDelivery,
|
||||
message_hover_tracking: messageHoverTracking,
|
||||
message_keyboard_focus: messageKeyboardFocus,
|
||||
blocked_message_groups: blockedMessageGroups,
|
||||
expression_info_card: expressionInfoCard,
|
||||
guild_header_collapse: guildHeaderCollapse,
|
||||
typing_indicator_rework: typingIndicatorRework,
|
||||
registration: {
|
||||
...registrationConfig,
|
||||
urls: registrationUrls,
|
||||
@@ -195,16 +173,25 @@ function completesInitialSetup(data: InstanceConfigUpdateRequest, setupConfigure
|
||||
);
|
||||
}
|
||||
|
||||
async function grantSetupCompleterAdminACL(ctx: Context<HonoEnv>): Promise<void> {
|
||||
async function grantSetupCompleterAdminACL(ctx: Context<HonoEnv>): Promise<boolean> {
|
||||
const user = ctx.get('user');
|
||||
if (!user || ctx.get('authTokenType') !== 'session' || hasAdminAuthenticationACL(user.acls)) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
const nextACLs = new Set(user.acls);
|
||||
nextACLs.add(AdminACLs.WILDCARD);
|
||||
const updatedUser = await ctx.get('userRepository').patchUpsert(user.id, {acls: nextACLs}, user.toRow());
|
||||
ctx.set('user', updatedUser);
|
||||
ctx.set('adminUserAcls', updatedUser.acls);
|
||||
return true;
|
||||
}
|
||||
|
||||
function listSuppliedSections(data: InstanceConfigUpdateRequest): string | undefined {
|
||||
const sections = Object.entries(data)
|
||||
.filter(([, value]) => value != null)
|
||||
.map(([key]) => key)
|
||||
.sort();
|
||||
return sections.length > 0 ? sections.join(',') : undefined;
|
||||
}
|
||||
|
||||
export function InstanceConfigAdminController(app: HonoApp) {
|
||||
@@ -224,7 +211,17 @@ export function InstanceConfigAdminController(app: HonoApp) {
|
||||
tags: 'Admin',
|
||||
}),
|
||||
async (ctx) => {
|
||||
return ctx.json(await buildInstanceConfigResponse());
|
||||
const response = await buildInstanceConfigResponse();
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'instance_config',
|
||||
targetId: 0n,
|
||||
action: AdminAuditReadActions.GET_INSTANCE_CONFIG,
|
||||
metadata: {
|
||||
registration_url_count: response.registration.urls.length,
|
||||
pending_registration_count: response.registration.pending_registrations.length,
|
||||
},
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
app.patch(
|
||||
@@ -268,89 +265,16 @@ export function InstanceConfigAdminController(app: HonoApp) {
|
||||
await instanceConfigRepository.setVoiceNoiseSuppressionConfig(validated);
|
||||
}
|
||||
}
|
||||
if (data.guild_activity_log_presentation) {
|
||||
const patch = omitUndefinedFields(data.guild_activity_log_presentation);
|
||||
if (data.screen_share_delivery) {
|
||||
const patch = omitUndefinedFields(data.screen_share_delivery);
|
||||
if (Object.keys(patch).length > 0) {
|
||||
const currentGuildActivityLogPresentation =
|
||||
await instanceConfigRepository.getGuildActivityLogPresentationConfig();
|
||||
const validated = GuildActivityLogPresentationConfigSchema.parse({
|
||||
...currentGuildActivityLogPresentation,
|
||||
const currentScreenShareDelivery = await instanceConfigRepository.getScreenShareDeliveryConfig();
|
||||
const validated = ScreenShareDeliveryConfigSchema.parse({
|
||||
...currentScreenShareDelivery,
|
||||
...patch,
|
||||
config_version: currentGuildActivityLogPresentation.config_version + 1,
|
||||
config_version: currentScreenShareDelivery.config_version + 1,
|
||||
});
|
||||
await instanceConfigRepository.setGuildActivityLogPresentationConfig(validated);
|
||||
}
|
||||
}
|
||||
if (data.message_hover_tracking) {
|
||||
const patch = omitUndefinedFields(data.message_hover_tracking);
|
||||
if (Object.keys(patch).length > 0) {
|
||||
const currentMessageHoverTracking = await instanceConfigRepository.getMessageHoverTrackingConfig();
|
||||
const validated = MessageHoverTrackingConfigSchema.parse({
|
||||
...currentMessageHoverTracking,
|
||||
...patch,
|
||||
config_version: currentMessageHoverTracking.config_version + 1,
|
||||
});
|
||||
await instanceConfigRepository.setMessageHoverTrackingConfig(validated);
|
||||
}
|
||||
}
|
||||
if (data.message_keyboard_focus) {
|
||||
const patch = omitUndefinedFields(data.message_keyboard_focus);
|
||||
if (Object.keys(patch).length > 0) {
|
||||
const currentMessageKeyboardFocus = await instanceConfigRepository.getMessageKeyboardFocusConfig();
|
||||
const validated = MessageKeyboardFocusConfigSchema.parse({
|
||||
...currentMessageKeyboardFocus,
|
||||
...patch,
|
||||
config_version: currentMessageKeyboardFocus.config_version + 1,
|
||||
});
|
||||
await instanceConfigRepository.setMessageKeyboardFocusConfig(validated);
|
||||
}
|
||||
}
|
||||
if (data.blocked_message_groups) {
|
||||
const patch = omitUndefinedFields(data.blocked_message_groups);
|
||||
if (Object.keys(patch).length > 0) {
|
||||
const currentBlockedMessageGroups = await instanceConfigRepository.getBlockedMessageGroupsConfig();
|
||||
const validated = BlockedMessageGroupsConfigSchema.parse({
|
||||
...currentBlockedMessageGroups,
|
||||
...patch,
|
||||
config_version: currentBlockedMessageGroups.config_version + 1,
|
||||
});
|
||||
await instanceConfigRepository.setBlockedMessageGroupsConfig(validated);
|
||||
}
|
||||
}
|
||||
if (data.expression_info_card) {
|
||||
const patch = omitUndefinedFields(data.expression_info_card);
|
||||
if (Object.keys(patch).length > 0) {
|
||||
const currentExpressionInfoCard = await instanceConfigRepository.getExpressionInfoCardConfig();
|
||||
const validated = ExpressionInfoCardConfigSchema.parse({
|
||||
...currentExpressionInfoCard,
|
||||
...patch,
|
||||
config_version: currentExpressionInfoCard.config_version + 1,
|
||||
});
|
||||
await instanceConfigRepository.setExpressionInfoCardConfig(validated);
|
||||
}
|
||||
}
|
||||
if (data.guild_header_collapse) {
|
||||
const patch = omitUndefinedFields(data.guild_header_collapse);
|
||||
if (Object.keys(patch).length > 0) {
|
||||
const currentGuildHeaderCollapse = await instanceConfigRepository.getGuildHeaderCollapseConfig();
|
||||
const validated = GuildHeaderCollapseConfigSchema.parse({
|
||||
...currentGuildHeaderCollapse,
|
||||
...patch,
|
||||
config_version: currentGuildHeaderCollapse.config_version + 1,
|
||||
});
|
||||
await instanceConfigRepository.setGuildHeaderCollapseConfig(validated);
|
||||
}
|
||||
}
|
||||
if (data.typing_indicator_rework) {
|
||||
const patch = omitUndefinedFields(data.typing_indicator_rework);
|
||||
if (Object.keys(patch).length > 0) {
|
||||
const currentTypingIndicatorRework = await instanceConfigRepository.getTypingIndicatorReworkConfig();
|
||||
const validated = TypingIndicatorReworkConfigSchema.parse({
|
||||
...currentTypingIndicatorRework,
|
||||
...patch,
|
||||
config_version: currentTypingIndicatorRework.config_version + 1,
|
||||
});
|
||||
await instanceConfigRepository.setTypingIndicatorReworkConfig(validated);
|
||||
await instanceConfigRepository.setScreenShareDeliveryConfig(validated);
|
||||
}
|
||||
}
|
||||
if (data.experiment_delivery) {
|
||||
@@ -418,6 +342,11 @@ export function InstanceConfigAdminController(app: HonoApp) {
|
||||
wordmark_url: readOptionalField(data.app_public.branding, 'wordmark_url'),
|
||||
favicon_url: readOptionalField(data.app_public.branding, 'favicon_url'),
|
||||
theme_color: readOptionalField(data.app_public.branding, 'theme_color'),
|
||||
status_page_url: readOptionalField(data.app_public.branding, 'status_page_url'),
|
||||
status_page_incident_history_url: readOptionalField(
|
||||
data.app_public.branding,
|
||||
'status_page_incident_history_url',
|
||||
),
|
||||
})
|
||||
: undefined,
|
||||
legal: data.app_public.legal
|
||||
@@ -519,10 +448,20 @@ export function InstanceConfigAdminController(app: HonoApp) {
|
||||
}),
|
||||
});
|
||||
}
|
||||
let grantedSetupCompleterAdmin = false;
|
||||
if (shouldGrantSetupCompleterAdmin) {
|
||||
await grantSetupCompleterAdminACL(ctx);
|
||||
grantedSetupCompleterAdmin = await grantSetupCompleterAdminACL(ctx);
|
||||
await instanceConfigRepository.markAdminBootstrapped();
|
||||
}
|
||||
await recordAdminWrite(ctx, {
|
||||
targetType: 'instance_config',
|
||||
targetId: 0n,
|
||||
action: 'update_instance_config',
|
||||
metadata: {
|
||||
sections: listSuppliedSections(data),
|
||||
granted_acls: grantedSetupCompleterAdmin ? AdminACLs.WILDCARD : undefined,
|
||||
},
|
||||
});
|
||||
return ctx.json(await buildInstanceConfigResponse());
|
||||
},
|
||||
);
|
||||
@@ -553,6 +492,12 @@ export function InstanceConfigAdminController(app: HonoApp) {
|
||||
});
|
||||
const brandingPatch: Partial<InstanceBranding> = {[`${kind}_url`]: prepared.newCdnUrl};
|
||||
await instanceConfigRepository.setAppPublicConfig({branding: brandingPatch});
|
||||
await recordAdminWrite(ctx, {
|
||||
targetType: 'instance_config',
|
||||
targetId: 0n,
|
||||
action: 'upload_branding_asset',
|
||||
metadata: {kind, cleared: prepared.newCdnUrl === null},
|
||||
});
|
||||
return ctx.json(await buildInstanceConfigResponse());
|
||||
},
|
||||
);
|
||||
@@ -573,6 +518,7 @@ export function InstanceConfigAdminController(app: HonoApp) {
|
||||
}),
|
||||
async (ctx) => {
|
||||
const data = ctx.req.valid('json');
|
||||
let result: InstanceEmailSmtpTestResponse;
|
||||
try {
|
||||
const provider = new SmtpEmailProvider({
|
||||
host: data.host,
|
||||
@@ -585,10 +531,17 @@ export function InstanceConfigAdminController(app: HonoApp) {
|
||||
socketTimeoutMs: 10000,
|
||||
});
|
||||
await provider.verify();
|
||||
return ctx.json({ok: true, error: null});
|
||||
result = {ok: true, error: null};
|
||||
} catch (error) {
|
||||
return ctx.json({ok: false, error: error instanceof Error ? error.message : String(error)});
|
||||
result = {ok: false, error: error instanceof Error ? error.message : String(error)};
|
||||
}
|
||||
await recordAdminWrite(ctx, {
|
||||
targetType: 'instance_config',
|
||||
targetId: 0n,
|
||||
action: 'test_smtp_connection',
|
||||
metadata: {port: data.port, secure: data.secure, ok: result.ok},
|
||||
});
|
||||
return ctx.json(result);
|
||||
},
|
||||
);
|
||||
app.post(
|
||||
@@ -615,6 +568,12 @@ export function InstanceConfigAdminController(app: HonoApp) {
|
||||
maxUses: data.max_uses ?? null,
|
||||
approvalRequired: data.approval_required,
|
||||
});
|
||||
await recordAdminWrite(ctx, {
|
||||
targetType: 'registration_url',
|
||||
targetId: 0n,
|
||||
action: 'create_registration_url',
|
||||
metadata: {approval_required: data.approval_required, max_uses: data.max_uses},
|
||||
});
|
||||
return ctx.json({
|
||||
registration_url: created.registrationUrl,
|
||||
code: created.code,
|
||||
@@ -639,6 +598,11 @@ export function InstanceConfigAdminController(app: HonoApp) {
|
||||
}),
|
||||
async (ctx) => {
|
||||
await instanceConfigRepository.revokeRegistrationUrl(ctx.req.valid('param').registration_url_id);
|
||||
await recordAdminWrite(ctx, {
|
||||
targetType: 'registration_url',
|
||||
targetId: 0n,
|
||||
action: 'revoke_registration_url',
|
||||
});
|
||||
return ctx.json(await buildInstanceConfigResponse());
|
||||
},
|
||||
);
|
||||
@@ -755,6 +719,12 @@ async function updatePendingRegistrationUser(
|
||||
const userRepository = ctx.get('userRepository');
|
||||
const user = await userRepository.findUnique(createUserID(BigInt(userId)));
|
||||
if (!user) {
|
||||
await recordAdminWrite(ctx, {
|
||||
targetType: 'user',
|
||||
targetId: BigInt(userId),
|
||||
action: decision === 'approve' ? 'approve_registration' : 'reject_registration',
|
||||
metadata: {account_found: false},
|
||||
});
|
||||
return;
|
||||
}
|
||||
const traits = new Set(user.traits);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {recordAdminWrite} from '@app/api/admin/AdminAuditRecorder';
|
||||
import {requireAdminACL} from '@app/api/middleware/AdminMiddleware';
|
||||
import {RateLimitMiddleware} from '@app/api/middleware/RateLimitMiddleware';
|
||||
import {OpenAPI} from '@app/api/middleware/ResponseTypeMiddleware';
|
||||
@@ -114,7 +115,15 @@ export function JobsAdminController(app: HonoApp) {
|
||||
}),
|
||||
async (ctx) => {
|
||||
const adminService = ctx.get('adminService');
|
||||
return ctx.json(await adminService.jobAdminService.cancelJob(ctx.req.valid('param').job_id));
|
||||
const {job_id} = ctx.req.valid('param');
|
||||
const result = await adminService.jobAdminService.cancelJob(job_id);
|
||||
await recordAdminWrite(ctx, {
|
||||
targetType: 'bulk_job',
|
||||
targetId: job_id,
|
||||
action: 'cancel_job',
|
||||
metadata: {cancelled: result.cancelled},
|
||||
});
|
||||
return ctx.json(result);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {AdminAuditReadActions} from '@app/api/admin/AdminAuditActions';
|
||||
import {recordAdminRead, recordAdminWrite} from '@app/api/admin/AdminAuditRecorder';
|
||||
import {Config} from '@app/api/Config';
|
||||
import type {LimitConfigService} from '@app/api/limits/LimitConfigService';
|
||||
import {requireAdminACL} from '@app/api/middleware/AdminMiddleware';
|
||||
@@ -10,7 +12,7 @@ import type {HonoApp} from '@app/api/types/HonoEnv';
|
||||
import {Validator} from '@app/api/Validator';
|
||||
import {AdminACLs} from '@fluxer/constants/src/AdminACLs';
|
||||
import {LIMIT_CATEGORY_LABELS, LIMIT_KEY_METADATA, LIMIT_KEYS} from '@fluxer/constants/src/LimitConfigMetadata';
|
||||
import type {LimitConfigSnapshot} from '@fluxer/limits/src/LimitTypes';
|
||||
import type {LimitConfigSnapshot, LimitRule} from '@fluxer/limits/src/LimitTypes';
|
||||
import {LimitConfigGetResponse, LimitConfigUpdateRequest} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
|
||||
function formatConfig(service: LimitConfigService) {
|
||||
@@ -27,6 +29,21 @@ function formatConfig(service: LimitConfigService) {
|
||||
};
|
||||
}
|
||||
|
||||
function describeLimitRule(rule: LimitRule): string {
|
||||
return JSON.stringify([
|
||||
rule.filters?.traits ?? [],
|
||||
rule.filters?.guildFeatures ?? [],
|
||||
LIMIT_KEYS.map((key) => rule.limits[key] ?? null),
|
||||
]);
|
||||
}
|
||||
|
||||
function countChangedLimitRules(before: LimitConfigSnapshot, after: LimitConfigSnapshot): number {
|
||||
const previous = new Map(before.rules.map((rule) => [rule.id, describeLimitRule(rule)]));
|
||||
const next = new Map(after.rules.map((rule) => [rule.id, describeLimitRule(rule)]));
|
||||
const ruleIds = new Set([...previous.keys(), ...next.keys()]);
|
||||
return Array.from(ruleIds).filter((ruleId) => previous.get(ruleId) !== next.get(ruleId)).length;
|
||||
}
|
||||
|
||||
export function LimitConfigAdminController(app: HonoApp) {
|
||||
app.get(
|
||||
'/admin/limit-config',
|
||||
@@ -44,7 +61,14 @@ export function LimitConfigAdminController(app: HonoApp) {
|
||||
}),
|
||||
async (ctx) => {
|
||||
const limitConfigService = ctx.get('limitConfigService') as LimitConfigService;
|
||||
return ctx.json(formatConfig(limitConfigService));
|
||||
const response = formatConfig(limitConfigService);
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'limit_config',
|
||||
targetId: 0n,
|
||||
action: AdminAuditReadActions.GET_LIMIT_CONFIG,
|
||||
metadata: {rule_count: response.limit_config.rules.length},
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
app.put(
|
||||
@@ -69,8 +93,20 @@ export function LimitConfigAdminController(app: HonoApp) {
|
||||
...data.limit_config,
|
||||
traitDefinitions: data.limit_config.traitDefinitions ?? [],
|
||||
};
|
||||
const previous = limitConfigService.getConfigSnapshot();
|
||||
await limitConfigService.updateConfig(normalized);
|
||||
return ctx.json(formatConfig(limitConfigService));
|
||||
const response = formatConfig(limitConfigService);
|
||||
await recordAdminWrite(ctx, {
|
||||
targetType: 'limit_config',
|
||||
targetId: 0n,
|
||||
action: 'update_limit_config',
|
||||
metadata: {
|
||||
rule_count: response.limit_config.rules.length,
|
||||
changed_rule_count: countChangedLimitRules(previous, response.limit_config),
|
||||
trait_definition_count: response.limit_config.traitDefinitions.length,
|
||||
},
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {AdminAuditReadActions} from '@app/api/admin/AdminAuditActions';
|
||||
import {recordAdminRead} from '@app/api/admin/AdminAuditRecorder';
|
||||
import {createAttachmentID, createChannelID, createMessageID, createReportID} from '@app/api/BrandedTypes';
|
||||
import {requireAdminACL} from '@app/api/middleware/AdminMiddleware';
|
||||
import {RateLimitMiddleware} from '@app/api/middleware/RateLimitMiddleware';
|
||||
@@ -57,34 +59,69 @@ export function MessageAdminController(app: HonoApp) {
|
||||
const adminService = ctx.get('adminService');
|
||||
const query = ctx.req.valid('query');
|
||||
if (query.message_id != null) {
|
||||
return ctx.json(
|
||||
await adminService.messageService.lookupMessage({
|
||||
const messageId = query.message_id;
|
||||
const response = await adminService.messageService.lookupMessage({
|
||||
channel_id: query.channel_id,
|
||||
message_id: messageId,
|
||||
context_limit: query.context_limit,
|
||||
});
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'message',
|
||||
targetId: messageId,
|
||||
action: AdminAuditReadActions.SEARCH_MESSAGES,
|
||||
metadata: {
|
||||
mode: 'message',
|
||||
channel_id: query.channel_id,
|
||||
message_id: query.message_id,
|
||||
context_limit: query.context_limit,
|
||||
}),
|
||||
);
|
||||
found: response.messages.some((message) => message.id === messageId.toString()),
|
||||
result_count: response.messages.length,
|
||||
},
|
||||
});
|
||||
return ctx.json(response);
|
||||
}
|
||||
if (query.attachment_id != null) {
|
||||
if (query.filename == null) {
|
||||
throw InputValidationError.fromCode('filename', ValidationErrorCodes.INVALID_FORMAT);
|
||||
}
|
||||
return ctx.json(
|
||||
await adminService.messageService.lookupMessageByAttachment({
|
||||
channel_id: query.channel_id,
|
||||
attachment_id: query.attachment_id,
|
||||
filename: query.filename,
|
||||
context_limit: query.context_limit,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return ctx.json(
|
||||
await adminService.messageService.searchChannelMessages({
|
||||
const response = await adminService.messageService.lookupMessageByAttachment({
|
||||
channel_id: query.channel_id,
|
||||
query: query.q ?? '',
|
||||
attachment_id: query.attachment_id,
|
||||
filename: query.filename,
|
||||
context_limit: query.context_limit,
|
||||
});
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'channel',
|
||||
targetId: query.channel_id,
|
||||
action: AdminAuditReadActions.SEARCH_MESSAGES,
|
||||
metadata: {
|
||||
mode: 'attachment',
|
||||
attachment_id: query.attachment_id,
|
||||
message_id: response.message_id,
|
||||
context_limit: query.context_limit,
|
||||
found: response.message_id !== null,
|
||||
result_count: response.messages.length,
|
||||
},
|
||||
});
|
||||
return ctx.json(response);
|
||||
}
|
||||
const response = await adminService.messageService.searchChannelMessages({
|
||||
channel_id: query.channel_id,
|
||||
query: query.q ?? '',
|
||||
limit: query.limit,
|
||||
});
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'channel',
|
||||
targetId: query.channel_id,
|
||||
action: AdminAuditReadActions.SEARCH_MESSAGES,
|
||||
metadata: {
|
||||
mode: 'search',
|
||||
has_query: query.q === undefined ? undefined : true,
|
||||
limit: query.limit,
|
||||
}),
|
||||
);
|
||||
result_count: response.messages.length,
|
||||
total: response.total,
|
||||
},
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
app.post(
|
||||
@@ -139,7 +176,14 @@ export function MessageAdminController(app: HonoApp) {
|
||||
async (ctx) => {
|
||||
const adminService = ctx.get('adminService');
|
||||
const {job_id} = ctx.req.valid('param');
|
||||
return ctx.json(await adminService.messageShredService.getMessageShredStatus(job_id.toString()));
|
||||
const response = await adminService.messageShredService.getMessageShredStatus(job_id.toString());
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'message_shred',
|
||||
targetId: 0n,
|
||||
action: AdminAuditReadActions.GET_MESSAGE_SHRED_STATUS,
|
||||
metadata: {job_id, status: response.status},
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
app.get(
|
||||
@@ -162,14 +206,25 @@ export function MessageAdminController(app: HonoApp) {
|
||||
const adminService = ctx.get('adminService');
|
||||
const {channel_id} = ctx.req.valid('param');
|
||||
const {limit, before, after} = ctx.req.valid('query');
|
||||
return ctx.json(
|
||||
await adminService.messageService.browseChannel({
|
||||
channel_id,
|
||||
const response = await adminService.messageService.browseChannel({
|
||||
channel_id,
|
||||
before,
|
||||
after,
|
||||
limit,
|
||||
});
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'channel',
|
||||
targetId: channel_id,
|
||||
action: AdminAuditReadActions.LIST_CHANNEL_MESSAGES,
|
||||
metadata: {
|
||||
limit,
|
||||
before,
|
||||
after,
|
||||
limit,
|
||||
}),
|
||||
);
|
||||
result_count: response.messages.length,
|
||||
has_more: response.has_more,
|
||||
},
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
app.get(
|
||||
@@ -192,13 +247,23 @@ export function MessageAdminController(app: HonoApp) {
|
||||
const adminService = ctx.get('adminService');
|
||||
const {channel_id, message_id} = ctx.req.valid('param');
|
||||
const {context_limit} = ctx.req.valid('query');
|
||||
return ctx.json(
|
||||
await adminService.messageService.lookupMessage({
|
||||
const response = await adminService.messageService.lookupMessage({
|
||||
channel_id,
|
||||
message_id,
|
||||
context_limit,
|
||||
});
|
||||
await recordAdminRead(ctx, {
|
||||
targetType: 'message',
|
||||
targetId: message_id,
|
||||
action: AdminAuditReadActions.GET_MESSAGE,
|
||||
metadata: {
|
||||
channel_id,
|
||||
message_id,
|
||||
context_limit,
|
||||
}),
|
||||
);
|
||||
found: response.messages.some((message) => message.id === message_id.toString()),
|
||||
result_count: response.messages.length,
|
||||
},
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
app.delete(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user