diff --git a/.dockerignore b/.dockerignore index 9860edd..e9ff2c1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -25,4 +25,6 @@ venv/ __pycache__/ *.pyc .cargo/ -infra/ \ No newline at end of file +infra/ +backups/ +certs/ \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1206f51..dc79e16 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,19 +11,30 @@ env: RUST_BACKTRACE: 1 jobs: - check-rust: - name: Rust Check + fmt: + name: Rust Format runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install Rust stable - uses: dtolnay/rust-action/setup@v1 + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libssl-dev libgtksourceview-3.0-dev + - name: Check formatting + run: cargo fmt --all -- --check + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + components: clippy - name: Cache Cargo uses: actions/cache@v4 @@ -32,35 +43,23 @@ jobs: ~/.cargo/registry ~/.cargo/git target - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: ${{ runner.os }}-cargo- + key: ${{ runner.os }}-cargo-clippy-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo-clippy- - - name: Cargo check (client) - run: cargo check -p eifeldc-client + - name: Clippy (server) + run: cargo clippy -p eifeldc-server -- -D warnings - - name: Cargo check (bot-sdk) - run: cargo check -p eifeldc-bot-sdk + - name: Clippy (bot-sdk) + run: cargo clippy -p eifeldc-bot-sdk -- -D warnings - - name: Cargo check (server) - run: cargo check -p eifeldc-server - - - name: Cargo clippy - run: cargo clippy --workspace -- -D warnings - - test-rust: - name: Rust Tests + test-server: + name: Test Server runs-on: ubuntu-latest - needs: check-rust steps: - uses: actions/checkout@v4 - name: Install Rust stable - uses: dtolnay/rust-action/setup@v1 - - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libssl-dev + uses: dtolnay/rust-toolchain@stable - name: Cache Cargo uses: actions/cache@v4 @@ -69,11 +68,14 @@ jobs: ~/.cargo/registry ~/.cargo/git target - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: ${{ runner.os }}-cargo- + key: ${{ runner.os }}-cargo-server-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo-server- - - name: Run tests - run: cargo test --workspace + - name: Test (server) + run: cargo test -p eifeldc-server + + - name: Test (bot-sdk) + run: cargo test -p eifeldc-bot-sdk check-frontend: name: Frontend Check @@ -89,118 +91,84 @@ jobs: with: node-version: 20 cache: npm - cache-dependency-path: client/src-ui/package.json + cache-dependency-path: client/src-ui/package-lock.json - name: Install dependencies run: npm ci - - name: Svelte check - run: npm run check - - name: Build run: npm run build - build-tauri-macos: - name: Build Tauri (macOS) - runs-on: macos-latest - needs: [check-rust, check-frontend] - steps: - - uses: actions/checkout@v4 - - - name: Install Rust stable - uses: dtolnay/rust-action/setup@v1 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: npm - cache-dependency-path: client/src-ui/package.json - - - name: Install frontend deps - run: npm ci - working-directory: client/src-ui - - - name: Build Tauri (macOS) - uses: tauri-apps/tauri-action@v0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - projectPath: client - tagName: v__VERSION__ - releaseName: "EifelDC v__VERSION__" - releaseBody: "EifelDC Release" - releaseDraft: true - args: --target aarch64-apple-darwin - - - name: Upload macOS .dmg - uses: actions/upload-artifact@v4 - with: - name: EifelDC-macos-aarch64 - path: client/src-tauri/target/release/bundle/dmg/*.dmg - - build-tauri-macos-intel: - name: Build Tauri (macOS Intel) - runs-on: macos-latest - needs: [check-rust, check-frontend] - steps: - - uses: actions/checkout@v4 - - - name: Install Rust stable - uses: dtolnay/rust-action/setup@v1 - with: - targets: x86_64-apple-darwin - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: npm - cache-dependency-path: client/src-ui/package.json - - - name: Install frontend deps - run: npm ci - working-directory: client/src-ui - - - name: Build Tauri (macOS Intel) - uses: tauri-apps/tauri-action@v0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - projectPath: client - tagName: v__VERSION__ - releaseName: "EifelDC v__VERSION__" - releaseBody: "EifelDC Release" - releaseDraft: true - args: --target x86_64-apple-darwin - - - name: Upload macOS Intel .dmg - uses: actions/upload-artifact@v4 - with: - name: EifelDC-macos-x86_64 - path: client/src-tauri/target/x86_64-apple-darwin/release/bundle/dmg/*.dmg - - build-tauri-linux: - name: Build Tauri (Linux) + check-client-tauri: + name: Tauri Client Check runs-on: ubuntu-latest - needs: [check-rust, check-frontend] steps: - uses: actions/checkout@v4 - name: Install Rust stable - uses: dtolnay/rust-action/setup@v1 + uses: dtolnay/rust-toolchain@stable - name: Install system dependencies run: | sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libssl-dev libgtksourceview-3.0-dev + sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libssl-dev + + - name: Cache Cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-client-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo-client- + + - name: Cargo check (client) + run: cargo check -p eifeldc-client + + build-docker: + name: Docker Build + runs-on: ubuntu-latest + needs: [test-server, check-frontend] + if: github.ref == 'refs/heads/main' + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: false + tags: eifeldc:latest + cache-from: type=gha + cache-to: type=gha,mode=max + + build-tauri-linux: + name: Build Tauri (Linux) + runs-on: ubuntu-latest + needs: [check-client-tauri, check-frontend] + if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') + steps: + - uses: actions/checkout@v4 + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libssl-dev - name: Setup Node uses: actions/setup-node@v4 with: node-version: 20 cache: npm - cache-dependency-path: client/src-ui/package.json + cache-dependency-path: client/src-ui/package-lock.json - name: Install frontend deps run: npm ci diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..2ed3226 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,235 @@ +name: Release + +on: + push: + tags: + - 'v*' + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + build-server: + name: Build Server Binary + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + + - name: Cache Cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo-release- + + - name: Build release binary + run: cargo build --release -p eifeldc-server + + - name: Upload server binary + uses: actions/upload-artifact@v4 + with: + name: eifeldc-server-linux-amd64 + path: target/release/eifeldc-server + + build-frontend: + name: Build Frontend + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: client/src-ui/package-lock.json + + - name: Install dependencies + run: cd client/src-ui && npm ci + + - name: Build + run: cd client/src-ui && npm run build + + - name: Upload frontend dist + uses: actions/upload-artifact@v4 + with: + name: eifeldc-frontend + path: client/src-ui/dist + + docker-push: + name: Docker Push + runs-on: ubuntu-latest + needs: [] + steps: + - uses: actions/checkout@v4 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/${{ github.repository }}/server + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + build-tauri-linux: + name: Build Tauri (Linux) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libssl-dev + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: client/src-ui/package-lock.json + + - name: Install frontend deps + run: cd client/src-ui && npm ci + + - name: Build Tauri (Linux) + uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + projectPath: client + tagName: ${{ github.ref_name }} + releaseName: "EifelDC ${{ github.ref_name }}" + releaseBody: "See [CHANGELOG.md](CHANGELOG.md) for details." + releaseDraft: true + + build-tauri-macos: + name: Build Tauri (macOS) + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: client/src-ui/package-lock.json + + - name: Install frontend deps + run: cd client/src-ui && npm ci + + - name: Build Tauri (macOS) + uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + projectPath: client + tagName: ${{ github.ref_name }} + releaseName: "EifelDC ${{ github.ref_name }}" + releaseBody: "See [CHANGELOG.md](CHANGELOG.md) for details." + releaseDraft: true + + build-tauri-windows: + name: Build Tauri (Windows) + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: client/src-ui/package-lock.json + + - name: Install frontend deps + run: cd client/src-ui && npm ci + + - name: Build Tauri (Windows) + uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + projectPath: client + tagName: ${{ github.ref_name }} + releaseName: "EifelDC ${{ github.ref_name }}" + releaseBody: "See [CHANGELOG.md](CHANGELOG.md) for details." + releaseDraft: true + + create-release: + name: Create GitHub Release + runs-on: ubuntu-latest + needs: [build-server, build-frontend, docker-push] + steps: + - uses: actions/checkout@v4 + + - name: Download server binary + uses: actions/download-artifact@v4 + with: + name: eifeldc-server-linux-amd64 + path: release-assets + + - name: Download frontend dist + uses: actions/download-artifact@v4 + with: + name: eifeldc-frontend + path: release-assets/frontend + + - name: Create release archive + run: | + cd release-assets + tar czf eifeldc-server-linux-amd64.tar.gz eifeldc-server + cd frontend + tar czf ../eifeldc-frontend.tar.gz . + cd .. + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + draft: true + generate_release_notes: true + files: | + release-assets/eifeldc-server-linux-amd64.tar.gz + release-assets/eifeldc-frontend.tar.gz \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index f96edd5..b1ecb06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aead" version = "0.5.2" @@ -56,6 +62,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -169,6 +190,29 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -201,6 +245,7 @@ dependencies = [ "matchit", "memchr", "mime", + "multer", "percent-encoding", "pin-project-lite", "rustversion", @@ -253,6 +298,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.21.7" @@ -271,6 +322,21 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "1.3.2" @@ -282,6 +348,9 @@ name = "bitflags" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] [[package]] name = "bitmaps" @@ -321,6 +390,36 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bs58" version = "0.5.1" @@ -336,6 +435,12 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + [[package]] name = "byteorder" version = "1.5.0" @@ -347,6 +452,9 @@ name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] [[package]] name = "bytesize" @@ -354,6 +462,73 @@ version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e93abca9e28e0a1b9877922aacb20576e05d4679ffa78c3d6dc22a26a216659" +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.11.1", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + [[package]] name = "cbc" version = "0.1.2" @@ -373,6 +548,33 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -391,6 +593,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chacha20" version = "0.9.1" @@ -426,7 +634,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -440,6 +648,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -470,6 +688,22 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -496,6 +730,30 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.10.1", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.10.1", + "libc", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -514,12 +772,42 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -531,6 +819,56 @@ dependencies = [ "typenum", ] +[[package]] +name = "cssparser" +version = "0.29.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "matches", + "phf 0.10.1", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "ctr" version = "0.9.2" @@ -568,6 +906,40 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + [[package]] name = "data-encoding" version = "2.11.0" @@ -636,6 +1008,7 @@ dependencies = [ "const-oid", "der_derive", "flagset", + "pem-rfc7468", "zeroize", ] @@ -650,6 +1023,65 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "device-info" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2ca8e71544c1b67dcdbc2699ab258828aff985e5bc8d5f6b486d90d7df2f848" +dependencies = [ + "core-foundation 0.10.1", + "jni", + "libc", + "thiserror 2.0.18", + "wasm-bindgen", + "web-sys", + "windows-sys 0.59.0", +] + [[package]] name = "digest" version = "0.10.7" @@ -657,10 +1089,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.1", + "block2", + "libc", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -672,6 +1138,94 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser 0.36.0", + "foldhash 0.2.0", + "html5ever 0.38.0", + "precomputed-hash", + "selectors 0.36.1", + "tendril 0.5.0", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + [[package]] name = "ed25519" version = "2.2.3" @@ -698,6 +1252,39 @@ dependencies = [ "zeroize", ] +[[package]] +name = "eifeldc-bot-sdk" +version = "0.1.0" +dependencies = [ + "anyhow", + "futures", + "matrix-sdk", + "mime", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "eifeldc-client" +version = "0.1.0" +dependencies = [ + "anyhow", + "reqwest 0.12.28", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-shell", + "thiserror 1.0.69", + "tokio", + "tracing", + "tracing-subscriber", + "url", +] + [[package]] name = "eifeldc-server" version = "0.1.0" @@ -705,11 +1292,18 @@ dependencies = [ "anyhow", "axum", "chrono", + "futures", + "lazy_static", + "livekit-api", "matrix-sdk", "matrix-sdk-base", + "mime", + "prometheus", "reqwest 0.12.28", + "rusqlite", "serde", "serde_json", + "tempfile", "thiserror 1.0.69", "tokio", "tower 0.4.13", @@ -726,6 +1320,47 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "embed-resource" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+spec-1.1.0", + "vswhom", + "winreg 0.55.0", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -741,6 +1376,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "errno" version = "0.3.14" @@ -837,24 +1483,69 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flagset" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -867,13 +1558,40 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared", + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -882,6 +1600,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -891,6 +1615,31 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -898,6 +1647,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -906,6 +1656,17 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.32" @@ -952,6 +1713,114 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -960,6 +1829,18 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", ] [[package]] @@ -971,7 +1852,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -1000,6 +1881,91 @@ dependencies = [ "wasip3", ] +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.11.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "gloo-timers" version = "0.3.0" @@ -1025,6 +1991,80 @@ dependencies = [ "web-sys", ] +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "h2" version = "0.3.27" @@ -1037,7 +2077,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -1056,13 +2096,19 @@ dependencies = [ "futures-core", "futures-sink", "http 1.4.0", - "indexmap", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -1079,7 +2125,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -1097,6 +2143,12 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -1109,6 +2161,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hkdf" version = "0.12.4" @@ -1127,6 +2185,28 @@ dependencies = [ "digest", ] +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever 0.14.1", + "match_token", +] + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever 0.38.0", +] + [[package]] name = "http" version = "0.2.12" @@ -1327,7 +2407,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.62.2", ] [[package]] @@ -1339,6 +2419,16 @@ dependencies = [ "cc", ] +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png", +] + [[package]] name = "icu_collections" version = "2.2.0" @@ -1427,6 +2517,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -1507,6 +2603,17 @@ dependencies = [ "web-sys", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -1519,6 +2626,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + [[package]] name = "inout" version = "0.1.4" @@ -1557,6 +2673,25 @@ dependencies = [ "serde", ] +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + [[package]] name = "itertools" version = "0.10.5" @@ -1566,6 +2701,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.12.1" @@ -1590,6 +2734,73 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "js-sys" version = "0.3.95" @@ -1620,6 +2831,60 @@ dependencies = [ "serde", ] +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "jsonwebtoken" +version = "10.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" +dependencies = [ + "base64 0.22.1", + "ed25519-dalek", + "getrandom 0.2.17", + "hmac", + "js-sys", + "p256", + "p384", + "rand 0.8.6", + "rsa", + "serde", + "serde_json", + "sha2", + "signature", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.11.1", + "serde", + "unicode-segmentation", +] + [[package]] name = "konst" version = "0.3.17" @@ -1640,11 +2905,26 @@ dependencies = [ "typewit", ] +[[package]] +name = "kuchikiki" +version = "0.8.8-speedreader" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" +dependencies = [ + "cssparser 0.29.6", + "html5ever 0.29.1", + "indexmap 2.14.0", + "selectors 0.24.0", +] + [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "leb128fmt" @@ -1652,18 +2932,68 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + [[package]] name = "libc" version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "libc", +] + [[package]] name = "libsqlite3-sys" version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" dependencies = [ + "cc", "pkg-config", "vcpkg", ] @@ -1680,6 +3010,59 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +[[package]] +name = "livekit-api" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900451a686a1ce8488c420e81a2135831383b03aade6bc3075cf80463d3dd6a4" +dependencies = [ + "base64 0.21.7", + "device-info", + "http 1.4.0", + "jsonwebtoken", + "livekit-protocol", + "log", + "os_info", + "parking_lot", + "pbjson-types", + "prost 0.12.6", + "rand 0.9.4", + "reqwest 0.12.28", + "scopeguard", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "livekit-protocol" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cf1cc4ab39d7857fb31be648f43aa7068acb54a6960270dccd300dd5c8d0a98" +dependencies = [ + "futures-util", + "livekit-runtime", + "parking_lot", + "pbjson", + "pbjson-types", + "prost 0.12.6", + "serde", + "thiserror 2.0.18", + "tokio", +] + +[[package]] +name = "livekit-runtime" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532e84c6cdc5fe774f2b5d9912597b5f3bea561927a48296d03e24549d21c3f6" +dependencies = [ + "tokio", + "tokio-stream", +] + [[package]] name = "lock_api" version = "0.4.14" @@ -1695,6 +3078,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + [[package]] name = "macroific" version = "1.3.1" @@ -1748,6 +3137,42 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache 0.8.9", + "string_cache_codegen 0.5.4", + "tendril 0.4.3", +] + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril 0.5.0", + "web_atoms", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "matchers" version = "0.2.0" @@ -1757,6 +3182,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + [[package]] name = "matchit" version = "0.7.3" @@ -1810,7 +3241,7 @@ dependencies = [ "gloo-timers", "http 0.2.12", "imbl", - "indexmap", + "indexmap 2.14.0", "matrix-sdk-base", "matrix-sdk-common", "matrix-sdk-indexeddb", @@ -1995,6 +3426,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -2017,6 +3457,16 @@ dependencies = [ "unicase", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.2.0" @@ -2024,10 +3474,54 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.61.2", ] +[[package]] +name = "muda" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c9fec5a4e89860383d778d10563a605838f8f0b2f9303868937e5ff32e86177" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http 1.4.0", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + [[package]] name = "native-tls" version = "0.2.18" @@ -2045,6 +3539,60 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -2054,6 +3602,48 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2061,6 +3651,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2073,6 +3664,224 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.1", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -2085,6 +3894,18 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "open" +version = "5.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f3bab717c29a857abf75fcef718d441ec7cb2725f937343c734740a985d37fd" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + [[package]] name = "openssl" version = "0.10.78" @@ -2093,7 +3914,7 @@ checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222" dependencies = [ "bitflags 2.11.1", "cfg-if", - "foreign-types", + "foreign-types 0.3.2", "libc", "once_cell", "openssl-macros", @@ -2129,6 +3950,87 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "os_info" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224" +dependencies = [ + "android_system_properties", + "log", + "nix", + "objc2", + "objc2-foundation", + "objc2-ui-kit", + "serde", + "windows-sys 0.61.2", +] + +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "parking" version = "2.2.1" @@ -2155,7 +4057,50 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-link", + "windows-link 0.2.1", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pbjson" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1030c719b0ec2a2d25a5df729d6cff1acf3cc230bf766f4f97833591f7577b90" +dependencies = [ + "base64 0.21.7", + "serde", +] + +[[package]] +name = "pbjson-build" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2580e33f2292d34be285c5bc3dba5259542b083cfad6037b6d70345f24dcb735" +dependencies = [ + "heck 0.4.1", + "itertools 0.11.0", + "prost 0.12.6", + "prost-types", +] + +[[package]] +name = "pbjson-types" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18f596653ba4ac51bdecbb4ef6773bc7f56042dc13927910de1684ad3d32aa12" +dependencies = [ + "bytes", + "chrono", + "pbjson", + "pbjson-build", + "prost 0.12.6", + "prost-build", + "serde", ] [[package]] @@ -2168,18 +4113,235 @@ dependencies = [ "hmac", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.14.0", +] + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared 0.8.0", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.6", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.6", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.2", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher 1.0.2", +] + [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + [[package]] name = "pkcs7" version = "0.4.1" @@ -2207,6 +4369,32 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -2227,6 +4415,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2236,6 +4430,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "prettyplease" version = "0.2.37" @@ -2246,6 +4446,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -2320,6 +4529,12 @@ dependencies = [ "quote", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.106" @@ -2329,6 +4544,31 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prometheus" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d33c28a30771f7f96db69893f78b857f7450d7e0237e9c8fc6427a81bae7ed1" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "memchr", + "parking_lot", + "protobuf", + "thiserror 1.0.69", +] + +[[package]] +name = "prost" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +dependencies = [ + "bytes", + "prost-derive 0.12.6", +] + [[package]] name = "prost" version = "0.13.5" @@ -2336,7 +4576,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" dependencies = [ "bytes", - "prost-derive", + "prost-derive 0.13.5", +] + +[[package]] +name = "prost-build" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" +dependencies = [ + "bytes", + "heck 0.5.0", + "itertools 0.12.1", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost 0.12.6", + "prost-types", + "regex", + "syn 2.0.117", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +dependencies = [ + "anyhow", + "itertools 0.12.1", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -2352,6 +4626,30 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "prost-types" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" +dependencies = [ + "prost 0.12.6", +] + +[[package]] +name = "protobuf" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" + +[[package]] +name = "quick-xml" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.45" @@ -2373,6 +4671,20 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + [[package]] name = "rand" version = "0.8.6" @@ -2394,6 +4706,16 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -2414,6 +4736,15 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -2432,6 +4763,24 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + [[package]] name = "rand_xoshiro" version = "0.6.0" @@ -2441,6 +4790,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + [[package]] name = "readlock" version = "0.1.11" @@ -2456,6 +4811,37 @@ dependencies = [ "bitflags 2.11.1", ] +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "regex" version = "1.12.3" @@ -2522,9 +4908,9 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", + "wasm-streams 0.4.2", "web-sys", - "winreg", + "winreg 0.50.0", ] [[package]] @@ -2569,6 +4955,74 @@ dependencies = [ "web-sys", ] +[[package]] +name = "reqwest" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.9.0", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper 1.0.2", + "tokio", + "tokio-util", + "tower 0.5.3", + "tower-http 0.6.8", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.5.0", + "web-sys", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + [[package]] name = "ring" version = "0.17.14" @@ -2602,6 +5056,26 @@ dependencies = [ "serde", ] +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "ruma" version = "0.9.4" @@ -2648,7 +5122,7 @@ dependencies = [ "form_urlencoded", "getrandom 0.2.17", "http 0.2.12", - "indexmap", + "indexmap 2.14.0", "js-sys", "js_int", "konst", @@ -2674,7 +5148,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d20a52770e5a9fb30b7a1c14ba8b3dcf76dadc01674e58e40094f78e6bd5e3f1" dependencies = [ "as_variant", - "indexmap", + "indexmap 2.14.0", "js_int", "js_option", "percent-encoding", @@ -2726,7 +5200,7 @@ dependencies = [ "ruma-identifiers-validation", "serde", "syn 2.0.117", - "toml", + "toml 0.8.2", ] [[package]] @@ -2743,6 +5217,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + [[package]] name = "rustc_version" version = "0.4.1" @@ -2819,6 +5299,15 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.29" @@ -2828,12 +5317,77 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "3.7.0" @@ -2857,11 +5411,52 @@ dependencies = [ "libc", ] +[[package]] +name = "selectors" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" +dependencies = [ + "bitflags 1.3.2", + "cssparser 0.29.6", + "derive_more 0.99.20", + "fxhash", + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc 0.2.0", + "smallvec", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.11.1", + "cssparser 0.36.0", + "derive_more 2.1.1", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen 0.13.1", + "precomputed-hash", + "rustc-hash", + "servo_arc 0.4.3", + "smallvec", +] + [[package]] name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "serde" @@ -2873,6 +5468,18 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + [[package]] name = "serde-wasm-bindgen" version = "0.6.5" @@ -2914,6 +5521,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "serde_html_form" version = "0.2.8" @@ -2921,7 +5539,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" dependencies = [ "form_urlencoded", - "indexmap", + "indexmap 2.14.0", "itoa", "ryu", "serde_core", @@ -2951,6 +5569,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "serde_spanned" version = "0.6.9" @@ -2960,6 +5589,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2972,6 +5610,78 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "servo_arc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "sha1" version = "0.10.6" @@ -3003,12 +5713,44 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -3025,9 +5767,28 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ + "digest", "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + [[package]] name = "slab" version = "0.4.12" @@ -3060,6 +5821,60 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "spki" version = "0.7.3" @@ -3076,12 +5891,78 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.13.1", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + [[package]] name = "syn" version = "1.0.109" @@ -3172,6 +6053,368 @@ dependencies = [ "libc", ] +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.34.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9103edf55f2da3c82aea4c7fab7c4241032bfeea0e71fa557d98e00e7ce7cc20" +dependencies = [ + "bitflags 2.11.1", + "block2", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "parking_lot", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da77cc00fb9028caf5b5d4650f75e31f1ef3693459dfca7f7e506d1ecef0ba2d" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http 1.4.0", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest 0.13.3", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "toml 0.9.12+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.117", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddde7d51c907b940fb573006cdda9a642d6a7c8153657e88f8a5c3c9290cd4aa" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "toml 0.9.12+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1fa4150c95ae391946cc8b8f905ab14797427caba3a8a2f79628e956da91809" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36e1ec28b79f3d0683f4507e1615c36292c0ea6716668770d4396b9b39871ed8" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-plugin-shell" +version = "2.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars 0.8.22", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "tokio", +] + +[[package]] +name = "tauri-runtime" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2826d79a3297ed08cd6ea7f412644ef58e32969504bc4fbd8d7dbeabc4445ea2" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http 1.4.0", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e" +dependencies = [ + "gtk", + "http 1.4.0", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dunce", + "glob", + "html5ever 0.29.1", + "http 1.4.0", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.3", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.2+spec-1.1.0", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -3185,6 +6428,27 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -3234,6 +6498,37 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.3" @@ -3351,11 +6646,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" dependencies = [ "serde", - "serde_spanned", + "serde_spanned 0.6.9", "toml_datetime 0.6.3", "toml_edit 0.20.2", ] +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.2", +] + [[package]] name = "toml_datetime" version = "0.6.3" @@ -3365,6 +6690,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" @@ -3380,7 +6714,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap", + "indexmap 2.14.0", "toml_datetime 0.6.3", "winnow 0.5.40", ] @@ -3391,9 +6725,9 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap", + "indexmap 2.14.0", "serde", - "serde_spanned", + "serde_spanned 0.6.9", "toml_datetime 0.6.3", "winnow 0.5.40", ] @@ -3404,7 +6738,7 @@ version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ - "indexmap", + "indexmap 2.14.0", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "winnow 1.0.2", @@ -3419,6 +6753,12 @@ dependencies = [ "winnow 1.0.2", ] +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + [[package]] name = "tower" version = "0.4.13" @@ -3545,6 +6885,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.23" @@ -3555,12 +6905,37 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex-automata", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", + "tracing-serde", +] + +[[package]] +name = "tray-icon" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror 2.0.18", + "windows-sys 0.60.2", ] [[package]] @@ -3587,6 +6962,12 @@ dependencies = [ "utf-8", ] +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.20.0" @@ -3618,6 +6999,47 @@ dependencies = [ "web-time", ] +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + [[package]] name = "unicase" version = "2.9.0" @@ -3630,6 +7052,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -3662,6 +7090,7 @@ dependencies = [ "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -3670,6 +7099,18 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + [[package]] name = "utf-8" version = "0.7.6" @@ -3690,6 +7131,7 @@ checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.2", "js-sys", + "serde_core", "wasm-bindgen", ] @@ -3705,6 +7147,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + [[package]] name = "version_check" version = "0.9.5" @@ -3730,7 +7178,7 @@ dependencies = [ "hmac", "matrix-pickle", "pkcs7", - "prost", + "prost 0.13.5", "rand 0.8.6", "serde", "serde_bytes", @@ -3742,6 +7190,36 @@ dependencies = [ "zeroize", ] +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -3751,6 +7229,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -3847,7 +7331,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap", + "indexmap 2.14.0", "wasm-encoder", "wasmparser", ] @@ -3865,6 +7349,19 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmparser" version = "0.244.0" @@ -3873,7 +7370,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags 2.11.1", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.14.0", "semver", ] @@ -3897,12 +7394,185 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web_atoms" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" +dependencies = [ + "phf 0.13.1", + "phf_codegen 0.13.1", + "string_cache 0.9.0", + "string_cache_codegen 0.6.1", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + [[package]] name = "wildmatch" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29333c3ea1ba8b17211763463ff24ee84e41c78224c16b001cd907e663a38c68" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -3911,9 +7581,20 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", ] [[package]] @@ -3938,21 +7619,46 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + [[package]] name = "windows-registry" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", ] [[package]] @@ -3961,7 +7667,16 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", ] [[package]] @@ -3970,7 +7685,16 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", ] [[package]] @@ -3991,13 +7715,46 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -4024,13 +7781,54 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4043,6 +7841,18 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -4055,6 +7865,18 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -4067,12 +7889,30 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -4085,6 +7925,18 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4097,6 +7949,18 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4109,6 +7973,18 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -4121,6 +7997,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.5.40" @@ -4130,6 +8012,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + [[package]] name = "winnow" version = "1.0.2" @@ -4149,6 +8037,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + [[package]] name = "wit-bindgen" version = "0.51.0" @@ -4171,7 +8069,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", - "heck", + "heck 0.5.0", "wit-parser", ] @@ -4182,8 +8080,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", - "heck", - "indexmap", + "heck 0.5.0", + "indexmap 2.14.0", "prettyplease", "syn 2.0.117", "wasm-metadata", @@ -4214,7 +8112,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags 2.11.1", - "indexmap", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -4233,7 +8131,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap", + "indexmap 2.14.0", "log", "semver", "serde", @@ -4249,6 +8147,71 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "wry" +version = "0.54.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a8135d8676225e5744de000d4dff5a082501bf7db6a1c1495034f8c314edbc" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http 1.4.0", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + [[package]] name = "x25519-dalek" version = "2.0.1" diff --git a/Cargo.toml b/Cargo.toml index dedfaf9..e776f20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,8 @@ resolver = "2" members = [ "server", + "bot-sdk", + "client", ] [workspace.dependencies] @@ -13,6 +15,6 @@ tokio = { version = "1", features = ["full"] } anyhow = "1" thiserror = "1" tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } reqwest = { version = "0.12", features = ["json", "multipart"] } url = "2" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 99ff998..4644f97 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,15 +12,27 @@ WORKDIR /app COPY Cargo.toml Cargo.lock ./ COPY server/Cargo.toml server/Cargo.toml -RUN mkdir -p server/src && echo "fn main() {}" > server/src/main.rs -RUN cargo build --release -p eifeldc-server && rm -rf server/src +COPY bot-sdk/Cargo.toml bot-sdk/Cargo.toml +COPY client/Cargo.toml client/Cargo.toml + +RUN mkdir -p server/src bot-sdk/src client/src && \ + echo "fn main() {}" > server/src/main.rs && \ + echo "" > bot-sdk/src/lib.rs && \ + echo "" > client/src/main.rs + +RUN cargo build --release -p eifeldc-server || true +RUN rm -rf server/src bot-sdk/src client/src COPY server/src server/src -RUN touch server/src/main.rs && cargo build --release -p eifeldc-server +COPY bot-sdk/src bot-sdk/src +COPY client/src client/src + +RUN touch server/src/main.rs bot-sdk/src/lib.rs client/src/main.rs && \ + cargo build --release -p eifeldc-server FROM debian:bookworm-slim -RUN apt-get update && apt-get install -y ca-certificates libssl3 && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y ca-certificates libssl3 curl && rm -rf /var/lib/apt/lists/* RUN useradd -m -d /home/eifeldc eifeldc @@ -31,7 +43,13 @@ USER eifeldc ENV EIFELDC_STATIC_DIR=/usr/share/eifeldc/client ENV RUST_LOG=eifeldc_server=info,tower_http=info +ENV LIVEKIT_API_KEY=devkey +ENV LIVEKIT_API_SECRET=devsecret +ENV LIVEKIT_URL=ws://livekit:7880 EXPOSE 3000 +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD curl -f http://localhost:3000/api/current-user || exit 1 + CMD ["eifeldc-server"] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..31eec13 --- /dev/null +++ b/Makefile @@ -0,0 +1,49 @@ +.PHONY: build check test dev clean docker docker-up docker-down docker-logs docker-build health backup setup + +build-server: + cargo build --release -p eifeldc-server + +build-bot-sdk: + cargo build --release -p eifeldc-bot-sdk + +build-frontend: + cd client/src-ui && npm ci && npm run build + +build: build-server build-frontend + +check: + cargo check -p eifeldc-server -p eifeldc-bot-sdk + cd client/src-ui && npm ci && npm run build + +test: + cargo test -p eifeldc-server -p eifeldc-bot-sdk + +clean: + cargo clean + rm -rf client/src-ui/node_modules client/src-ui/dist + +dev: + cargo run -p eifeldc-server + +docker-build: + docker build -t eifeldc:latest . + +docker-up: + cd infra && docker compose up -d + +docker-down: + cd infra && docker compose down + +docker-logs: + cd infra && docker compose logs -f eifeldc + +docker-restart: docker-down docker-up + +health: + bash infra/scripts/healthcheck.sh + +backup: + bash infra/scripts/backup.sh + +setup: + bash infra/scripts/setup.sh \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..401cd30 --- /dev/null +++ b/README.md @@ -0,0 +1,300 @@ +# EifelDC + +Discord-like Matrix chat platform built with Rust, Svelte, and Tauri. + +## Architecture + +| Component | Crate | Description | +|---|---|---| +| **Server** | `eifeldc-server` | Axum web server, Matrix SDK, LiveKit voice, SQLite sessions | +| **Bot SDK** | `eifeldc-bot-sdk` | SDK for building Matrix bots | +| **Client** | `eifeldc-client` | Tauri desktop app | +| **Frontend** | — | Svelte + TypeScript UI | + +## Prerequisites + +- Rust 1.82+ (stable) +- Node.js 20+ +- A running Matrix Synapse homeserver +- LiveKit server (for voice) + +## Quick Start + +### Build Server & Bot SDK + +```bash +cargo build --release -p eifeldc-server -p eifeldc-bot-sdk +``` + +### Build Frontend + +```bash +cd client/src-ui +npm ci +npm run build +``` + +### Build Tauri Desktop App + +Requires system dependencies: + +```bash +# Ubuntu/Debian +sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libssl-dev +``` + +```bash +cargo build --release -p eifeldc-client +``` + +### Run the Server + +```bash +cargo run -p eifeldc-server +``` + +The server starts on `http://0.0.0.0:3000` and serves the frontend from `client/src-ui/dist`. + +## Environment Variables + +| Variable | Default | Description | +|---|---|---| +| `EIFELDC_DB` | `eifeldc.db` | SQLite database path for session storage | +| `EIFELDC_STATIC_DIR` | `client/src-ui/dist` | Path to frontend static files | +| `LIVEKIT_API_KEY` | `devkey` | LiveKit API key | +| `LIVEKIT_API_SECRET` | `devsecret` | LiveKit API secret | +| `LIVEKIT_URL` | `ws://localhost:7880` | LiveKit server URL | +| `RUST_LOG` | — | Logging filter (e.g. `eifeldc_server=info`) | +| `EIFELDC_LOG_FORMAT` | `pretty` | Log format: `pretty` or `json` | +| `EIFELDC_CORS_ORIGINS` | `*` | Comma-separated allowed origins (e.g. `https://app.example.org,https://dev.example.org`) | +| `EIFELDC_SESSION_TTL` | — | Session TTL in seconds (no expiry if unset) | +| `EIFELDC_RATE_LIMIT` | `60` | Max requests per minute per client | +| `EIFELDC_MAX_UPLOAD_MB` | `50` | Max upload size in MB | + +## Docker Deployment + +```bash +# Configure environment +cp infra/.env.example infra/.env +# Edit infra/.env with your domain and secrets + +# Setup infrastructure (generates secrets, configs, certs) +bash infra/scripts/setup.sh your.domain.com + +# Or manually: +cd infra +docker compose up -d +``` + +Services: EifelDC server, Synapse, PostgreSQL, LiveKit, coturn, nginx. + +### Useful Commands + +```bash +make docker-build # Build Docker image +make docker-up # Start containers +make docker-down # Stop containers +make docker-logs # Follow EifelDC logs +make health # Run healthcheck +make backup # Backup data +``` + +## API Reference + +All endpoints are under `/api`. Public endpoints require no auth. Protected endpoints require a `Bearer` token in the `Authorization` header. + +### Authentication + +| Method | Endpoint | Auth | Description | +|---|---|---|---| +| POST | `/api/login` | No | Login with Matrix credentials | +| POST | `/api/register` | No | Register a new Matrix user | +| GET | `/api/current-user` | No | Get current user ID from token | +| POST | `/api/logout` | Yes | Logout and invalidate session | + +**Login Request:** +```json +{ "homeserver": "https://matrix.example.org", "username": "alice", "password": "secret" } +``` + +**Login Response:** +```json +{ "success": true, "user_id": "@alice:example.org", "token": "uuid-token", "error": null } +``` + +### Rooms + +| Method | Endpoint | Description | +|---|---|---| +| GET | `/api/rooms` | List joined rooms | +| POST | `/api/rooms/create` | Create a new room | +| POST | `/api/rooms/join` | Join a room by ID or alias | +| POST | `/api/rooms/{room_id}/leave` | Leave a room | +| GET | `/api/rooms/{room_id}/members` | List room members | +| POST | `/api/rooms/{room_id}/name` | Set room name | +| POST | `/api/rooms/{room_id}/topic` | Set room topic | +| POST | `/api/rooms/{room_id}/avatar` | Set room avatar (multipart) | +| GET | `/api/rooms/unread` | Get unread notification counts | +| POST | `/api/rooms/{room_id}/read` | Mark room as read | + +### Messages + +| Method | Endpoint | Description | +|---|---|---| +| GET | `/api/rooms/{room_id}/messages` | Get room messages | +| POST | `/api/rooms/{room_id}/send` | Send a message | +| POST | `/api/rooms/{room_id}/edit` | Edit a message | +| POST | `/api/rooms/{room_id}/delete/{event_id}` | Delete a message | +| POST | `/api/rooms/{room_id}/react` | React to a message | +| POST | `/api/rooms/{room_id}/typing` | Set typing notification | +| POST | `/api/rooms/{room_id}/upload` | Upload a file (multipart) | + +### Threads + +| Method | Endpoint | Description | +|---|---|---| +| GET | `/api/rooms/{room_id}/threads` | List threads in a room | +| GET | `/api/rooms/{room_id}/threads/{thread_id}` | Get thread messages | +| POST | `/api/rooms/{room_id}/threads/{thread_id}/reply` | Reply in a thread | +| POST | `/api/rooms/{room_id}/reply` | Reply to a message | + +### Voice (LiveKit) + +| Method | Endpoint | Description | +|---|---|---| +| POST | `/api/voice/join` | Join a voice channel | +| POST | `/api/voice/leave` | Leave voice channel | +| POST | `/api/voice/toggle-mute` | Toggle mute | +| POST | `/api/voice/toggle-deafen` | Toggle deafen | +| GET | `/api/voice/participants` | Get voice participants | + +### Presence + +| Method | Endpoint | Description | +|---|---|---| +| POST | `/api/presence/set` | Set presence status | +| GET | `/api/presence/{user_id}` | Get user presence | + +### Profile + +| Method | Endpoint | Description | +|---|---|---| +| GET | `/api/profile/me` | Get own profile | +| GET | `/api/profile/{user_id}` | Get user profile | +| POST | `/api/profile/displayname` | Set display name | +| POST | `/api/profile/avatar` | Upload avatar (multipart) | + +### Emoji & Roles + +| Method | Endpoint | Description | +|---|---|---| +| GET | `/api/rooms/{room_id}/emoji` | Get custom emoji | +| POST | `/api/rooms/{room_id}/emoji/upload` | Upload custom emoji | +| GET | `/api/rooms/{room_id}/roles` | Get room roles | +| POST | `/api/rooms/{room_id}/roles/assign` | Assign role to user | +| POST | `/api/rooms/{room_id}/roles/remove` | Remove role from user | +| GET | `/api/rooms/{room_id}/permissions/{user_id}` | Get user permissions | + +### Media & WebSocket + +| Method | Endpoint | Description | +|---|---|---| +| GET | `/api/media/{mxc_path}` | Proxy Matrix media | +| GET | `/api/metrics` | Prometheus metrics endpoint | +| GET | `/api/ws?token={token}` | WebSocket for real-time events | + +### WebSocket Events + +### Monitoring + +**Prometheus Metrics** are exposed at `/api/metrics`: + +| Metric | Type | Description | +|---|---|---| +| `eifeldc_http_requests_total` | Counter | Total HTTP requests | +| `eifeldc_active_sessions` | Gauge | Active user sessions | +| `eifeldc_active_websockets` | Gauge | Active WebSocket connections | +| `eifeldc_messages_sent_total` | Counter | Total messages sent | +| `eifeldc_rooms_joined_total` | Counter | Total room join operations | +| `eifeldc_uploads_total` | Counter | Total file uploads | +| `eifeldc_voice_participants` | Gauge | Current voice participants | + +**Structured Logging** — set `EIFELDC_LOG_FORMAT=json` for JSON output: + +```json +{"timestamp":"2026-04-29T10:00:00Z","level":"INFO","target":"eifeldc_server::routes::auth","fields":{"message":"LiveKit URL: ws://localhost:7880"}} +``` + +Use `RUST_LOG` for filtering: `RUST_LOG=eifeldc_server=debug,tower_http=info` + +### WebSocket Events + +```json +{ "type": "message", "room_id": "...", "event_id": "...", "sender": "...", "body": "...", "timestamp": 0 } +{ "type": "message_edited", "room_id": "...", "event_id": "...", "new_body": "..." } +{ "type": "message_deleted", "room_id": "...", "redacts": "..." } +{ "type": "reaction", "room_id": "...", "event_id": "...", "key": "👍", "sender": "..." } +{ "type": "room_joined", "room_id": "...", "name": "..." } +{ "type": "room_left", "room_id": "..." } +{ "type": "presence", "user_id": "...", "status": "online|idle|offline", "status_msg": "..." } +{ "type": "typing", "room_id": "...", "user_id": "...", "typing": true } +{ "type": "voice_state_update", "room_id": "...", "user_id": "...", "muted": false, "deafened": false } +{ "type": "voice_user_joined", "room_id": "...", "user_id": "..." } +{ "type": "voice_user_left", "room_id": "...", "user_id": "..." } +{ "type": "thread_reply", "room_id": "...", "root_event_id": "...", "event_id": "...", "sender": "...", "body": "...", "timestamp": 0 } +``` + +## Bot SDK + +```rust +use std::sync::Arc; +use eifeldc_bot_sdk::{BotClient, BotEvent, CommandContext}; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let mut bot = BotClient::new("https://matrix.example.org") + .with_auth("botuser", "botpassword"); + + bot.on_event(|event| { + match event { + BotEvent::Message { room_id, sender, body, .. } => { + println!("{} in {}: {}", sender, room_id, body); + } + _ => {} + } + }); + + bot.on_command("hello", Arc::new(|ctx: CommandContext| { + println!("Hello command from {} in {}!", ctx.sender, ctx.room_id); + })); + + bot.start().await?; + Ok(()) +} +``` + +## Development + +```bash +# Check compilation +cargo check -p eifeldc-server -p eifeldc-bot-sdk + +# Run linter +cargo clippy -p eifeldc-server -p eifeldc-bot-sdk -- -D warnings + +# Check formatting +cargo fmt --all -- --check + +# Run tests +cargo test -p eifeldc-server -p eifeldc-bot-sdk + +# Build frontend +cd client/src-ui && npm ci && npm run build + +# Run dev server +cargo run -p eifeldc-server +``` + +## License + +All rights reserved. \ No newline at end of file diff --git a/bot-sdk/Cargo.toml b/bot-sdk/Cargo.toml index 2708190..3a61df4 100644 --- a/bot-sdk/Cargo.toml +++ b/bot-sdk/Cargo.toml @@ -5,18 +5,10 @@ edition = "2021" [dependencies] matrix-sdk = { workspace = true } -matrix-sdk-base = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } anyhow = { workspace = true } -thiserror = { workspace = true } tracing = { workspace = true } -tracing-subscriber = { workspace = true } -reqwest = { workspace = true } -url = { workspace = true } -async-trait = "0.1" -chrono = { version = "0.4", features = ["serde"] } - -[dev-dependencies] -mockito = "1" \ No newline at end of file +futures = "0.3" +mime = "0.3" \ No newline at end of file diff --git a/bot-sdk/src/auth.rs b/bot-sdk/src/auth.rs index a3892fa..3f8eefd 100644 --- a/bot-sdk/src/auth.rs +++ b/bot-sdk/src/auth.rs @@ -1,3 +1,4 @@ +#[derive(Default)] pub struct BotAuth { pub username: String, pub password: String, @@ -21,4 +22,46 @@ impl BotAuth { pub fn is_configured(&self) -> bool { !self.username.is_empty() && !self.password.is_empty() } -} \ No newline at end of file +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_creates_empty_auth() { + let auth = BotAuth::new(); + assert!(auth.username.is_empty()); + assert!(auth.password.is_empty()); + assert!(!auth.is_configured()); + } + + #[test] + fn default_creates_empty_auth() { + let auth = BotAuth::default(); + assert!(auth.username.is_empty()); + assert!(auth.password.is_empty()); + } + + #[test] + fn with_credentials_creates_configured_auth() { + let auth = BotAuth::with_credentials("botuser", "secret123"); + assert_eq!(auth.username, "botuser"); + assert_eq!(auth.password, "secret123"); + assert!(auth.is_configured()); + } + + #[test] + fn is_configured_false_when_username_empty() { + let mut auth = BotAuth::new(); + auth.password = "secret".to_string(); + assert!(!auth.is_configured()); + } + + #[test] + fn is_configured_false_when_password_empty() { + let mut auth = BotAuth::new(); + auth.username = "user".to_string(); + assert!(!auth.is_configured()); + } +} diff --git a/bot-sdk/src/client.rs b/bot-sdk/src/client.rs index 1673213..af844e2 100644 --- a/bot-sdk/src/client.rs +++ b/bot-sdk/src/client.rs @@ -1,132 +1,574 @@ +use crate::auth::BotAuth; +use crate::commands::{CommandHandler, CommandRegistry, SharedCommandRegistry}; +use crate::event::{BotEvent, EventHandler, SharedEventHandler}; +use crate::room::{RoomInfo, RoomManager, SharedRoomManager}; +use matrix_sdk::config::SyncSettings; +use matrix_sdk::ruma::events::reaction::ReactionEventContent; +use matrix_sdk::ruma::events::relation::{Annotation, InReplyTo, Replacement}; +use matrix_sdk::ruma::events::room::message::{ + EmoteMessageEventContent, FileMessageEventContent, ImageMessageEventContent, MessageType, + Relation, RoomMessageEventContent, RoomMessageEventContentWithoutRelation, +}; +use matrix_sdk::ruma::OwnedRoomId; use matrix_sdk::Client; use std::sync::Arc; -use tokio::sync::RwLock; - -use crate::auth::BotAuth; -use crate::commands::CommandRegistry; -use crate::event::EventHandler; -use crate::room::RoomManager; +use tokio::sync::watch; +use tokio::sync::Mutex; pub struct BotClient { - client: Arc>>, - auth: Arc>, - commands: Arc>, - event_handler: Arc>, - room_manager: Arc>, homeserver: String, + auth: BotAuth, + client: Option, + command_prefix: String, + commands: SharedCommandRegistry, + event_handlers: SharedEventHandler, + rooms: SharedRoomManager, + shutdown_tx: Option>, } impl BotClient { pub fn new(homeserver: &str) -> Self { Self { - client: Arc::new(RwLock::new(None)), - auth: Arc::new(RwLock::new(BotAuth::new())), - commands: Arc::new(RwLock::new(CommandRegistry::new())), - event_handler: Arc::new(RwLock::new(EventHandler::new())), - room_manager: Arc::new(RwLock::new(RoomManager::new())), homeserver: homeserver.to_string(), + auth: BotAuth::new(), + client: None, + command_prefix: "!".to_string(), + commands: Arc::new(Mutex::new(CommandRegistry::new())), + event_handlers: Arc::new(Mutex::new(EventHandler::new())), + rooms: Arc::new(Mutex::new(RoomManager::new())), + shutdown_tx: None, } } - pub fn with_auth(self, username: &str, password: &str) -> Self { - let auth = BotAuth::with_credentials(username, password); - self.auth = Arc::new(RwLock::new(auth)); + pub fn with_auth(mut self, username: &str, password: &str) -> Self { + self.auth = BotAuth::with_credentials(username, password); self } - pub async fn start(&self) -> anyhow::Result<()> { + pub fn with_command_prefix(mut self, prefix: &str) -> Self { + self.command_prefix = prefix.to_string(); + self + } + + pub fn on_command(&self, name: &str, handler: CommandHandler) { + let mut commands = self.commands.blocking_lock(); + commands.set_prefix(&self.command_prefix); + commands.register(name, handler); + } + + pub fn on_event(&self, handler: impl Fn(BotEvent) + Send + Sync + 'static) { + let mut handlers = self.event_handlers.blocking_lock(); + handlers.add_handler(Arc::new(handler)); + } + + pub fn get_rooms(&self) -> Vec { + self.rooms + .blocking_lock() + .list_rooms() + .iter() + .map(|r| (*r).clone()) + .collect() + } + + pub async fn send_message(&self, room_id: &str, message: &str) -> anyhow::Result { + let client = self + .client + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Bot not started"))?; + let rid: OwnedRoomId = room_id + .parse() + .map_err(|e| anyhow::anyhow!("Invalid room ID: {}", e))?; + let room = client + .get_room(&rid) + .ok_or_else(|| anyhow::anyhow!("Room not found: {}", room_id))?; + let content = + matrix_sdk::ruma::events::room::message::RoomMessageEventContent::text_plain(message); + let response = room + .send(content) + .await + .map_err(|e| anyhow::anyhow!("Failed to send message: {}", e))?; + Ok(response.event_id.to_string()) + } + + pub async fn send_notice(&self, room_id: &str, message: &str) -> anyhow::Result { + let client = self + .client + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Bot not started"))?; + let rid: OwnedRoomId = room_id + .parse() + .map_err(|e| anyhow::anyhow!("Invalid room ID: {}", e))?; + let room = client + .get_room(&rid) + .ok_or_else(|| anyhow::anyhow!("Room not found: {}", room_id))?; + let content = + matrix_sdk::ruma::events::room::message::RoomMessageEventContent::notice_plain(message); + let response = room + .send(content) + .await + .map_err(|e| anyhow::anyhow!("Failed to send notice: {}", e))?; + Ok(response.event_id.to_string()) + } + + pub async fn send_emote(&self, room_id: &str, message: &str) -> anyhow::Result { + let client = self + .client + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Bot not started"))?; + let rid: OwnedRoomId = room_id + .parse() + .map_err(|e| anyhow::anyhow!("Invalid room ID: {}", e))?; + let room = client + .get_room(&rid) + .ok_or_else(|| anyhow::anyhow!("Room not found: {}", room_id))?; + let content = RoomMessageEventContent::new(MessageType::Emote( + EmoteMessageEventContent::plain(message), + )); + let response = room + .send(content) + .await + .map_err(|e| anyhow::anyhow!("Failed to send emote: {}", e))?; + Ok(response.event_id.to_string()) + } + + pub async fn edit_message( + &self, + room_id: &str, + event_id: &str, + new_body: &str, + ) -> anyhow::Result { + let client = self + .client + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Bot not started"))?; + let rid: OwnedRoomId = room_id + .parse() + .map_err(|e| anyhow::anyhow!("Invalid room ID: {}", e))?; + let room = client + .get_room(&rid) + .ok_or_else(|| anyhow::anyhow!("Room not found: {}", room_id))?; + let eid: matrix_sdk::ruma::OwnedEventId = event_id + .parse() + .map_err(|e| anyhow::anyhow!("Invalid event ID: {}", e))?; + let new_content = RoomMessageEventContentWithoutRelation::text_plain(new_body); + let replaces = Replacement::new(eid, new_content); + let mut content = RoomMessageEventContent::text_plain(new_body); + content.relates_to = Some(Relation::Replacement(replaces)); + let response = room + .send(content) + .await + .map_err(|e| anyhow::anyhow!("Failed to edit message: {}", e))?; + Ok(response.event_id.to_string()) + } + + pub async fn delete_message( + &self, + room_id: &str, + event_id: &str, + reason: Option<&str>, + ) -> anyhow::Result<()> { + let client = self + .client + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Bot not started"))?; + let rid: OwnedRoomId = room_id + .parse() + .map_err(|e| anyhow::anyhow!("Invalid room ID: {}", e))?; + let room = client + .get_room(&rid) + .ok_or_else(|| anyhow::anyhow!("Room not found: {}", room_id))?; + let eid: matrix_sdk::ruma::OwnedEventId = event_id + .parse() + .map_err(|e| anyhow::anyhow!("Invalid event ID: {}", e))?; + room.redact(&eid, reason, None) + .await + .map_err(|e| anyhow::anyhow!("Failed to delete message: {}", e))?; + Ok(()) + } + + pub async fn react(&self, room_id: &str, event_id: &str, key: &str) -> anyhow::Result { + let client = self + .client + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Bot not started"))?; + let rid: OwnedRoomId = room_id + .parse() + .map_err(|e| anyhow::anyhow!("Invalid room ID: {}", e))?; + let room = client + .get_room(&rid) + .ok_or_else(|| anyhow::anyhow!("Room not found: {}", room_id))?; + let eid: matrix_sdk::ruma::OwnedEventId = event_id + .parse() + .map_err(|e| anyhow::anyhow!("Invalid event ID: {}", e))?; + let annotation = Annotation::new(eid.clone(), key.to_string()); + let react_content = ReactionEventContent::new(annotation); + let response = room + .send(react_content) + .await + .map_err(|e| anyhow::anyhow!("Failed to react: {}", e))?; + Ok(response.event_id.to_string()) + } + + pub async fn upload_media( + &self, + room_id: &str, + data: Vec, + content_type: &str, + filename: &str, + ) -> anyhow::Result { + let client = self + .client + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Bot not started"))?; + let rid: OwnedRoomId = room_id + .parse() + .map_err(|e| anyhow::anyhow!("Invalid room ID: {}", e))?; + let room = client + .get_room(&rid) + .ok_or_else(|| anyhow::anyhow!("Room not found: {}", room_id))?; + let mime_type: mime::Mime = content_type + .parse() + .map_err(|e| anyhow::anyhow!("Invalid MIME type: {}", e))?; + let upload = client + .media() + .upload(&mime_type, data) + .await + .map_err(|e| anyhow::anyhow!("Upload failed: {}", e))?; + let content = if mime_type.type_() == mime::IMAGE { + RoomMessageEventContent::new(MessageType::Image(ImageMessageEventContent::plain( + filename.to_string(), + upload.content_uri, + ))) + } else { + RoomMessageEventContent::new(MessageType::File(FileMessageEventContent::plain( + filename.to_string(), + upload.content_uri, + ))) + }; + let response = room + .send(content) + .await + .map_err(|e| anyhow::anyhow!("Failed to send media: {}", e))?; + Ok(response.event_id.to_string()) + } + + pub async fn join_room(&self, room_id_or_alias: &str) -> anyhow::Result { + let client = self + .client + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Bot not started"))?; + let alias: matrix_sdk::ruma::OwnedRoomOrAliasId = room_id_or_alias + .parse() + .map_err(|e| anyhow::anyhow!("Invalid room alias: {}", e))?; + let room = client + .join_room_by_id_or_alias(&alias, &[]) + .await + .map_err(|e| anyhow::anyhow!("Failed to join room: {}", e))?; + Ok(room.room_id().to_string()) + } + + pub async fn leave_room(&self, room_id: &str) -> anyhow::Result<()> { + let client = self + .client + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Bot not started"))?; + let rid: OwnedRoomId = room_id + .parse() + .map_err(|e| anyhow::anyhow!("Invalid room ID: {}", e))?; + let room = client + .get_room(&rid) + .ok_or_else(|| anyhow::anyhow!("Room not found: {}", room_id))?; + room.leave() + .await + .map_err(|e| anyhow::anyhow!("Failed to leave room: {}", e))?; + Ok(()) + } + + pub async fn get_room_members(&self, room_id: &str) -> anyhow::Result> { + let client = self + .client + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Bot not started"))?; + let rid: OwnedRoomId = room_id + .parse() + .map_err(|e| anyhow::anyhow!("Invalid room ID: {}", e))?; + let room = client + .get_room(&rid) + .ok_or_else(|| anyhow::anyhow!("Room not found: {}", room_id))?; + let members = room + .members(matrix_sdk::RoomMemberships::JOIN) + .await + .map_err(|e| anyhow::anyhow!("Failed to get members: {}", e))?; + Ok(members.iter().map(|m| m.user_id().to_string()).collect()) + } + + pub async fn set_room_name(&self, room_id: &str, name: &str) -> anyhow::Result<()> { + let client = self + .client + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Bot not started"))?; + let rid: OwnedRoomId = room_id + .parse() + .map_err(|e| anyhow::anyhow!("Invalid room ID: {}", e))?; + let room = client + .get_room(&rid) + .ok_or_else(|| anyhow::anyhow!("Room not found: {}", room_id))?; + room.set_name(name.to_string()) + .await + .map_err(|e| anyhow::anyhow!("Failed to set room name: {}", e))?; + Ok(()) + } + + pub async fn set_room_topic(&self, room_id: &str, topic: &str) -> anyhow::Result<()> { + let client = self + .client + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Bot not started"))?; + let rid: OwnedRoomId = room_id + .parse() + .map_err(|e| anyhow::anyhow!("Invalid room ID: {}", e))?; + let room = client + .get_room(&rid) + .ok_or_else(|| anyhow::anyhow!("Room not found: {}", room_id))?; + room.set_room_topic(topic) + .await + .map_err(|e| anyhow::anyhow!("Failed to set room topic: {}", e))?; + Ok(()) + } + + pub async fn get_display_name(&self, user_id: &str) -> anyhow::Result> { + let client = self + .client + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Bot not started"))?; + let uid: matrix_sdk::ruma::OwnedUserId = user_id + .parse() + .map_err(|e| anyhow::anyhow!("Invalid user ID: {}", e))?; + let profile = client + .get_profile(&uid) + .await + .map_err(|e| anyhow::anyhow!("Failed to get profile: {}", e))?; + Ok(profile.displayname) + } + + pub async fn set_display_name(&self, name: &str) -> anyhow::Result<()> { + let client = self + .client + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Bot not started"))?; + client + .account() + .set_display_name(Some(name)) + .await + .map_err(|e| anyhow::anyhow!("Failed to set display name: {}", e))?; + Ok(()) + } + + pub async fn send_reply( + &self, + room_id: &str, + reply_to_event_id: &str, + message: &str, + ) -> anyhow::Result { + let client = self + .client + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Bot not started"))?; + let rid: OwnedRoomId = room_id + .parse() + .map_err(|e| anyhow::anyhow!("Invalid room ID: {}", e))?; + let room = client + .get_room(&rid) + .ok_or_else(|| anyhow::anyhow!("Room not found: {}", room_id))?; + let eid: matrix_sdk::ruma::OwnedEventId = reply_to_event_id + .parse() + .map_err(|e| anyhow::anyhow!("Invalid event ID: {}", e))?; + let mut content = RoomMessageEventContent::text_plain(message); + let reply = InReplyTo::new(eid); + content.relates_to = Some(Relation::Reply { in_reply_to: reply }); + let response = room + .send(content) + .await + .map_err(|e| anyhow::anyhow!("Failed to send reply: {}", e))?; + Ok(response.event_id.to_string()) + } + + pub async fn start(&mut self) -> anyhow::Result<()> { let client = Client::builder() .homeserver_url(&self.homeserver) .build() .await?; - { - let mut guard = self.client.write().await; - *guard = Some(client.clone()); - } - - let auth = self.auth.read().await; client .matrix_auth() - .login_username(&auth.username, &auth.password) + .login_username(&self.auth.username, &self.auth.password) .send() .await?; - tracing::info!("Bot logged in as {}", auth.username); - drop(auth); + tracing::info!("Bot logged in as {}", self.auth.username); + + self.client = Some(client.clone()); + + let (shutdown_tx, mut shutdown_rx) = watch::channel(false); + self.shutdown_tx = Some(shutdown_tx); + + let commands = self.commands.clone(); + let event_handlers = self.event_handlers.clone(); + let rooms = self.rooms.clone(); + + tracing::info!("Bot starting sync loop with manual event processing..."); - let mut sync_token: Option = None; loop { - let mut settings = matrix_sdk::config::SyncSettings::new(); - if let Some(token) = sync_token.as_ref() { - settings = settings.token(token.clone()); - } - match client.sync_once(settings).await { - Ok(response) => { - sync_token = Some(response.next_batch); + tokio::select! { + result = client.sync_once(SyncSettings::new()) => { + match result { + Ok(response) => { + { + let eh = event_handlers.lock().await; + eh.dispatch(BotEvent::SyncComplete); + } - let handler = self.event_handler.read().await; - handler.dispatch("sync"); - drop(handler); + for (room_id, joined) in &response.rooms.join { + let _room = match client.get_room(room_id) { + Some(r) => r, + None => continue, + }; - let rooms = client.joined_rooms(); - let mut room_mgr = self.room_manager.write().await; - for room in rooms { - let name = room.display_name().await.map(|n| n.to_string()).unwrap_or_default(); - room_mgr.add_room(crate::room::RoomInfo { - room_id: room.room_id().to_string(), - name, - is_encrypted: room.is_encrypted().await.unwrap_or(false), - }); + let room_id_str = room_id.to_string(); + + for event in &joined.timeline.events { + let raw_json: serde_json::Value = match event.event.deserialize_as() { + Ok(v) => v, + Err(_) => continue, + }; + + let event_type = raw_json.get("type").and_then(|v| v.as_str()).unwrap_or(""); + let sender = raw_json.get("sender").and_then(|v| v.as_str()).unwrap_or(""); + let event_id = event.event_id().map(|e| e.to_string()).unwrap_or_default(); + let content = raw_json.get("content").unwrap_or(&serde_json::Value::Null); + + if event_type == "m.room.message" { + let body = content.get("body").and_then(|v| v.as_str()).unwrap_or(""); + let msgtype = content.get("msgtype").and_then(|v| v.as_str()).unwrap_or("m.text"); + let url = content.get("url").and_then(|v| v.as_str()).map(|s| s.to_string()); + + if msgtype == "m.image" { + let eh = event_handlers.lock().await; + eh.dispatch(BotEvent::Image { + room_id: room_id_str.clone(), + event_id: event_id.clone(), + sender: sender.to_string(), + body: body.to_string(), + url: url.unwrap_or_default(), + }); + drop(eh); + } else if msgtype == "m.file" || msgtype == "m.video" || msgtype == "m.audio" { + let eh = event_handlers.lock().await; + eh.dispatch(BotEvent::File { + room_id: room_id_str.clone(), + event_id: event_id.clone(), + sender: sender.to_string(), + body: body.to_string(), + url: url.unwrap_or_default(), + }); + drop(eh); + } else if !body.is_empty() { + let eh = event_handlers.lock().await; + eh.dispatch(BotEvent::Message { + room_id: room_id_str.clone(), + event_id: event_id.clone(), + sender: sender.to_string(), + body: body.to_string(), + }); + drop(eh); + + let reg = commands.lock().await; + reg.parse_and_execute(body, sender, &room_id_str); + } + } else if event_type == "m.reaction" { + let key = content.get("m.relates_to") + .and_then(|r| r.get("key")) + .and_then(|v| v.as_str()) + .unwrap_or(""); + let relates_to = content.get("m.relates_to") + .and_then(|r| r.get("event_id")) + .and_then(|v| v.as_str()) + .unwrap_or(""); + + let eh = event_handlers.lock().await; + eh.dispatch(BotEvent::Reaction { + room_id: room_id_str.clone(), + event_id: event_id.clone(), + sender: sender.to_string(), + key: key.to_string(), + relates_to: relates_to.to_string(), + }); + } else if event_type == "m.room.redaction" { + let redacts = raw_json.get("redacts").and_then(|v| v.as_str()).unwrap_or(""); + let eh = event_handlers.lock().await; + eh.dispatch(BotEvent::Redaction { + room_id: room_id_str.clone(), + event_id: event_id.clone(), + sender: sender.to_string(), + redacts: redacts.to_string(), + }); + } else if event_type == "m.room.member" { + let membership = content.get("membership").and_then(|v| v.as_str()).unwrap_or(""); + + let eh = event_handlers.lock().await; + if membership == "join" { + eh.dispatch(BotEvent::MemberJoined { + room_id: room_id_str.clone(), + user_id: sender.to_string(), + }); + } else if membership == "leave" || membership == "ban" { + eh.dispatch(BotEvent::MemberLeft { + room_id: room_id_str.clone(), + user_id: sender.to_string(), + }); + } + } + } + } + + let joined_rooms = client.joined_rooms(); + let mut room_mgr = rooms.lock().await; + room_mgr.rooms.clear(); + for room in &joined_rooms { + let room_id = room.room_id().to_string(); + let name = room.display_name().await + .map(|n| n.to_string()) + .unwrap_or_else(|_| room_id.clone()); + let is_encrypted = room.is_encrypted().await.unwrap_or(false); + room_mgr.add_room(RoomInfo::new(room_id, name, is_encrypted)); + } + } + Err(e) => { + tracing::error!("Sync error: {}", e); + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + } } } - Err(e) => { - tracing::error!("Sync error: {}", e); - tokio::time::sleep(std::time::Duration::from_secs(5)).await; + _ = shutdown_rx.changed() => { + tracing::info!("Bot shutting down..."); + break; } } } - } - - pub async fn stop(&self) -> anyhow::Result<()> { - let mut guard = self.client.write().await; - *guard = None; - Ok(()) - } - - pub async fn send_message(&self, room_id: &str, message: &str) -> anyhow::Result<()> { - let guard = self.client.read().await; - let client = guard.as_ref().ok_or(anyhow::anyhow!("Not connected"))?; - - let rid = matrix_sdk::ruma::room_id!(room_id) - .map_err(|_| anyhow::anyhow!("Invalid room ID"))?; - let room = client.get_room(&rid) - .ok_or(anyhow::anyhow!("Room not found"))?; - - let content = matrix_sdk::ruma::events::room::message::RoomMessageEventContent::text_plain(message); - let txn_id = matrix_sdk::ruma::TransactionId::new(); - room.send(content, Some(&txn_id)).await?; Ok(()) } - pub async fn on_command(&self, name: &str, handler: Box) { - let mut commands = self.commands.write().await; - commands.register(name, handler); + pub fn stop(&mut self) { + if let Some(tx) = self.shutdown_tx.take() { + let _ = tx.send(true); + } + self.client = None; } +} - pub async fn on_event(&self, handler: Box) { - let mut event_handler = self.event_handler.write().await; - event_handler.add_handler(handler); +impl Drop for BotClient { + fn drop(&mut self) { + self.stop(); } - - pub async fn get_rooms(&self) -> Vec { - let room_mgr = self.room_manager.read().await; - room_mgr.list_rooms().into_iter().cloned().collect() - } - - pub async fn handle_message(&self, room_id: &str, sender: &str, body: &str) { - let commands = self.commands.read().await; - commands.parse_and_execute(body, sender); - } -} \ No newline at end of file +} diff --git a/bot-sdk/src/commands.rs b/bot-sdk/src/commands.rs index 4272f2e..31e6a25 100644 --- a/bot-sdk/src/commands.rs +++ b/bot-sdk/src/commands.rs @@ -1,12 +1,31 @@ use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; -type CommandHandler = Box; +#[derive(Clone, Debug)] +pub struct CommandContext { + pub room_id: String, + pub sender: String, + pub args: String, + pub command: String, +} + +pub type CommandHandler = Arc; pub struct CommandRegistry { commands: HashMap, prefix: String, } +impl Default for CommandRegistry { + fn default() -> Self { + Self { + commands: HashMap::new(), + prefix: "!".to_string(), + } + } +} + impl CommandRegistry { pub fn new() -> Self { Self { @@ -15,9 +34,11 @@ impl CommandRegistry { } } - pub fn with_prefix(mut self, prefix: &str) -> Self { - self.prefix = prefix.to_string(); - self + pub fn with_prefix(prefix: &str) -> Self { + Self { + commands: HashMap::new(), + prefix: prefix.to_string(), + } } pub fn set_prefix(&mut self, prefix: &str) { @@ -32,18 +53,24 @@ impl CommandRegistry { self.commands.remove(name); } - pub fn parse_and_execute(&self, message: &str, sender: &str) { + pub fn parse_and_execute(&self, message: &str, sender: &str, room_id: &str) { if !message.starts_with(&self.prefix) { return; } let content = &message[self.prefix.len()..]; - let parts: Vec<&str> = content.splitn(2, ' ').collect(); + let parts: Vec<&str> = content.splitn(2, char::is_whitespace).collect(); let command = parts[0]; - let args = parts.get(1).unwrap_or(&""); + let args = parts.get(1).unwrap_or(&"").to_string(); if let Some(handler) = self.commands.get(command) { - handler(args, sender); + let ctx = CommandContext { + room_id: room_id.to_string(), + sender: sender.to_string(), + args, + command: command.to_string(), + }; + handler(ctx); } } @@ -54,4 +81,142 @@ impl CommandRegistry { pub fn has_command(&self, name: &str) -> bool { self.commands.contains_key(name) } -} \ No newline at end of file +} + +pub type SharedCommandRegistry = Arc>; + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicUsize, Ordering}; + + #[test] + fn new_creates_empty_registry() { + let reg = CommandRegistry::new(); + assert!(reg.list_commands().is_empty()); + } + + #[test] + fn default_creates_empty_registry() { + let reg = CommandRegistry::default(); + assert!(reg.list_commands().is_empty()); + } + + #[test] + fn with_prefix_sets_custom_prefix() { + let mut reg = CommandRegistry::with_prefix("~"); + let counter = Arc::new(AtomicUsize::new(0)); + let counter_clone = counter.clone(); + reg.register( + "test", + Arc::new(move |_| { + counter_clone.fetch_add(1, Ordering::SeqCst); + }), + ); + reg.parse_and_execute("~test", "user", "!room"); + assert_eq!(counter.load(Ordering::SeqCst), 1); + } + + #[test] + fn register_and_execute_command() { + let mut reg = CommandRegistry::new(); + let counter = Arc::new(AtomicUsize::new(0)); + let counter_clone = counter.clone(); + reg.register( + "hello", + Arc::new(move |_| { + counter_clone.fetch_add(1, Ordering::SeqCst); + }), + ); + reg.parse_and_execute("!hello", "user", "room"); + assert_eq!(counter.load(Ordering::SeqCst), 1); + } + + #[test] + fn register_command_with_args() { + let mut reg = CommandRegistry::new(); + let captured = Arc::new(Mutex::new(String::new())); + let captured_clone = captured.clone(); + reg.register( + "echo", + Arc::new(move |ctx| { + let _ = captured_clone.try_lock().map(|mut g| *g = ctx.args); + }), + ); + reg.parse_and_execute("!echo hello world", "user", "room"); + assert_eq!(captured_args(&captured), "hello world"); + } + + #[test] + fn ignore_message_without_prefix() { + let reg = CommandRegistry::new(); + let counter = Arc::new(AtomicUsize::new(0)); + let mut reg = reg; + let counter_clone = counter.clone(); + reg.register( + "hello", + Arc::new(move |_| { + counter_clone.fetch_add(1, Ordering::SeqCst); + }), + ); + reg.parse_and_execute("hello", "user", "room"); + assert_eq!(counter.load(Ordering::SeqCst), 0); + } + + #[test] + fn ignore_unknown_command() { + let reg = CommandRegistry::new(); + let counter = Arc::new(AtomicUsize::new(0)); + let mut reg = reg; + let counter_clone = counter.clone(); + reg.register( + "hello", + Arc::new(move |_| { + counter_clone.fetch_add(1, Ordering::SeqCst); + }), + ); + reg.parse_and_execute("!unknown", "user", "room"); + assert_eq!(counter.load(Ordering::SeqCst), 0); + } + + #[test] + fn unregister_removes_command() { + let mut reg = CommandRegistry::new(); + reg.register("hello", Arc::new(|_| {})); + assert!(reg.has_command("hello")); + reg.unregister("hello"); + assert!(!reg.has_command("hello")); + } + + #[test] + fn set_prefix_changes_prefix() { + let mut reg = CommandRegistry::new(); + reg.set_prefix("~"); + let counter = Arc::new(AtomicUsize::new(0)); + let counter_clone = counter.clone(); + reg.register( + "test", + Arc::new(move |_| { + counter_clone.fetch_add(1, Ordering::SeqCst); + }), + ); + reg.parse_and_execute("~test", "user", "room"); + assert_eq!(counter.load(Ordering::SeqCst), 1); + reg.parse_and_execute("!test", "user", "room"); + assert_eq!(counter.load(Ordering::SeqCst), 1); + } + + #[test] + fn list_commands_returns_all_names() { + let mut reg = CommandRegistry::new(); + reg.register("hello", Arc::new(|_| {})); + reg.register("ping", Arc::new(|_| {})); + let mut names = reg.list_commands(); + names.sort(); + assert_eq!(names, vec!["hello", "ping"]); + } + + fn captured_args(captured: &Arc>) -> String { + captured.try_lock().unwrap().clone() + } +} diff --git a/bot-sdk/src/event.rs b/bot-sdk/src/event.rs index 4db18e8..970aa44 100644 --- a/bot-sdk/src/event.rs +++ b/bot-sdk/src/event.rs @@ -1,25 +1,187 @@ -type EventCallback = Box; +use std::sync::Arc; +use tokio::sync::Mutex; +#[derive(Clone, Debug)] +pub enum BotEvent { + Message { + room_id: String, + event_id: String, + sender: String, + body: String, + }, + Image { + room_id: String, + event_id: String, + sender: String, + body: String, + url: String, + }, + File { + room_id: String, + event_id: String, + sender: String, + body: String, + url: String, + }, + Reaction { + room_id: String, + event_id: String, + sender: String, + key: String, + relates_to: String, + }, + Redaction { + room_id: String, + event_id: String, + sender: String, + redacts: String, + }, + MemberJoined { + room_id: String, + user_id: String, + }, + MemberLeft { + room_id: String, + user_id: String, + }, + RoomJoined { + room_id: String, + room_name: String, + }, + RoomLeft { + room_id: String, + }, + SyncComplete, +} + +type EventCallback = Arc; + +#[derive(Default)] pub struct EventHandler { handlers: Vec, } impl EventHandler { pub fn new() -> Self { - Self { handlers: Vec::new() } + Self { + handlers: Vec::new(), + } } pub fn add_handler(&mut self, handler: EventCallback) { self.handlers.push(handler); } - pub fn dispatch(&self, event: &str) { + pub fn dispatch(&self, event: BotEvent) { for handler in &self.handlers { - handler(event); + handler(event.clone()); } } pub fn handler_count(&self) -> usize { self.handlers.len() } -} \ No newline at end of file +} + +pub type SharedEventHandler = Arc>; + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicUsize, Ordering}; + + #[test] + fn new_creates_empty_handler() { + let handler = EventHandler::new(); + assert_eq!(handler.handler_count(), 0); + } + + #[test] + fn default_creates_empty_handler() { + let handler = EventHandler::default(); + assert_eq!(handler.handler_count(), 0); + } + + #[test] + fn add_handler_increments_count() { + let mut handler = EventHandler::new(); + handler.add_handler(Arc::new(|_| {})); + assert_eq!(handler.handler_count(), 1); + handler.add_handler(Arc::new(|_| {})); + assert_eq!(handler.handler_count(), 2); + } + + #[test] + fn dispatch_calls_all_handlers() { + let mut handler = EventHandler::new(); + let counter = Arc::new(AtomicUsize::new(0)); + let c1 = counter.clone(); + handler.add_handler(Arc::new(move |_| { + c1.fetch_add(1, Ordering::SeqCst); + })); + let c2 = counter.clone(); + handler.add_handler(Arc::new(move |_| { + c2.fetch_add(1, Ordering::SeqCst); + })); + handler.dispatch(BotEvent::SyncComplete); + assert_eq!(counter.load(Ordering::SeqCst), 2); + } + + #[test] + fn dispatch_message_event() { + let mut handler = EventHandler::new(); + let captured = Arc::new(Mutex::new(String::new())); + let captured_clone = captured.clone(); + handler.add_handler(Arc::new(move |event| { + if let BotEvent::Message { body, .. } = event { + let _ = captured_clone.try_lock().map(|mut g| *g = body); + } + })); + handler.dispatch(BotEvent::Message { + room_id: "!room:server".to_string(), + event_id: "$event".to_string(), + sender: "@user:server".to_string(), + body: "hello".to_string(), + }); + assert_eq!(captured.try_lock().unwrap().as_str(), "hello"); + } + + #[test] + fn dispatch_reaction_event() { + let mut handler = EventHandler::new(); + let counter = Arc::new(AtomicUsize::new(0)); + let c = counter.clone(); + handler.add_handler(Arc::new(move |event| { + if let BotEvent::Reaction { key, .. } = event { + if key == "👍" { + c.fetch_add(1, Ordering::SeqCst); + } + } + })); + handler.dispatch(BotEvent::Reaction { + room_id: "!room:server".to_string(), + event_id: "$event".to_string(), + sender: "@user:server".to_string(), + key: "👍".to_string(), + relates_to: "$orig".to_string(), + }); + assert_eq!(counter.load(Ordering::SeqCst), 1); + } + + #[test] + fn dispatch_member_joined_event() { + let mut handler = EventHandler::new(); + let captured = Arc::new(Mutex::new(String::new())); + let c = captured.clone(); + handler.add_handler(Arc::new(move |event| { + if let BotEvent::MemberJoined { user_id, .. } = event { + let _ = c.try_lock().map(|mut g| *g = user_id); + } + })); + handler.dispatch(BotEvent::MemberJoined { + room_id: "!room:server".to_string(), + user_id: "@alice:server".to_string(), + }); + assert_eq!(captured.try_lock().unwrap().as_str(), "@alice:server"); + } +} diff --git a/bot-sdk/src/lib.rs b/bot-sdk/src/lib.rs index 0b09cdd..d45bfde 100644 --- a/bot-sdk/src/lib.rs +++ b/bot-sdk/src/lib.rs @@ -1,8 +1,41 @@ +//! EifelDC Bot SDK — Build Matrix bots for the EifelDC platform. +//! +//! # Quick Start +//! ```no_run +//! use std::sync::Arc; +//! use eifeldc_bot_sdk::{BotClient, BotEvent, CommandContext}; +//! +//! #[tokio::main] +//! async fn main() -> anyhow::Result<()> { +//! let mut bot = BotClient::new("https://matrix.example.org") +//! .with_auth("botuser", "botpassword"); +//! +//! bot.on_event(|event| { +//! match event { +//! BotEvent::Message { room_id, sender, body, .. } => { +//! println!("{} in {}: {}", sender, room_id, body); +//! } +//! _ => {} +//! } +//! }); +//! +//! bot.on_command("hello", Arc::new(|ctx: CommandContext| { +//! println!("Hello command from {} in {}!", ctx.sender, ctx.room_id); +//! })); +//! +//! bot.start().await?; +//! Ok(()) +//! } +//! ``` + +pub mod auth; pub mod client; pub mod commands; pub mod event; pub mod room; -pub mod auth; +pub use auth::BotAuth; pub use client::BotClient; -pub use auth::BotAuth; \ No newline at end of file +pub use commands::{CommandContext, CommandHandler, CommandRegistry}; +pub use event::{BotEvent, EventHandler}; +pub use room::{RoomInfo, RoomManager}; diff --git a/bot-sdk/src/room.rs b/bot-sdk/src/room.rs index f84e89f..b4688c3 100644 --- a/bot-sdk/src/room.rs +++ b/bot-sdk/src/room.rs @@ -1,19 +1,34 @@ use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct RoomInfo { pub room_id: String, pub name: String, pub is_encrypted: bool, } +impl RoomInfo { + pub fn new(room_id: String, name: String, is_encrypted: bool) -> Self { + Self { + room_id, + name, + is_encrypted, + } + } +} + +#[derive(Default)] pub struct RoomManager { - rooms: HashMap, + pub rooms: HashMap, } impl RoomManager { pub fn new() -> Self { - Self { rooms: HashMap::new() } + Self { + rooms: HashMap::new(), + } } pub fn add_room(&mut self, room: RoomInfo) { @@ -35,4 +50,112 @@ impl RoomManager { pub fn room_count(&self) -> usize { self.rooms.len() } -} \ No newline at end of file +} + +pub type SharedRoomManager = Arc>; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_creates_empty_manager() { + let mgr = RoomManager::new(); + assert_eq!(mgr.room_count(), 0); + } + + #[test] + fn default_creates_empty_manager() { + let mgr = RoomManager::default(); + assert_eq!(mgr.room_count(), 0); + } + + #[test] + fn add_room_inserts_room() { + let mut mgr = RoomManager::new(); + mgr.add_room(RoomInfo::new( + "!room1:server".into(), + "Room One".into(), + false, + )); + assert_eq!(mgr.room_count(), 1); + } + + #[test] + fn add_multiple_rooms() { + let mut mgr = RoomManager::new(); + mgr.add_room(RoomInfo::new("!r1:server".into(), "A".into(), false)); + mgr.add_room(RoomInfo::new("!r2:server".into(), "B".into(), true)); + assert_eq!(mgr.room_count(), 2); + } + + #[test] + fn get_room_returns_correct_room() { + let mut mgr = RoomManager::new(); + mgr.add_room(RoomInfo::new( + "!room1:server".into(), + "Room One".into(), + false, + )); + let room = mgr.get_room("!room1:server").unwrap(); + assert_eq!(room.name, "Room One"); + assert!(!room.is_encrypted); + } + + #[test] + fn get_room_returns_none_for_missing() { + let mgr = RoomManager::new(); + assert!(mgr.get_room("!nonexistent:server").is_none()); + } + + #[test] + fn remove_room_deletes_room() { + let mut mgr = RoomManager::new(); + mgr.add_room(RoomInfo::new( + "!room1:server".into(), + "Room One".into(), + false, + )); + assert_eq!(mgr.room_count(), 1); + mgr.remove_room("!room1:server"); + assert_eq!(mgr.room_count(), 0); + } + + #[test] + fn remove_nonexistent_room_is_noop() { + let mut mgr = RoomManager::new(); + mgr.add_room(RoomInfo::new( + "!room1:server".into(), + "Room One".into(), + false, + )); + mgr.remove_room("!nonexistent:server"); + assert_eq!(mgr.room_count(), 1); + } + + #[test] + fn add_room_overwrites_existing() { + let mut mgr = RoomManager::new(); + mgr.add_room(RoomInfo::new( + "!room1:server".into(), + "Old Name".into(), + false, + )); + mgr.add_room(RoomInfo::new( + "!room1:server".into(), + "New Name".into(), + true, + )); + assert_eq!(mgr.room_count(), 1); + assert_eq!(mgr.get_room("!room1:server").unwrap().name, "New Name"); + assert!(mgr.get_room("!room1:server").unwrap().is_encrypted); + } + + #[test] + fn list_rooms_returns_all() { + let mut mgr = RoomManager::new(); + mgr.add_room(RoomInfo::new("!r1:server".into(), "A".into(), false)); + mgr.add_room(RoomInfo::new("!r2:server".into(), "B".into(), true)); + assert_eq!(mgr.list_rooms().len(), 2); + } +} diff --git a/client/Cargo.toml b/client/Cargo.toml index f66ee3e..3aa72be 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,8 +4,6 @@ version = "0.1.0" edition = "2021" [dependencies] -matrix-sdk = { workspace = true } -matrix-sdk-base = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } @@ -15,15 +13,9 @@ tracing = { workspace = true } tracing-subscriber = { workspace = true } reqwest = { workspace = true } url = { workspace = true } -tauri = { version = "1", features = ["shell-open", "dialog-all"] } -tauri-plugin-oauth = "2.0" -sled = "0.34" -futures = "0.3" -async-trait = "0.1" -chrono = { version = "0.4", features = ["serde"] } -rand = "0.8" -base64 = "0.22" -sha2 = "0.10" +tauri = { version = "2", features = [] } +tauri-plugin-shell = "2" +tauri-plugin-dialog = "2" [build-dependencies] -tauri-build = { version = "1", features = [] } \ No newline at end of file +tauri-build = { version = "2", features = [] } \ No newline at end of file diff --git a/client/build.rs b/client/build.rs index 82d481c..d860e1e 100644 --- a/client/build.rs +++ b/client/build.rs @@ -1,3 +1,3 @@ fn main() { tauri_build::build() -} \ No newline at end of file +} diff --git a/client/gen/schemas/acl-manifests.json b/client/gen/schemas/acl-manifests.json new file mode 100644 index 0000000..9a894c2 --- /dev/null +++ b/client/gen/schemas/acl-manifests.json @@ -0,0 +1 @@ +{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)","commands":{"allow":["message"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)","commands":{"allow":["message"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)","commands":{"allow":[],"deny":["message"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)","commands":{"allow":[],"deny":["message"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"shell":{"default_permission":{"identifier":"default","description":"This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n","permissions":["allow-open"]},"permissions":{"allow-execute":{"identifier":"allow-execute","description":"Enables the execute command without any pre-configured scope.","commands":{"allow":["execute"],"deny":[]}},"allow-kill":{"identifier":"allow-kill","description":"Enables the kill command without any pre-configured scope.","commands":{"allow":["kill"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-spawn":{"identifier":"allow-spawn","description":"Enables the spawn command without any pre-configured scope.","commands":{"allow":["spawn"],"deny":[]}},"allow-stdin-write":{"identifier":"allow-stdin-write","description":"Enables the stdin_write command without any pre-configured scope.","commands":{"allow":["stdin_write"],"deny":[]}},"deny-execute":{"identifier":"deny-execute","description":"Denies the execute command without any pre-configured scope.","commands":{"allow":[],"deny":["execute"]}},"deny-kill":{"identifier":"deny-kill","description":"Denies the kill command without any pre-configured scope.","commands":{"allow":[],"deny":["kill"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-spawn":{"identifier":"deny-spawn","description":"Denies the spawn command without any pre-configured scope.","commands":{"allow":[],"deny":["spawn"]}},"deny-stdin-write":{"identifier":"deny-stdin-write","description":"Denies the stdin_write command without any pre-configured scope.","commands":{"allow":[],"deny":["stdin_write"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"cmd":{"description":"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"}},"required":["cmd","name"],"type":"object"},{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"},"sidecar":{"description":"If this command is a sidecar command.","type":"boolean"}},"required":["name","sidecar"],"type":"object"}],"definitions":{"ShellScopeEntryAllowedArg":{"anyOf":[{"description":"A non-configurable argument that is passed to the command in the order it was specified.","type":"string"},{"additionalProperties":false,"description":"A variable that is set while calling the command from the webview API.","properties":{"raw":{"default":false,"description":"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.","type":"boolean"},"validator":{"description":"[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ","type":"string"}},"required":["validator"],"type":"object"}],"description":"A command argument allowed to be executed by the webview API."},"ShellScopeEntryAllowedArgs":{"anyOf":[{"description":"Use a simple boolean to allow all or disable all arguments to this command configuration.","type":"boolean"},{"description":"A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.","items":{"$ref":"#/definitions/ShellScopeEntryAllowedArg"},"type":"array"}],"description":"A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration."}},"description":"Shell scope entry.","title":"ShellScopeEntry"}}} \ No newline at end of file diff --git a/client/gen/schemas/capabilities.json b/client/gen/schemas/capabilities.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/client/gen/schemas/capabilities.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/client/gen/schemas/desktop-schema.json b/client/gen/schemas/desktop-schema.json new file mode 100644 index 0000000..634faec --- /dev/null +++ b/client/gen/schemas/desktop-schema.json @@ -0,0 +1,2630 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CapabilityFile", + "description": "Capability formats accepted in a capability file.", + "anyOf": [ + { + "description": "A single capability.", + "allOf": [ + { + "$ref": "#/definitions/Capability" + } + ] + }, + { + "description": "A list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + }, + { + "description": "A list of capabilities.", + "type": "object", + "required": [ + "capabilities" + ], + "properties": { + "capabilities": { + "description": "The list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + } + } + } + ], + "definitions": { + "Capability": { + "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", + "type": "object", + "required": [ + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", + "type": "string" + }, + "description": { + "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", + "default": "", + "type": "string" + }, + "remote": { + "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", + "anyOf": [ + { + "$ref": "#/definitions/CapabilityRemote" + }, + { + "type": "null" + } + ] + }, + "local": { + "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", + "default": true, + "type": "boolean" + }, + "windows": { + "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "webviews": { + "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "permissions": { + "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionEntry" + }, + "uniqueItems": true + }, + "platforms": { + "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "CapabilityRemote": { + "description": "Configuration for remote URLs that are associated with the capability.", + "type": "object", + "required": [ + "urls" + ], + "properties": { + "urls": { + "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionEntry": { + "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", + "anyOf": [ + { + "description": "Reference a permission or permission set by identifier.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + { + "description": "Reference a permission or permission set by identifier and extends its scope.", + "type": "object", + "allOf": [ + { + "if": { + "properties": { + "identifier": { + "anyOf": [ + { + "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`", + "type": "string", + "const": "shell:default", + "markdownDescription": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`" + }, + { + "description": "Enables the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-execute", + "markdownDescription": "Enables the execute command without any pre-configured scope." + }, + { + "description": "Enables the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-kill", + "markdownDescription": "Enables the kill command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-spawn", + "markdownDescription": "Enables the spawn command without any pre-configured scope." + }, + { + "description": "Enables the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-stdin-write", + "markdownDescription": "Enables the stdin_write command without any pre-configured scope." + }, + { + "description": "Denies the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-execute", + "markdownDescription": "Denies the execute command without any pre-configured scope." + }, + { + "description": "Denies the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-kill", + "markdownDescription": "Denies the kill command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-spawn", + "markdownDescription": "Denies the spawn command without any pre-configured scope." + }, + { + "description": "Denies the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-stdin-write", + "markdownDescription": "Denies the stdin_write command without any pre-configured scope." + } + ] + } + } + }, + "then": { + "properties": { + "allow": { + "items": { + "title": "ShellScopeEntry", + "description": "Shell scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "cmd", + "name" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "cmd": { + "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "type": "string" + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "name", + "sidecar" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + }, + "sidecar": { + "description": "If this command is a sidecar command.", + "type": "boolean" + } + }, + "additionalProperties": false + } + ] + } + }, + "deny": { + "items": { + "title": "ShellScopeEntry", + "description": "Shell scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "cmd", + "name" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "cmd": { + "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "type": "string" + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "name", + "sidecar" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + }, + "sidecar": { + "description": "If this command is a sidecar command.", + "type": "boolean" + } + }, + "additionalProperties": false + } + ] + } + } + } + }, + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + } + } + }, + { + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + } + ], + "required": [ + "identifier" + ] + } + ] + }, + "Identifier": { + "description": "Permission identifier", + "oneOf": [ + { + "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", + "type": "string", + "const": "core:default", + "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`", + "type": "string", + "const": "core:app:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`" + }, + { + "description": "Enables the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-hide", + "markdownDescription": "Enables the app_hide command without any pre-configured scope." + }, + { + "description": "Enables the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-show", + "markdownDescription": "Enables the app_show command without any pre-configured scope." + }, + { + "description": "Enables the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-bundle-type", + "markdownDescription": "Enables the bundle_type command without any pre-configured scope." + }, + { + "description": "Enables the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-default-window-icon", + "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." + }, + { + "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-fetch-data-store-identifiers", + "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Enables the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-identifier", + "markdownDescription": "Enables the identifier command without any pre-configured scope." + }, + { + "description": "Enables the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-name", + "markdownDescription": "Enables the name command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-data-store", + "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." + }, + { + "description": "Enables the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-listener", + "markdownDescription": "Enables the remove_listener command without any pre-configured scope." + }, + { + "description": "Enables the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-app-theme", + "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-dock-visibility", + "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Enables the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-tauri-version", + "markdownDescription": "Enables the tauri_version command without any pre-configured scope." + }, + { + "description": "Enables the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-version", + "markdownDescription": "Enables the version command without any pre-configured scope." + }, + { + "description": "Denies the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-hide", + "markdownDescription": "Denies the app_hide command without any pre-configured scope." + }, + { + "description": "Denies the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-show", + "markdownDescription": "Denies the app_show command without any pre-configured scope." + }, + { + "description": "Denies the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-bundle-type", + "markdownDescription": "Denies the bundle_type command without any pre-configured scope." + }, + { + "description": "Denies the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-default-window-icon", + "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." + }, + { + "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-fetch-data-store-identifiers", + "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Denies the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-identifier", + "markdownDescription": "Denies the identifier command without any pre-configured scope." + }, + { + "description": "Denies the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-name", + "markdownDescription": "Denies the name command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-data-store", + "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." + }, + { + "description": "Denies the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-listener", + "markdownDescription": "Denies the remove_listener command without any pre-configured scope." + }, + { + "description": "Denies the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-app-theme", + "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-dock-visibility", + "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Denies the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-tauri-version", + "markdownDescription": "Denies the tauri_version command without any pre-configured scope." + }, + { + "description": "Denies the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-version", + "markdownDescription": "Denies the version command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", + "type": "string", + "const": "core:event:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" + }, + { + "description": "Enables the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit", + "markdownDescription": "Enables the emit command without any pre-configured scope." + }, + { + "description": "Enables the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit-to", + "markdownDescription": "Enables the emit_to command without any pre-configured scope." + }, + { + "description": "Enables the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-listen", + "markdownDescription": "Enables the listen command without any pre-configured scope." + }, + { + "description": "Enables the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-unlisten", + "markdownDescription": "Enables the unlisten command without any pre-configured scope." + }, + { + "description": "Denies the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit", + "markdownDescription": "Denies the emit command without any pre-configured scope." + }, + { + "description": "Denies the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit-to", + "markdownDescription": "Denies the emit_to command without any pre-configured scope." + }, + { + "description": "Denies the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-listen", + "markdownDescription": "Denies the listen command without any pre-configured scope." + }, + { + "description": "Denies the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-unlisten", + "markdownDescription": "Denies the unlisten command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", + "type": "string", + "const": "core:image:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" + }, + { + "description": "Enables the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-bytes", + "markdownDescription": "Enables the from_bytes command without any pre-configured scope." + }, + { + "description": "Enables the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-path", + "markdownDescription": "Enables the from_path command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-rgba", + "markdownDescription": "Enables the rgba command without any pre-configured scope." + }, + { + "description": "Enables the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-size", + "markdownDescription": "Enables the size command without any pre-configured scope." + }, + { + "description": "Denies the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-bytes", + "markdownDescription": "Denies the from_bytes command without any pre-configured scope." + }, + { + "description": "Denies the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-path", + "markdownDescription": "Denies the from_path command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-rgba", + "markdownDescription": "Denies the rgba command without any pre-configured scope." + }, + { + "description": "Denies the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-size", + "markdownDescription": "Denies the size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", + "type": "string", + "const": "core:menu:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" + }, + { + "description": "Enables the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-append", + "markdownDescription": "Enables the append command without any pre-configured scope." + }, + { + "description": "Enables the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-create-default", + "markdownDescription": "Enables the create_default command without any pre-configured scope." + }, + { + "description": "Enables the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-get", + "markdownDescription": "Enables the get command without any pre-configured scope." + }, + { + "description": "Enables the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-insert", + "markdownDescription": "Enables the insert command without any pre-configured scope." + }, + { + "description": "Enables the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-checked", + "markdownDescription": "Enables the is_checked command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-items", + "markdownDescription": "Enables the items command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-popup", + "markdownDescription": "Enables the popup command without any pre-configured scope." + }, + { + "description": "Enables the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-prepend", + "markdownDescription": "Enables the prepend command without any pre-configured scope." + }, + { + "description": "Enables the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove", + "markdownDescription": "Enables the remove command without any pre-configured scope." + }, + { + "description": "Enables the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove-at", + "markdownDescription": "Enables the remove_at command without any pre-configured scope." + }, + { + "description": "Enables the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-accelerator", + "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." + }, + { + "description": "Enables the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-app-menu", + "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-help-menu-for-nsapp", + "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-window-menu", + "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-windows-menu-for-nsapp", + "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-checked", + "markdownDescription": "Enables the set_checked command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-text", + "markdownDescription": "Enables the set_text command without any pre-configured scope." + }, + { + "description": "Enables the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-text", + "markdownDescription": "Enables the text command without any pre-configured scope." + }, + { + "description": "Denies the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-append", + "markdownDescription": "Denies the append command without any pre-configured scope." + }, + { + "description": "Denies the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-create-default", + "markdownDescription": "Denies the create_default command without any pre-configured scope." + }, + { + "description": "Denies the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-get", + "markdownDescription": "Denies the get command without any pre-configured scope." + }, + { + "description": "Denies the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-insert", + "markdownDescription": "Denies the insert command without any pre-configured scope." + }, + { + "description": "Denies the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-checked", + "markdownDescription": "Denies the is_checked command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-items", + "markdownDescription": "Denies the items command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-popup", + "markdownDescription": "Denies the popup command without any pre-configured scope." + }, + { + "description": "Denies the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-prepend", + "markdownDescription": "Denies the prepend command without any pre-configured scope." + }, + { + "description": "Denies the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove", + "markdownDescription": "Denies the remove command without any pre-configured scope." + }, + { + "description": "Denies the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove-at", + "markdownDescription": "Denies the remove_at command without any pre-configured scope." + }, + { + "description": "Denies the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-accelerator", + "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." + }, + { + "description": "Denies the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-app-menu", + "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-help-menu-for-nsapp", + "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-window-menu", + "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-windows-menu-for-nsapp", + "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-checked", + "markdownDescription": "Denies the set_checked command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-text", + "markdownDescription": "Denies the set_text command without any pre-configured scope." + }, + { + "description": "Denies the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-text", + "markdownDescription": "Denies the text command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", + "type": "string", + "const": "core:path:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" + }, + { + "description": "Enables the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-basename", + "markdownDescription": "Enables the basename command without any pre-configured scope." + }, + { + "description": "Enables the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-dirname", + "markdownDescription": "Enables the dirname command without any pre-configured scope." + }, + { + "description": "Enables the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-extname", + "markdownDescription": "Enables the extname command without any pre-configured scope." + }, + { + "description": "Enables the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-is-absolute", + "markdownDescription": "Enables the is_absolute command without any pre-configured scope." + }, + { + "description": "Enables the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-join", + "markdownDescription": "Enables the join command without any pre-configured scope." + }, + { + "description": "Enables the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-normalize", + "markdownDescription": "Enables the normalize command without any pre-configured scope." + }, + { + "description": "Enables the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve", + "markdownDescription": "Enables the resolve command without any pre-configured scope." + }, + { + "description": "Enables the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve-directory", + "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." + }, + { + "description": "Denies the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-basename", + "markdownDescription": "Denies the basename command without any pre-configured scope." + }, + { + "description": "Denies the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-dirname", + "markdownDescription": "Denies the dirname command without any pre-configured scope." + }, + { + "description": "Denies the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-extname", + "markdownDescription": "Denies the extname command without any pre-configured scope." + }, + { + "description": "Denies the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-is-absolute", + "markdownDescription": "Denies the is_absolute command without any pre-configured scope." + }, + { + "description": "Denies the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-join", + "markdownDescription": "Denies the join command without any pre-configured scope." + }, + { + "description": "Denies the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-normalize", + "markdownDescription": "Denies the normalize command without any pre-configured scope." + }, + { + "description": "Denies the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve", + "markdownDescription": "Denies the resolve command without any pre-configured scope." + }, + { + "description": "Denies the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve-directory", + "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", + "type": "string", + "const": "core:resources:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`", + "type": "string", + "const": "core:tray:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`" + }, + { + "description": "Enables the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-get-by-id", + "markdownDescription": "Enables the get_by_id command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-remove-by-id", + "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-as-template", + "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-menu", + "markdownDescription": "Enables the set_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-show-menu-on-left-click", + "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Enables the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-temp-dir-path", + "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-tooltip", + "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." + }, + { + "description": "Enables the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-visible", + "markdownDescription": "Enables the set_visible command without any pre-configured scope." + }, + { + "description": "Denies the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-get-by-id", + "markdownDescription": "Denies the get_by_id command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-remove-by-id", + "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-as-template", + "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-menu", + "markdownDescription": "Denies the set_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-show-menu-on-left-click", + "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Denies the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-temp-dir-path", + "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-tooltip", + "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." + }, + { + "description": "Denies the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-visible", + "markdownDescription": "Denies the set_visible command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", + "type": "string", + "const": "core:webview:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" + }, + { + "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-clear-all-browsing-data", + "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Enables the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview", + "markdownDescription": "Enables the create_webview command without any pre-configured scope." + }, + { + "description": "Enables the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview-window", + "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." + }, + { + "description": "Enables the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-get-all-webviews", + "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-internal-toggle-devtools", + "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Enables the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-print", + "markdownDescription": "Enables the print command without any pre-configured scope." + }, + { + "description": "Enables the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-reparent", + "markdownDescription": "Enables the reparent command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-auto-resize", + "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-background-color", + "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-focus", + "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-position", + "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-size", + "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-zoom", + "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Enables the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-close", + "markdownDescription": "Enables the webview_close command without any pre-configured scope." + }, + { + "description": "Enables the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-hide", + "markdownDescription": "Enables the webview_hide command without any pre-configured scope." + }, + { + "description": "Enables the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-position", + "markdownDescription": "Enables the webview_position command without any pre-configured scope." + }, + { + "description": "Enables the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-show", + "markdownDescription": "Enables the webview_show command without any pre-configured scope." + }, + { + "description": "Enables the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-size", + "markdownDescription": "Enables the webview_size command without any pre-configured scope." + }, + { + "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-clear-all-browsing-data", + "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Denies the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview", + "markdownDescription": "Denies the create_webview command without any pre-configured scope." + }, + { + "description": "Denies the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview-window", + "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." + }, + { + "description": "Denies the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-get-all-webviews", + "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-internal-toggle-devtools", + "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Denies the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-print", + "markdownDescription": "Denies the print command without any pre-configured scope." + }, + { + "description": "Denies the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-reparent", + "markdownDescription": "Denies the reparent command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-auto-resize", + "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-background-color", + "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-focus", + "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-position", + "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-size", + "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-zoom", + "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Denies the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-close", + "markdownDescription": "Denies the webview_close command without any pre-configured scope." + }, + { + "description": "Denies the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-hide", + "markdownDescription": "Denies the webview_hide command without any pre-configured scope." + }, + { + "description": "Denies the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-position", + "markdownDescription": "Denies the webview_position command without any pre-configured scope." + }, + { + "description": "Denies the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-show", + "markdownDescription": "Denies the webview_show command without any pre-configured scope." + }, + { + "description": "Denies the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-size", + "markdownDescription": "Denies the webview_size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`", + "type": "string", + "const": "core:window:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`" + }, + { + "description": "Enables the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-available-monitors", + "markdownDescription": "Enables the available_monitors command without any pre-configured scope." + }, + { + "description": "Enables the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-center", + "markdownDescription": "Enables the center command without any pre-configured scope." + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Enables the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-create", + "markdownDescription": "Enables the create command without any pre-configured scope." + }, + { + "description": "Enables the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-current-monitor", + "markdownDescription": "Enables the current_monitor command without any pre-configured scope." + }, + { + "description": "Enables the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-cursor-position", + "markdownDescription": "Enables the cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-destroy", + "markdownDescription": "Enables the destroy command without any pre-configured scope." + }, + { + "description": "Enables the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-get-all-windows", + "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." + }, + { + "description": "Enables the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-hide", + "markdownDescription": "Enables the hide command without any pre-configured scope." + }, + { + "description": "Enables the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-position", + "markdownDescription": "Enables the inner_position command without any pre-configured scope." + }, + { + "description": "Enables the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-size", + "markdownDescription": "Enables the inner_size command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-internal-toggle-maximize", + "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-always-on-top", + "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-closable", + "markdownDescription": "Enables the is_closable command without any pre-configured scope." + }, + { + "description": "Enables the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-decorated", + "markdownDescription": "Enables the is_decorated command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-focused", + "markdownDescription": "Enables the is_focused command without any pre-configured scope." + }, + { + "description": "Enables the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-fullscreen", + "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximizable", + "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximized", + "markdownDescription": "Enables the is_maximized command without any pre-configured scope." + }, + { + "description": "Enables the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimizable", + "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimized", + "markdownDescription": "Enables the is_minimized command without any pre-configured scope." + }, + { + "description": "Enables the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-resizable", + "markdownDescription": "Enables the is_resizable command without any pre-configured scope." + }, + { + "description": "Enables the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-visible", + "markdownDescription": "Enables the is_visible command without any pre-configured scope." + }, + { + "description": "Enables the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-maximize", + "markdownDescription": "Enables the maximize command without any pre-configured scope." + }, + { + "description": "Enables the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-minimize", + "markdownDescription": "Enables the minimize command without any pre-configured scope." + }, + { + "description": "Enables the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-monitor-from-point", + "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Enables the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-position", + "markdownDescription": "Enables the outer_position command without any pre-configured scope." + }, + { + "description": "Enables the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-size", + "markdownDescription": "Enables the outer_size command without any pre-configured scope." + }, + { + "description": "Enables the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-primary-monitor", + "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." + }, + { + "description": "Enables the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-request-user-attention", + "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." + }, + { + "description": "Enables the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scale-factor", + "markdownDescription": "Enables the scale_factor command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-bottom", + "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-top", + "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-background-color", + "markdownDescription": "Enables the set_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-count", + "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-label", + "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." + }, + { + "description": "Enables the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-closable", + "markdownDescription": "Enables the set_closable command without any pre-configured scope." + }, + { + "description": "Enables the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-content-protected", + "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-grab", + "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-icon", + "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-position", + "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-visible", + "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Enables the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-decorations", + "markdownDescription": "Enables the set_decorations command without any pre-configured scope." + }, + { + "description": "Enables the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-effects", + "markdownDescription": "Enables the set_effects command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focus", + "markdownDescription": "Enables the set_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focusable", + "markdownDescription": "Enables the set_focusable command without any pre-configured scope." + }, + { + "description": "Enables the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-fullscreen", + "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-ignore-cursor-events", + "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Enables the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-max-size", + "markdownDescription": "Enables the set_max_size command without any pre-configured scope." + }, + { + "description": "Enables the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-maximizable", + "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-min-size", + "markdownDescription": "Enables the set_min_size command without any pre-configured scope." + }, + { + "description": "Enables the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-minimizable", + "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-overlay-icon", + "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-position", + "markdownDescription": "Enables the set_position command without any pre-configured scope." + }, + { + "description": "Enables the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-progress-bar", + "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Enables the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-resizable", + "markdownDescription": "Enables the set_resizable command without any pre-configured scope." + }, + { + "description": "Enables the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-shadow", + "markdownDescription": "Enables the set_shadow command without any pre-configured scope." + }, + { + "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-simple-fullscreen", + "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size", + "markdownDescription": "Enables the set_size command without any pre-configured scope." + }, + { + "description": "Enables the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size-constraints", + "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Enables the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-skip-taskbar", + "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Enables the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-theme", + "markdownDescription": "Enables the set_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title-bar-style", + "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-visible-on-all-workspaces", + "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Enables the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-dragging", + "markdownDescription": "Enables the start_dragging command without any pre-configured scope." + }, + { + "description": "Enables the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-resize-dragging", + "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Enables the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-theme", + "markdownDescription": "Enables the theme command without any pre-configured scope." + }, + { + "description": "Enables the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-title", + "markdownDescription": "Enables the title command without any pre-configured scope." + }, + { + "description": "Enables the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-toggle-maximize", + "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unmaximize", + "markdownDescription": "Enables the unmaximize command without any pre-configured scope." + }, + { + "description": "Enables the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unminimize", + "markdownDescription": "Enables the unminimize command without any pre-configured scope." + }, + { + "description": "Denies the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-available-monitors", + "markdownDescription": "Denies the available_monitors command without any pre-configured scope." + }, + { + "description": "Denies the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-center", + "markdownDescription": "Denies the center command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Denies the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-create", + "markdownDescription": "Denies the create command without any pre-configured scope." + }, + { + "description": "Denies the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-current-monitor", + "markdownDescription": "Denies the current_monitor command without any pre-configured scope." + }, + { + "description": "Denies the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-cursor-position", + "markdownDescription": "Denies the cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-destroy", + "markdownDescription": "Denies the destroy command without any pre-configured scope." + }, + { + "description": "Denies the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-get-all-windows", + "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." + }, + { + "description": "Denies the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-hide", + "markdownDescription": "Denies the hide command without any pre-configured scope." + }, + { + "description": "Denies the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-position", + "markdownDescription": "Denies the inner_position command without any pre-configured scope." + }, + { + "description": "Denies the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-size", + "markdownDescription": "Denies the inner_size command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-internal-toggle-maximize", + "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-always-on-top", + "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-closable", + "markdownDescription": "Denies the is_closable command without any pre-configured scope." + }, + { + "description": "Denies the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-decorated", + "markdownDescription": "Denies the is_decorated command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-focused", + "markdownDescription": "Denies the is_focused command without any pre-configured scope." + }, + { + "description": "Denies the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-fullscreen", + "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximizable", + "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximized", + "markdownDescription": "Denies the is_maximized command without any pre-configured scope." + }, + { + "description": "Denies the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimizable", + "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimized", + "markdownDescription": "Denies the is_minimized command without any pre-configured scope." + }, + { + "description": "Denies the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-resizable", + "markdownDescription": "Denies the is_resizable command without any pre-configured scope." + }, + { + "description": "Denies the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-visible", + "markdownDescription": "Denies the is_visible command without any pre-configured scope." + }, + { + "description": "Denies the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-maximize", + "markdownDescription": "Denies the maximize command without any pre-configured scope." + }, + { + "description": "Denies the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-minimize", + "markdownDescription": "Denies the minimize command without any pre-configured scope." + }, + { + "description": "Denies the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-monitor-from-point", + "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Denies the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-position", + "markdownDescription": "Denies the outer_position command without any pre-configured scope." + }, + { + "description": "Denies the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-size", + "markdownDescription": "Denies the outer_size command without any pre-configured scope." + }, + { + "description": "Denies the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-primary-monitor", + "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." + }, + { + "description": "Denies the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-request-user-attention", + "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." + }, + { + "description": "Denies the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scale-factor", + "markdownDescription": "Denies the scale_factor command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-bottom", + "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-top", + "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-background-color", + "markdownDescription": "Denies the set_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-count", + "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-label", + "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." + }, + { + "description": "Denies the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-closable", + "markdownDescription": "Denies the set_closable command without any pre-configured scope." + }, + { + "description": "Denies the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-content-protected", + "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-grab", + "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-icon", + "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-position", + "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-visible", + "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Denies the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-decorations", + "markdownDescription": "Denies the set_decorations command without any pre-configured scope." + }, + { + "description": "Denies the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-effects", + "markdownDescription": "Denies the set_effects command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focus", + "markdownDescription": "Denies the set_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focusable", + "markdownDescription": "Denies the set_focusable command without any pre-configured scope." + }, + { + "description": "Denies the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-fullscreen", + "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-ignore-cursor-events", + "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Denies the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-max-size", + "markdownDescription": "Denies the set_max_size command without any pre-configured scope." + }, + { + "description": "Denies the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-maximizable", + "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-min-size", + "markdownDescription": "Denies the set_min_size command without any pre-configured scope." + }, + { + "description": "Denies the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-minimizable", + "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-overlay-icon", + "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-position", + "markdownDescription": "Denies the set_position command without any pre-configured scope." + }, + { + "description": "Denies the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-progress-bar", + "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Denies the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-resizable", + "markdownDescription": "Denies the set_resizable command without any pre-configured scope." + }, + { + "description": "Denies the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-shadow", + "markdownDescription": "Denies the set_shadow command without any pre-configured scope." + }, + { + "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-simple-fullscreen", + "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size", + "markdownDescription": "Denies the set_size command without any pre-configured scope." + }, + { + "description": "Denies the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size-constraints", + "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Denies the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-skip-taskbar", + "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Denies the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-theme", + "markdownDescription": "Denies the set_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title-bar-style", + "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-visible-on-all-workspaces", + "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "Denies the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-dragging", + "markdownDescription": "Denies the start_dragging command without any pre-configured scope." + }, + { + "description": "Denies the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-resize-dragging", + "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Denies the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-theme", + "markdownDescription": "Denies the theme command without any pre-configured scope." + }, + { + "description": "Denies the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-title", + "markdownDescription": "Denies the title command without any pre-configured scope." + }, + { + "description": "Denies the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-toggle-maximize", + "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unmaximize", + "markdownDescription": "Denies the unmaximize command without any pre-configured scope." + }, + { + "description": "Denies the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unminimize", + "markdownDescription": "Denies the unminimize command without any pre-configured scope." + }, + { + "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`", + "type": "string", + "const": "dialog:default", + "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`" + }, + { + "description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", + "type": "string", + "const": "dialog:allow-ask", + "markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + }, + { + "description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", + "type": "string", + "const": "dialog:allow-confirm", + "markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + }, + { + "description": "Enables the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-message", + "markdownDescription": "Enables the message command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-save", + "markdownDescription": "Enables the save command without any pre-configured scope." + }, + { + "description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", + "type": "string", + "const": "dialog:deny-ask", + "markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + }, + { + "description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", + "type": "string", + "const": "dialog:deny-confirm", + "markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + }, + { + "description": "Denies the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-message", + "markdownDescription": "Denies the message command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-save", + "markdownDescription": "Denies the save command without any pre-configured scope." + }, + { + "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`", + "type": "string", + "const": "shell:default", + "markdownDescription": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`" + }, + { + "description": "Enables the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-execute", + "markdownDescription": "Enables the execute command without any pre-configured scope." + }, + { + "description": "Enables the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-kill", + "markdownDescription": "Enables the kill command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-spawn", + "markdownDescription": "Enables the spawn command without any pre-configured scope." + }, + { + "description": "Enables the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-stdin-write", + "markdownDescription": "Enables the stdin_write command without any pre-configured scope." + }, + { + "description": "Denies the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-execute", + "markdownDescription": "Denies the execute command without any pre-configured scope." + }, + { + "description": "Denies the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-kill", + "markdownDescription": "Denies the kill command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-spawn", + "markdownDescription": "Denies the spawn command without any pre-configured scope." + }, + { + "description": "Denies the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-stdin-write", + "markdownDescription": "Denies the stdin_write command without any pre-configured scope." + } + ] + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "ShellScopeEntryAllowedArg": { + "description": "A command argument allowed to be executed by the webview API.", + "anyOf": [ + { + "description": "A non-configurable argument that is passed to the command in the order it was specified.", + "type": "string" + }, + { + "description": "A variable that is set while calling the command from the webview API.", + "type": "object", + "required": [ + "validator" + ], + "properties": { + "raw": { + "description": "Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.", + "default": false, + "type": "boolean" + }, + "validator": { + "description": "[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ", + "type": "string" + } + }, + "additionalProperties": false + } + ] + }, + "ShellScopeEntryAllowedArgs": { + "description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.", + "anyOf": [ + { + "description": "Use a simple boolean to allow all or disable all arguments to this command configuration.", + "type": "boolean" + }, + { + "description": "A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.", + "type": "array", + "items": { + "$ref": "#/definitions/ShellScopeEntryAllowedArg" + } + } + ] + } + } +} \ No newline at end of file diff --git a/client/gen/schemas/linux-schema.json b/client/gen/schemas/linux-schema.json new file mode 100644 index 0000000..634faec --- /dev/null +++ b/client/gen/schemas/linux-schema.json @@ -0,0 +1,2630 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CapabilityFile", + "description": "Capability formats accepted in a capability file.", + "anyOf": [ + { + "description": "A single capability.", + "allOf": [ + { + "$ref": "#/definitions/Capability" + } + ] + }, + { + "description": "A list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + }, + { + "description": "A list of capabilities.", + "type": "object", + "required": [ + "capabilities" + ], + "properties": { + "capabilities": { + "description": "The list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + } + } + } + ], + "definitions": { + "Capability": { + "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", + "type": "object", + "required": [ + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", + "type": "string" + }, + "description": { + "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", + "default": "", + "type": "string" + }, + "remote": { + "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", + "anyOf": [ + { + "$ref": "#/definitions/CapabilityRemote" + }, + { + "type": "null" + } + ] + }, + "local": { + "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", + "default": true, + "type": "boolean" + }, + "windows": { + "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "webviews": { + "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "permissions": { + "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionEntry" + }, + "uniqueItems": true + }, + "platforms": { + "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "CapabilityRemote": { + "description": "Configuration for remote URLs that are associated with the capability.", + "type": "object", + "required": [ + "urls" + ], + "properties": { + "urls": { + "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionEntry": { + "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", + "anyOf": [ + { + "description": "Reference a permission or permission set by identifier.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + { + "description": "Reference a permission or permission set by identifier and extends its scope.", + "type": "object", + "allOf": [ + { + "if": { + "properties": { + "identifier": { + "anyOf": [ + { + "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`", + "type": "string", + "const": "shell:default", + "markdownDescription": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`" + }, + { + "description": "Enables the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-execute", + "markdownDescription": "Enables the execute command without any pre-configured scope." + }, + { + "description": "Enables the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-kill", + "markdownDescription": "Enables the kill command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-spawn", + "markdownDescription": "Enables the spawn command without any pre-configured scope." + }, + { + "description": "Enables the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-stdin-write", + "markdownDescription": "Enables the stdin_write command without any pre-configured scope." + }, + { + "description": "Denies the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-execute", + "markdownDescription": "Denies the execute command without any pre-configured scope." + }, + { + "description": "Denies the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-kill", + "markdownDescription": "Denies the kill command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-spawn", + "markdownDescription": "Denies the spawn command without any pre-configured scope." + }, + { + "description": "Denies the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-stdin-write", + "markdownDescription": "Denies the stdin_write command without any pre-configured scope." + } + ] + } + } + }, + "then": { + "properties": { + "allow": { + "items": { + "title": "ShellScopeEntry", + "description": "Shell scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "cmd", + "name" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "cmd": { + "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "type": "string" + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "name", + "sidecar" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + }, + "sidecar": { + "description": "If this command is a sidecar command.", + "type": "boolean" + } + }, + "additionalProperties": false + } + ] + } + }, + "deny": { + "items": { + "title": "ShellScopeEntry", + "description": "Shell scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "cmd", + "name" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "cmd": { + "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "type": "string" + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "name", + "sidecar" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + }, + "sidecar": { + "description": "If this command is a sidecar command.", + "type": "boolean" + } + }, + "additionalProperties": false + } + ] + } + } + } + }, + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + } + } + }, + { + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + } + ], + "required": [ + "identifier" + ] + } + ] + }, + "Identifier": { + "description": "Permission identifier", + "oneOf": [ + { + "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", + "type": "string", + "const": "core:default", + "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`", + "type": "string", + "const": "core:app:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`" + }, + { + "description": "Enables the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-hide", + "markdownDescription": "Enables the app_hide command without any pre-configured scope." + }, + { + "description": "Enables the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-show", + "markdownDescription": "Enables the app_show command without any pre-configured scope." + }, + { + "description": "Enables the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-bundle-type", + "markdownDescription": "Enables the bundle_type command without any pre-configured scope." + }, + { + "description": "Enables the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-default-window-icon", + "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." + }, + { + "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-fetch-data-store-identifiers", + "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Enables the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-identifier", + "markdownDescription": "Enables the identifier command without any pre-configured scope." + }, + { + "description": "Enables the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-name", + "markdownDescription": "Enables the name command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-data-store", + "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." + }, + { + "description": "Enables the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-listener", + "markdownDescription": "Enables the remove_listener command without any pre-configured scope." + }, + { + "description": "Enables the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-app-theme", + "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-dock-visibility", + "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Enables the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-tauri-version", + "markdownDescription": "Enables the tauri_version command without any pre-configured scope." + }, + { + "description": "Enables the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-version", + "markdownDescription": "Enables the version command without any pre-configured scope." + }, + { + "description": "Denies the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-hide", + "markdownDescription": "Denies the app_hide command without any pre-configured scope." + }, + { + "description": "Denies the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-show", + "markdownDescription": "Denies the app_show command without any pre-configured scope." + }, + { + "description": "Denies the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-bundle-type", + "markdownDescription": "Denies the bundle_type command without any pre-configured scope." + }, + { + "description": "Denies the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-default-window-icon", + "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." + }, + { + "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-fetch-data-store-identifiers", + "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Denies the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-identifier", + "markdownDescription": "Denies the identifier command without any pre-configured scope." + }, + { + "description": "Denies the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-name", + "markdownDescription": "Denies the name command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-data-store", + "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." + }, + { + "description": "Denies the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-listener", + "markdownDescription": "Denies the remove_listener command without any pre-configured scope." + }, + { + "description": "Denies the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-app-theme", + "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-dock-visibility", + "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Denies the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-tauri-version", + "markdownDescription": "Denies the tauri_version command without any pre-configured scope." + }, + { + "description": "Denies the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-version", + "markdownDescription": "Denies the version command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", + "type": "string", + "const": "core:event:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" + }, + { + "description": "Enables the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit", + "markdownDescription": "Enables the emit command without any pre-configured scope." + }, + { + "description": "Enables the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit-to", + "markdownDescription": "Enables the emit_to command without any pre-configured scope." + }, + { + "description": "Enables the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-listen", + "markdownDescription": "Enables the listen command without any pre-configured scope." + }, + { + "description": "Enables the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-unlisten", + "markdownDescription": "Enables the unlisten command without any pre-configured scope." + }, + { + "description": "Denies the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit", + "markdownDescription": "Denies the emit command without any pre-configured scope." + }, + { + "description": "Denies the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit-to", + "markdownDescription": "Denies the emit_to command without any pre-configured scope." + }, + { + "description": "Denies the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-listen", + "markdownDescription": "Denies the listen command without any pre-configured scope." + }, + { + "description": "Denies the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-unlisten", + "markdownDescription": "Denies the unlisten command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", + "type": "string", + "const": "core:image:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" + }, + { + "description": "Enables the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-bytes", + "markdownDescription": "Enables the from_bytes command without any pre-configured scope." + }, + { + "description": "Enables the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-path", + "markdownDescription": "Enables the from_path command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-rgba", + "markdownDescription": "Enables the rgba command without any pre-configured scope." + }, + { + "description": "Enables the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-size", + "markdownDescription": "Enables the size command without any pre-configured scope." + }, + { + "description": "Denies the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-bytes", + "markdownDescription": "Denies the from_bytes command without any pre-configured scope." + }, + { + "description": "Denies the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-path", + "markdownDescription": "Denies the from_path command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-rgba", + "markdownDescription": "Denies the rgba command without any pre-configured scope." + }, + { + "description": "Denies the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-size", + "markdownDescription": "Denies the size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", + "type": "string", + "const": "core:menu:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" + }, + { + "description": "Enables the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-append", + "markdownDescription": "Enables the append command without any pre-configured scope." + }, + { + "description": "Enables the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-create-default", + "markdownDescription": "Enables the create_default command without any pre-configured scope." + }, + { + "description": "Enables the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-get", + "markdownDescription": "Enables the get command without any pre-configured scope." + }, + { + "description": "Enables the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-insert", + "markdownDescription": "Enables the insert command without any pre-configured scope." + }, + { + "description": "Enables the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-checked", + "markdownDescription": "Enables the is_checked command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-items", + "markdownDescription": "Enables the items command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-popup", + "markdownDescription": "Enables the popup command without any pre-configured scope." + }, + { + "description": "Enables the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-prepend", + "markdownDescription": "Enables the prepend command without any pre-configured scope." + }, + { + "description": "Enables the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove", + "markdownDescription": "Enables the remove command without any pre-configured scope." + }, + { + "description": "Enables the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove-at", + "markdownDescription": "Enables the remove_at command without any pre-configured scope." + }, + { + "description": "Enables the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-accelerator", + "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." + }, + { + "description": "Enables the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-app-menu", + "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-help-menu-for-nsapp", + "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-window-menu", + "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-windows-menu-for-nsapp", + "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-checked", + "markdownDescription": "Enables the set_checked command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-text", + "markdownDescription": "Enables the set_text command without any pre-configured scope." + }, + { + "description": "Enables the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-text", + "markdownDescription": "Enables the text command without any pre-configured scope." + }, + { + "description": "Denies the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-append", + "markdownDescription": "Denies the append command without any pre-configured scope." + }, + { + "description": "Denies the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-create-default", + "markdownDescription": "Denies the create_default command without any pre-configured scope." + }, + { + "description": "Denies the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-get", + "markdownDescription": "Denies the get command without any pre-configured scope." + }, + { + "description": "Denies the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-insert", + "markdownDescription": "Denies the insert command without any pre-configured scope." + }, + { + "description": "Denies the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-checked", + "markdownDescription": "Denies the is_checked command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-items", + "markdownDescription": "Denies the items command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-popup", + "markdownDescription": "Denies the popup command without any pre-configured scope." + }, + { + "description": "Denies the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-prepend", + "markdownDescription": "Denies the prepend command without any pre-configured scope." + }, + { + "description": "Denies the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove", + "markdownDescription": "Denies the remove command without any pre-configured scope." + }, + { + "description": "Denies the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove-at", + "markdownDescription": "Denies the remove_at command without any pre-configured scope." + }, + { + "description": "Denies the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-accelerator", + "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." + }, + { + "description": "Denies the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-app-menu", + "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-help-menu-for-nsapp", + "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-window-menu", + "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-windows-menu-for-nsapp", + "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-checked", + "markdownDescription": "Denies the set_checked command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-text", + "markdownDescription": "Denies the set_text command without any pre-configured scope." + }, + { + "description": "Denies the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-text", + "markdownDescription": "Denies the text command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", + "type": "string", + "const": "core:path:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" + }, + { + "description": "Enables the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-basename", + "markdownDescription": "Enables the basename command without any pre-configured scope." + }, + { + "description": "Enables the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-dirname", + "markdownDescription": "Enables the dirname command without any pre-configured scope." + }, + { + "description": "Enables the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-extname", + "markdownDescription": "Enables the extname command without any pre-configured scope." + }, + { + "description": "Enables the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-is-absolute", + "markdownDescription": "Enables the is_absolute command without any pre-configured scope." + }, + { + "description": "Enables the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-join", + "markdownDescription": "Enables the join command without any pre-configured scope." + }, + { + "description": "Enables the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-normalize", + "markdownDescription": "Enables the normalize command without any pre-configured scope." + }, + { + "description": "Enables the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve", + "markdownDescription": "Enables the resolve command without any pre-configured scope." + }, + { + "description": "Enables the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve-directory", + "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." + }, + { + "description": "Denies the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-basename", + "markdownDescription": "Denies the basename command without any pre-configured scope." + }, + { + "description": "Denies the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-dirname", + "markdownDescription": "Denies the dirname command without any pre-configured scope." + }, + { + "description": "Denies the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-extname", + "markdownDescription": "Denies the extname command without any pre-configured scope." + }, + { + "description": "Denies the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-is-absolute", + "markdownDescription": "Denies the is_absolute command without any pre-configured scope." + }, + { + "description": "Denies the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-join", + "markdownDescription": "Denies the join command without any pre-configured scope." + }, + { + "description": "Denies the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-normalize", + "markdownDescription": "Denies the normalize command without any pre-configured scope." + }, + { + "description": "Denies the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve", + "markdownDescription": "Denies the resolve command without any pre-configured scope." + }, + { + "description": "Denies the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve-directory", + "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", + "type": "string", + "const": "core:resources:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`", + "type": "string", + "const": "core:tray:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`" + }, + { + "description": "Enables the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-get-by-id", + "markdownDescription": "Enables the get_by_id command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-remove-by-id", + "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-as-template", + "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-menu", + "markdownDescription": "Enables the set_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-show-menu-on-left-click", + "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Enables the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-temp-dir-path", + "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-tooltip", + "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." + }, + { + "description": "Enables the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-visible", + "markdownDescription": "Enables the set_visible command without any pre-configured scope." + }, + { + "description": "Denies the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-get-by-id", + "markdownDescription": "Denies the get_by_id command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-remove-by-id", + "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-as-template", + "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-menu", + "markdownDescription": "Denies the set_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-show-menu-on-left-click", + "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Denies the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-temp-dir-path", + "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-tooltip", + "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." + }, + { + "description": "Denies the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-visible", + "markdownDescription": "Denies the set_visible command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", + "type": "string", + "const": "core:webview:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" + }, + { + "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-clear-all-browsing-data", + "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Enables the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview", + "markdownDescription": "Enables the create_webview command without any pre-configured scope." + }, + { + "description": "Enables the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview-window", + "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." + }, + { + "description": "Enables the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-get-all-webviews", + "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-internal-toggle-devtools", + "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Enables the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-print", + "markdownDescription": "Enables the print command without any pre-configured scope." + }, + { + "description": "Enables the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-reparent", + "markdownDescription": "Enables the reparent command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-auto-resize", + "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-background-color", + "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-focus", + "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-position", + "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-size", + "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-zoom", + "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Enables the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-close", + "markdownDescription": "Enables the webview_close command without any pre-configured scope." + }, + { + "description": "Enables the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-hide", + "markdownDescription": "Enables the webview_hide command without any pre-configured scope." + }, + { + "description": "Enables the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-position", + "markdownDescription": "Enables the webview_position command without any pre-configured scope." + }, + { + "description": "Enables the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-show", + "markdownDescription": "Enables the webview_show command without any pre-configured scope." + }, + { + "description": "Enables the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-size", + "markdownDescription": "Enables the webview_size command without any pre-configured scope." + }, + { + "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-clear-all-browsing-data", + "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Denies the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview", + "markdownDescription": "Denies the create_webview command without any pre-configured scope." + }, + { + "description": "Denies the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview-window", + "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." + }, + { + "description": "Denies the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-get-all-webviews", + "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-internal-toggle-devtools", + "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Denies the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-print", + "markdownDescription": "Denies the print command without any pre-configured scope." + }, + { + "description": "Denies the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-reparent", + "markdownDescription": "Denies the reparent command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-auto-resize", + "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-background-color", + "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-focus", + "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-position", + "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-size", + "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-zoom", + "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Denies the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-close", + "markdownDescription": "Denies the webview_close command without any pre-configured scope." + }, + { + "description": "Denies the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-hide", + "markdownDescription": "Denies the webview_hide command without any pre-configured scope." + }, + { + "description": "Denies the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-position", + "markdownDescription": "Denies the webview_position command without any pre-configured scope." + }, + { + "description": "Denies the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-show", + "markdownDescription": "Denies the webview_show command without any pre-configured scope." + }, + { + "description": "Denies the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-size", + "markdownDescription": "Denies the webview_size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`", + "type": "string", + "const": "core:window:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`" + }, + { + "description": "Enables the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-available-monitors", + "markdownDescription": "Enables the available_monitors command without any pre-configured scope." + }, + { + "description": "Enables the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-center", + "markdownDescription": "Enables the center command without any pre-configured scope." + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Enables the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-create", + "markdownDescription": "Enables the create command without any pre-configured scope." + }, + { + "description": "Enables the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-current-monitor", + "markdownDescription": "Enables the current_monitor command without any pre-configured scope." + }, + { + "description": "Enables the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-cursor-position", + "markdownDescription": "Enables the cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-destroy", + "markdownDescription": "Enables the destroy command without any pre-configured scope." + }, + { + "description": "Enables the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-get-all-windows", + "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." + }, + { + "description": "Enables the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-hide", + "markdownDescription": "Enables the hide command without any pre-configured scope." + }, + { + "description": "Enables the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-position", + "markdownDescription": "Enables the inner_position command without any pre-configured scope." + }, + { + "description": "Enables the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-size", + "markdownDescription": "Enables the inner_size command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-internal-toggle-maximize", + "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-always-on-top", + "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-closable", + "markdownDescription": "Enables the is_closable command without any pre-configured scope." + }, + { + "description": "Enables the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-decorated", + "markdownDescription": "Enables the is_decorated command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-focused", + "markdownDescription": "Enables the is_focused command without any pre-configured scope." + }, + { + "description": "Enables the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-fullscreen", + "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximizable", + "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximized", + "markdownDescription": "Enables the is_maximized command without any pre-configured scope." + }, + { + "description": "Enables the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimizable", + "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimized", + "markdownDescription": "Enables the is_minimized command without any pre-configured scope." + }, + { + "description": "Enables the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-resizable", + "markdownDescription": "Enables the is_resizable command without any pre-configured scope." + }, + { + "description": "Enables the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-visible", + "markdownDescription": "Enables the is_visible command without any pre-configured scope." + }, + { + "description": "Enables the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-maximize", + "markdownDescription": "Enables the maximize command without any pre-configured scope." + }, + { + "description": "Enables the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-minimize", + "markdownDescription": "Enables the minimize command without any pre-configured scope." + }, + { + "description": "Enables the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-monitor-from-point", + "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Enables the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-position", + "markdownDescription": "Enables the outer_position command without any pre-configured scope." + }, + { + "description": "Enables the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-size", + "markdownDescription": "Enables the outer_size command without any pre-configured scope." + }, + { + "description": "Enables the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-primary-monitor", + "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." + }, + { + "description": "Enables the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-request-user-attention", + "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." + }, + { + "description": "Enables the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scale-factor", + "markdownDescription": "Enables the scale_factor command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-bottom", + "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-top", + "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-background-color", + "markdownDescription": "Enables the set_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-count", + "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-label", + "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." + }, + { + "description": "Enables the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-closable", + "markdownDescription": "Enables the set_closable command without any pre-configured scope." + }, + { + "description": "Enables the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-content-protected", + "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-grab", + "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-icon", + "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-position", + "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-visible", + "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Enables the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-decorations", + "markdownDescription": "Enables the set_decorations command without any pre-configured scope." + }, + { + "description": "Enables the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-effects", + "markdownDescription": "Enables the set_effects command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focus", + "markdownDescription": "Enables the set_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focusable", + "markdownDescription": "Enables the set_focusable command without any pre-configured scope." + }, + { + "description": "Enables the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-fullscreen", + "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-ignore-cursor-events", + "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Enables the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-max-size", + "markdownDescription": "Enables the set_max_size command without any pre-configured scope." + }, + { + "description": "Enables the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-maximizable", + "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-min-size", + "markdownDescription": "Enables the set_min_size command without any pre-configured scope." + }, + { + "description": "Enables the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-minimizable", + "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-overlay-icon", + "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-position", + "markdownDescription": "Enables the set_position command without any pre-configured scope." + }, + { + "description": "Enables the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-progress-bar", + "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Enables the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-resizable", + "markdownDescription": "Enables the set_resizable command without any pre-configured scope." + }, + { + "description": "Enables the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-shadow", + "markdownDescription": "Enables the set_shadow command without any pre-configured scope." + }, + { + "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-simple-fullscreen", + "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size", + "markdownDescription": "Enables the set_size command without any pre-configured scope." + }, + { + "description": "Enables the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size-constraints", + "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Enables the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-skip-taskbar", + "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Enables the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-theme", + "markdownDescription": "Enables the set_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title-bar-style", + "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-visible-on-all-workspaces", + "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Enables the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-dragging", + "markdownDescription": "Enables the start_dragging command without any pre-configured scope." + }, + { + "description": "Enables the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-resize-dragging", + "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Enables the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-theme", + "markdownDescription": "Enables the theme command without any pre-configured scope." + }, + { + "description": "Enables the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-title", + "markdownDescription": "Enables the title command without any pre-configured scope." + }, + { + "description": "Enables the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-toggle-maximize", + "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unmaximize", + "markdownDescription": "Enables the unmaximize command without any pre-configured scope." + }, + { + "description": "Enables the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unminimize", + "markdownDescription": "Enables the unminimize command without any pre-configured scope." + }, + { + "description": "Denies the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-available-monitors", + "markdownDescription": "Denies the available_monitors command without any pre-configured scope." + }, + { + "description": "Denies the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-center", + "markdownDescription": "Denies the center command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Denies the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-create", + "markdownDescription": "Denies the create command without any pre-configured scope." + }, + { + "description": "Denies the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-current-monitor", + "markdownDescription": "Denies the current_monitor command without any pre-configured scope." + }, + { + "description": "Denies the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-cursor-position", + "markdownDescription": "Denies the cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-destroy", + "markdownDescription": "Denies the destroy command without any pre-configured scope." + }, + { + "description": "Denies the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-get-all-windows", + "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." + }, + { + "description": "Denies the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-hide", + "markdownDescription": "Denies the hide command without any pre-configured scope." + }, + { + "description": "Denies the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-position", + "markdownDescription": "Denies the inner_position command without any pre-configured scope." + }, + { + "description": "Denies the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-size", + "markdownDescription": "Denies the inner_size command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-internal-toggle-maximize", + "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-always-on-top", + "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-closable", + "markdownDescription": "Denies the is_closable command without any pre-configured scope." + }, + { + "description": "Denies the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-decorated", + "markdownDescription": "Denies the is_decorated command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-focused", + "markdownDescription": "Denies the is_focused command without any pre-configured scope." + }, + { + "description": "Denies the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-fullscreen", + "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximizable", + "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximized", + "markdownDescription": "Denies the is_maximized command without any pre-configured scope." + }, + { + "description": "Denies the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimizable", + "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimized", + "markdownDescription": "Denies the is_minimized command without any pre-configured scope." + }, + { + "description": "Denies the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-resizable", + "markdownDescription": "Denies the is_resizable command without any pre-configured scope." + }, + { + "description": "Denies the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-visible", + "markdownDescription": "Denies the is_visible command without any pre-configured scope." + }, + { + "description": "Denies the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-maximize", + "markdownDescription": "Denies the maximize command without any pre-configured scope." + }, + { + "description": "Denies the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-minimize", + "markdownDescription": "Denies the minimize command without any pre-configured scope." + }, + { + "description": "Denies the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-monitor-from-point", + "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Denies the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-position", + "markdownDescription": "Denies the outer_position command without any pre-configured scope." + }, + { + "description": "Denies the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-size", + "markdownDescription": "Denies the outer_size command without any pre-configured scope." + }, + { + "description": "Denies the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-primary-monitor", + "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." + }, + { + "description": "Denies the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-request-user-attention", + "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." + }, + { + "description": "Denies the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scale-factor", + "markdownDescription": "Denies the scale_factor command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-bottom", + "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-top", + "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-background-color", + "markdownDescription": "Denies the set_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-count", + "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-label", + "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." + }, + { + "description": "Denies the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-closable", + "markdownDescription": "Denies the set_closable command without any pre-configured scope." + }, + { + "description": "Denies the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-content-protected", + "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-grab", + "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-icon", + "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-position", + "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-visible", + "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Denies the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-decorations", + "markdownDescription": "Denies the set_decorations command without any pre-configured scope." + }, + { + "description": "Denies the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-effects", + "markdownDescription": "Denies the set_effects command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focus", + "markdownDescription": "Denies the set_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focusable", + "markdownDescription": "Denies the set_focusable command without any pre-configured scope." + }, + { + "description": "Denies the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-fullscreen", + "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-ignore-cursor-events", + "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Denies the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-max-size", + "markdownDescription": "Denies the set_max_size command without any pre-configured scope." + }, + { + "description": "Denies the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-maximizable", + "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-min-size", + "markdownDescription": "Denies the set_min_size command without any pre-configured scope." + }, + { + "description": "Denies the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-minimizable", + "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-overlay-icon", + "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-position", + "markdownDescription": "Denies the set_position command without any pre-configured scope." + }, + { + "description": "Denies the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-progress-bar", + "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Denies the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-resizable", + "markdownDescription": "Denies the set_resizable command without any pre-configured scope." + }, + { + "description": "Denies the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-shadow", + "markdownDescription": "Denies the set_shadow command without any pre-configured scope." + }, + { + "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-simple-fullscreen", + "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size", + "markdownDescription": "Denies the set_size command without any pre-configured scope." + }, + { + "description": "Denies the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size-constraints", + "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Denies the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-skip-taskbar", + "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Denies the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-theme", + "markdownDescription": "Denies the set_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title-bar-style", + "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-visible-on-all-workspaces", + "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "Denies the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-dragging", + "markdownDescription": "Denies the start_dragging command without any pre-configured scope." + }, + { + "description": "Denies the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-resize-dragging", + "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Denies the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-theme", + "markdownDescription": "Denies the theme command without any pre-configured scope." + }, + { + "description": "Denies the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-title", + "markdownDescription": "Denies the title command without any pre-configured scope." + }, + { + "description": "Denies the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-toggle-maximize", + "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unmaximize", + "markdownDescription": "Denies the unmaximize command without any pre-configured scope." + }, + { + "description": "Denies the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unminimize", + "markdownDescription": "Denies the unminimize command without any pre-configured scope." + }, + { + "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`", + "type": "string", + "const": "dialog:default", + "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`" + }, + { + "description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", + "type": "string", + "const": "dialog:allow-ask", + "markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + }, + { + "description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", + "type": "string", + "const": "dialog:allow-confirm", + "markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + }, + { + "description": "Enables the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-message", + "markdownDescription": "Enables the message command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-save", + "markdownDescription": "Enables the save command without any pre-configured scope." + }, + { + "description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", + "type": "string", + "const": "dialog:deny-ask", + "markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + }, + { + "description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", + "type": "string", + "const": "dialog:deny-confirm", + "markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + }, + { + "description": "Denies the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-message", + "markdownDescription": "Denies the message command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-save", + "markdownDescription": "Denies the save command without any pre-configured scope." + }, + { + "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`", + "type": "string", + "const": "shell:default", + "markdownDescription": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`" + }, + { + "description": "Enables the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-execute", + "markdownDescription": "Enables the execute command without any pre-configured scope." + }, + { + "description": "Enables the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-kill", + "markdownDescription": "Enables the kill command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-spawn", + "markdownDescription": "Enables the spawn command without any pre-configured scope." + }, + { + "description": "Enables the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-stdin-write", + "markdownDescription": "Enables the stdin_write command without any pre-configured scope." + }, + { + "description": "Denies the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-execute", + "markdownDescription": "Denies the execute command without any pre-configured scope." + }, + { + "description": "Denies the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-kill", + "markdownDescription": "Denies the kill command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-spawn", + "markdownDescription": "Denies the spawn command without any pre-configured scope." + }, + { + "description": "Denies the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-stdin-write", + "markdownDescription": "Denies the stdin_write command without any pre-configured scope." + } + ] + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "ShellScopeEntryAllowedArg": { + "description": "A command argument allowed to be executed by the webview API.", + "anyOf": [ + { + "description": "A non-configurable argument that is passed to the command in the order it was specified.", + "type": "string" + }, + { + "description": "A variable that is set while calling the command from the webview API.", + "type": "object", + "required": [ + "validator" + ], + "properties": { + "raw": { + "description": "Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.", + "default": false, + "type": "boolean" + }, + "validator": { + "description": "[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ", + "type": "string" + } + }, + "additionalProperties": false + } + ] + }, + "ShellScopeEntryAllowedArgs": { + "description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.", + "anyOf": [ + { + "description": "Use a simple boolean to allow all or disable all arguments to this command configuration.", + "type": "boolean" + }, + { + "description": "A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.", + "type": "array", + "items": { + "$ref": "#/definitions/ShellScopeEntryAllowedArg" + } + } + ] + } + } +} \ No newline at end of file diff --git a/client/src-tauri/capabilities/default.json b/client/src-tauri/capabilities/default.json new file mode 100644 index 0000000..30c6cc7 --- /dev/null +++ b/client/src-tauri/capabilities/default.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-utils/schema.json", + "identifier": "default", + "description": "Default capability for EifelDC", + "windows": ["main"], + "permissions": [ + "core:default", + "shell:allow-open", + "dialog:allow-open", + "dialog:allow-save", + "dialog:allow-message", + "dialog:allow-ask", + "dialog:allow-confirm" + ] +} \ No newline at end of file diff --git a/client/src-tauri/icons/128x128.png b/client/src-tauri/icons/128x128.png index 01f35ac..03e1ee8 100644 Binary files a/client/src-tauri/icons/128x128.png and b/client/src-tauri/icons/128x128.png differ diff --git a/client/src-tauri/icons/128x128@2x.png b/client/src-tauri/icons/128x128@2x.png index 3fe82fc..528ceb8 100644 Binary files a/client/src-tauri/icons/128x128@2x.png and b/client/src-tauri/icons/128x128@2x.png differ diff --git a/client/src-tauri/icons/32x32.png b/client/src-tauri/icons/32x32.png index e267a39..2f75fce 100644 Binary files a/client/src-tauri/icons/32x32.png and b/client/src-tauri/icons/32x32.png differ diff --git a/client/src-tauri/tauri.conf.json b/client/src-tauri/tauri.conf.json index 3f69807..966a67a 100644 --- a/client/src-tauri/tauri.conf.json +++ b/client/src-tauri/tauri.conf.json @@ -1,69 +1,55 @@ { - "build": { - "beforeDevCommand": "npm run dev", - "beforeBuildCommand": "npm run build", - "devPath": "http://localhost:5173", - "distDir": "../src-ui/dist" - }, - "package": { - "productName": "EifelDC", - "version": "0.1.0" - }, - "tauri": { - "allowlist": { - "all": false, - "shell": { "open": true }, - "dialog": { "all": true }, - "window": { "all": true } - }, - "bundle": { - "active": true, - "identifier": "de.eifeldc", - "icon": [ - "icons/32x32.png", - "icons/128x128.png", - "icons/128x128@2x.png", - "icons/icon.icns", - "icons/icon.ico" - ], - "resources": [], - "copyright": "", - "category": "SocialNetworking", - "shortDescription": "EifelDC - Matrix Chat Client", - "longDescription": "EifelDC is a Discord-like Matrix chat client built with Tauri, Svelte and Rust.", - "windows": { - "certificateThumbprint": null, - "digestAlgorithm": "sha256", - "timestampUrl": "" - }, - "macOS": { - "frameworks": [], - "minimumSystemVersion": "10.15", - "exceptionDomain": "", - "signingIdentity": null, - "entitlements": "macos/EifelDC.entitlements", - "providerShortName": null - } - }, - "security": { - "csp": "default-src 'self'; connect-src https: wss: http://localhost:*; img-src https: data:; style-src 'self' 'unsafe-inline'; script-src 'self'" - }, - "windows": [ - { - "title": "EifelDC", - "width": 1280, - "height": 720, - "minWidth": 800, - "minHeight": 600, - "resizable": true, - "fullscreen": false, - "decorations": true, - "transparent": false - } - ], - "systemTray": { - "iconPath": "icons/32x32.png", - "iconAsTemplate": true - } + "$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-cli/schema.json", + "productName": "EifelDC", + "version": "0.1.0", + "identifier": "de.eifeldc", + "build": { + "beforeDevCommand": "npm run dev", + "beforeBuildCommand": "npm run build", + "devUrl": "http://localhost:5173", + "frontendDist": "../src-ui/dist" + }, + "app": { + "windows": [ + { + "title": "EifelDC", + "width": 1280, + "height": 720, + "minWidth": 800, + "minHeight": 600, + "resizable": true, + "fullscreen": false, + "decorations": true, + "transparent": false + } + ], + "security": { + "csp": "default-src 'self'; connect-src https: wss: http://localhost:*; img-src https: data:; style-src 'self' 'unsafe-inline'; script-src 'self'" } + }, + "bundle": { + "active": true, + "icon": [ + "src-tauri/icons/32x32.png", + "src-tauri/icons/128x128.png", + "src-tauri/icons/128x128@2x.png", + "src-tauri/icons/icon.icns", + "src-tauri/icons/icon.ico" + ], + "category": "SocialNetworking", + "shortDescription": "EifelDC - Matrix Chat Client", + "longDescription": "EifelDC is a Discord-like Matrix chat client built with Tauri, Svelte and Rust.", + "macOS": { + "minimumSystemVersion": "10.15", + "entitlements": "macos/EifelDC.entitlements" + }, + "windows": { + "digestAlgorithm": "sha256" + } + }, + "plugins": { + "shell": { + "open": true + } + } } \ No newline at end of file diff --git a/client/src-ui/package-lock.json b/client/src-ui/package-lock.json index 3c5bbd4..fd320e1 100644 --- a/client/src-ui/package-lock.json +++ b/client/src-ui/package-lock.json @@ -7,6 +7,9 @@ "": { "name": "eifeldc-ui", "version": "0.1.0", + "dependencies": { + "livekit-client": "^2.18.7" + }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^3", "@tauri-apps/api": "^1", @@ -32,6 +35,12 @@ "node": ">=6.0.0" } }, + "node_modules/@bufbuild/protobuf": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.1.tgz", + "integrity": "sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -462,6 +471,21 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@livekit/mutex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@livekit/mutex/-/mutex-1.1.1.tgz", + "integrity": "sha512-EsshAucklmpuUAfkABPxJNhzj9v2sG7JuzFDL4ML1oJQSV14sqrpTYnsaOudMAw9yOaW53NU3QQTlUQoRs4czw==", + "license": "Apache-2.0" + }, + "node_modules/@livekit/protocol": { + "version": "1.45.3", + "resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.45.3.tgz", + "integrity": "sha512-WmMxBTsy4dRBqcrswFwUUlgq3Z0nnhOqKR6tX749Rb/PcB1yBMUtrHxZvcsS6qi3/5+86zHeVG+exmu1sZqfJg==", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "^1.10.0" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", @@ -869,6 +893,13 @@ "url": "https://opencollective.com/tauri" } }, + "node_modules/@types/dom-mediacapture-record": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.22.tgz", + "integrity": "sha512-mUMZLK3NvwRLcAAT9qmcK+9p7tpU2FHdDsntR3YI4+GY88XrgG4XiE7u1Q2LAN2/FZOz/tdMDC3GQCR4T8nFuw==", + "license": "MIT", + "peer": true + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1138,6 +1169,15 @@ "@types/estree": "^1.0.0" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -1290,6 +1330,15 @@ "@types/estree": "^1.0.6" } }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -1300,6 +1349,26 @@ "node": ">=6" } }, + "node_modules/livekit-client": { + "version": "2.18.7", + "resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.18.7.tgz", + "integrity": "sha512-8mMNfJ9sPTWT9W5CsV23yJhut4vtw0M4c/AR5diRvLn6XsUDij7Odr6GrI9Oj5/T5vDis4y2JTumMiTweGlqMA==", + "license": "Apache-2.0", + "dependencies": { + "@livekit/mutex": "1.1.1", + "@livekit/protocol": "1.45.3", + "events": "^3.3.0", + "jose": "^6.1.0", + "loglevel": "^1.9.2", + "sdp-transform": "^2.15.0", + "tslib": "2.8.1", + "typed-emitter": "^2.1.0", + "webrtc-adapter": "9.0.5" + }, + "peerDependencies": { + "@types/dom-mediacapture-record": "^1" + } + }, "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", @@ -1307,6 +1376,19 @@ "dev": true, "license": "MIT" }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -1569,6 +1651,16 @@ "fsevents": "~2.3.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -1595,6 +1687,21 @@ "rimraf": "^2.5.2" } }, + "node_modules/sdp": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.2.tgz", + "integrity": "sha512-xZocWwfyp4hkbN4hLWxMjmv2Q8aNa9MhmOZ7L9aCZPT+dZsgRr6wZRrSYE3HTdyk/2pZKPSgqI7ns7Een1xMSA==", + "license": "MIT" + }, + "node_modules/sdp-transform": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.15.0.tgz", + "integrity": "sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==", + "license": "MIT", + "bin": { + "sdp-verify": "checker.js" + } + }, "node_modules/sorcery": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.1.tgz", @@ -1774,9 +1881,17 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, + "node_modules/typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz", + "integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==", + "license": "MIT", + "optionalDependencies": { + "rxjs": "*" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -1866,6 +1981,19 @@ } } }, + "node_modules/webrtc-adapter": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.5.tgz", + "integrity": "sha512-U9vjByy/sK2OMXu5mmfuZFKTMIUQe34c0JXRO+oDrxJTsntdYT2iIFwYMOV7HhMTuktcZLGf2W1N/OcSf9ssWg==", + "license": "BSD-3-Clause", + "dependencies": { + "sdp": "^3.2.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">=3.10.0" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/client/src-ui/package.json b/client/src-ui/package.json index 3ffb12c..b86a8ec 100644 --- a/client/src-ui/package.json +++ b/client/src-ui/package.json @@ -18,5 +18,8 @@ "tslib": "^2", "typescript": "^5.9.3", "vite": "^5" + }, + "dependencies": { + "livekit-client": "^2.18.7" } } diff --git a/client/src-ui/src/App.svelte b/client/src-ui/src/App.svelte index 53bc8a1..b4fed6d 100644 --- a/client/src-ui/src/App.svelte +++ b/client/src-ui/src/App.svelte @@ -6,7 +6,9 @@ import ChatArea from './components/ChatArea.svelte'; import MemberList from './components/MemberList.svelte'; import VoicePanel from './components/VoicePanel.svelte'; - import { currentUser, refreshChannels } from './lib/store'; + import { currentUser, refreshChannels, handleWsEvent, addServer, switchServer } from './lib/store'; + import { voiceRoom, disconnectFromVoice } from './lib/voice'; + import { connectWebSocket, disconnectWebSocket } from './lib/api'; let loggedIn = false; let loading = true; @@ -15,7 +17,21 @@ return typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window; } + async function setupTauriSync() { + if (!isTauri()) return; + try { + const { listen } = await import('@tauri-apps/api/event'); + await listen>('sync-event', (event) => { + handleWsEvent(event.payload as any); + }); + } catch (e) { + console.error('Failed to listen for Tauri sync events', e); + } + } + onMount(async () => { + await setupTauriSync(); + if (isTauri()) { try { const { invoke } = await import('@tauri-apps/api/tauri'); @@ -51,6 +67,7 @@ status: 'online', }); await refreshChannels(); + connectWebSocket(handleWsEvent); } } } catch (e) { @@ -60,6 +77,24 @@ } loading = false; }); + + function handleLogin(e: CustomEvent) { + loggedIn = true; + const homeserver = e.detail.homeserver; + if (homeserver) { + const server = addServer(homeserver); + switchServer(server); + } + if (!isTauri()) { + connectWebSocket(handleWsEvent); + } + } + + function handleLogout() { + disconnectWebSocket(); + disconnectFromVoice(); + loggedIn = false; + } {#if loading} @@ -73,7 +108,7 @@

Loading EifelDC...

{:else if !loggedIn} - (loggedIn = true)} /> + {:else}
diff --git a/client/src-ui/src/components/ChannelSidebar.svelte b/client/src-ui/src/components/ChannelSidebar.svelte index 9b82b24..f7f8523 100644 --- a/client/src-ui/src/components/ChannelSidebar.svelte +++ b/client/src-ui/src/components/ChannelSidebar.svelte @@ -1,7 +1,8 @@ -
+ showEmojiPicker = false} /> + +
# {$currentChannel?.name || 'Kanal auswählen'} @@ -70,49 +323,191 @@ | {$currentChannel.topic} {/if} -
- -
- {#if loading} -
Nachrichten werden geladen...
- {:else if $messages.length === 0} -
Noch keine Nachrichten. Sei der Erste!
- {:else} - {#each $messages as msg (msg.id)} -
-
-
{extractUsername(msg.sender).charAt(0).toUpperCase()}
-
-
-
- {extractUsername(msg.sender)} - {formatTime(msg.timestamp)} -
-
{msg.content}
- {#if Object.keys(msg.reactions).length > 0} -
- {#each Object.entries(msg.reactions) as [emoji, users]} - {emoji} {users.length} - {/each} -
- {/if} -
-
- {/each} + {#if $currentChannel} + {/if}
+ {#if isDragging} +
+
📎 Dateien hier ablegen
+
+ {/if} +
+
+ {#if loading} +
Nachrichten werden geladen...
+ {:else if $messages.length === 0} +
Noch keine Nachrichten. Sei der Erste!
+ {:else} + {#each $messages as msg (msg.id)} +
showContextMenu(e, msg.id)}> +
+
{extractUsername(msg.sender).charAt(0).toUpperCase()}
+
+
+
+ {extractUsername(msg.sender)} + {formatTime(msg.timestamp)} + {#if msg.edited} + (bearbeitet) + {/if} +
+ {#if msg.replyTo} + {@const repliedMsg = $messages.find(m => m.id === msg.replyTo)} +
{ + const el = document.getElementById('msg-' + msg.replyTo); + if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' }); + }}> + {repliedMsg ? extractUsername(repliedMsg.sender) : 'Unbekannt'} + {repliedMsg ? repliedMsg.content : '...'} +
+ {/if} +
+ {#if msg.msgtype === 'm.image' && msg.mediaUrl} + {msg.filename + {#if msg.content && msg.content !== msg.filename} + {msg.content} + {/if} + {:else if (msg.msgtype === 'm.file' || msg.msgtype === 'm.video' || msg.msgtype === 'm.audio') && msg.mediaUrl} +
+ + {#if msg.msgtype === 'm.video'}🎬{:else if msg.msgtype === 'm.audio'}🎵{:else}📄{/if} + + + {msg.filename || msg.content} + + {#if msg.size} + {formatFileSize(msg.size)} + {/if} +
+ {:else} + {msg.content} + {/if} +
+ {#if Object.keys(msg.reactions).length > 0} +
+ {#each Object.entries(msg.reactions) as [emoji, users]} + + {/each} +
+ {/if} + {#if (msg.threadReplyCount || 0) > 0 || msg.threadRoot} + + {/if} +
+
+ {/each} + {/if} +
+ + {#if activeThreadId} +
+
+ Thread + +
+
+ {#if threadLoading} +
Laden...
+ {:else if threadMessages.length === 0} +
Noch keine Antworten im Thread.
+ {:else} + {#each threadMessages as tmsg} +
+
{extractUsername(tmsg.sender).charAt(0).toUpperCase()}
+
+ {extractUsername(tmsg.sender)} + {formatTime(tmsg.timestamp)} +
{tmsg.content}
+
+
+ {/each} + {/if} +
+
+ { if (e.key === 'Enter') handleThreadReply(); }} + /> + +
+
+ {/if} +
+ + {#if contextMenuId} +
+ + + + +
+ {#each quickReactions as emoji} + + {/each} +
+
+ {/if} + + {#if showRoomSettings && $currentChannel} + + {/if} +
{#if $currentChannel} -
- +
+ + + + + {#if replyingTo} +
+ Antwort an {extractUsername(replyingTo.sender)} + {replyingTo.content.slice(0, 60)}{replyingTo.content.length > 60 ? '...' : ''} + +
+ {/if} { if (e.key === 'Enter' && !e.shiftKey) handleSend(); }} + placeholder={editingId ? 'Nachricht bearbeiten...' : replyingTo ? 'Antworten...' : 'Nachricht in #{$currentChannel?.name} senden'} + on:keydown={(e) => { if (e.key === 'Enter' && !e.shiftKey) handleSend(); if (e.key === 'Escape') { if (editingId) cancelEdit(); if (replyingTo) cancelReply(); } }} + on:input={handleInput} /> - + {#if editingId} + + {/if} +
{:else}
Wähle einen Kanal aus
@@ -126,6 +521,29 @@ flex-direction: column; flex: 1; min-width: 0; + position: relative; + } + + .drop-overlay { + position: absolute; + inset: 0; + background: rgba(88, 101, 242, 0.15); + border: 3px dashed var(--accent); + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; + z-index: 50; + pointer-events: none; + } + + .drop-text { + font-size: 1.2rem; + font-weight: 600; + color: var(--accent); + padding: 24px 48px; + background: var(--bg-secondary); + border-radius: var(--radius-md); } .chat-header { @@ -163,6 +581,13 @@ text-overflow: ellipsis; } + .chat-content { + flex: 1; + display: flex; + min-height: 0; + overflow: hidden; + } + .messages { flex: 1; overflow-y: auto; @@ -177,6 +602,7 @@ gap: 12px; padding: 4px 0; border-radius: var(--radius-sm); + position: relative; } .message:hover { @@ -216,16 +642,197 @@ color: var(--text-muted); } + .edited-badge { + font-size: 0.7rem; + color: var(--text-muted); + } + .message-body { font-size: 0.95rem; line-height: 1.4; word-wrap: break-word; } + .message-image { + max-width: 400px; + max-height: 300px; + border-radius: var(--radius-sm); + display: block; + margin: 4px 0; + cursor: pointer; + } + + .message-file { + display: inline-flex; + align-items: center; + gap: 8px; + background: var(--bg-tertiary); + padding: 8px 12px; + border-radius: var(--radius-md); + border: 1px solid var(--border); + max-width: 400px; + } + + .file-icon { + font-size: 1.4rem; + flex-shrink: 0; + } + + .file-link { + color: var(--accent); + text-decoration: none; + font-size: 0.9rem; + word-break: break-all; + } + + .file-link:hover { + text-decoration: underline; + } + + .file-size { + color: var(--text-muted); + font-size: 0.75rem; + flex-shrink: 0; + } + + .settings-btn { + background: none; + border: none; + color: var(--text-secondary); + cursor: pointer; + font-size: 1.1rem; + padding: 0 4px; + margin-left: auto; + } + + .settings-btn:hover { + color: var(--text-primary); + } + + .modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 200; + } + + .modal { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 24px; + min-width: 320px; + max-width: 90vw; + } + + .modal h3 { + margin: 0 0 16px; + font-size: 1.1rem; + } + + .modal-field { + margin-bottom: 12px; + } + + .modal-field label { + display: block; + font-size: 0.85rem; + color: var(--text-secondary); + margin-bottom: 4px; + } + + .modal-field input { + width: 100%; + padding: 8px 12px; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-size: 0.95rem; + outline: none; + } + + .modal-field input:focus { + border-color: var(--accent); + } + + .modal-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 16px; + } + + .btn-primary { + padding: 8px 16px; + background: var(--accent); + color: white; + border: none; + border-radius: var(--radius-sm); + cursor: pointer; + font-weight: 600; + } + + .btn-primary:hover { + background: var(--accent-hover); + } + + .btn-secondary { + padding: 8px 16px; + background: transparent; + color: var(--text-secondary); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + cursor: pointer; + } + + .btn-secondary:hover { + color: var(--text-primary); + border-color: var(--text-secondary); + } + + .reply-quote { + background: var(--bg-tertiary); + border-left: 3px solid var(--accent); + padding: 4px 8px; + border-radius: var(--radius-sm); + margin-bottom: 4px; + cursor: pointer; + font-size: 0.85rem; + max-width: 400px; + overflow: hidden; + } + + .reply-quote:hover { + background: var(--bg-hover); + } + + .reply-author { + font-weight: 600; + font-size: 0.8rem; + color: var(--accent); + display: block; + } + + .reply-text { + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: block; + } + .reactions { display: flex; gap: 4px; margin-top: 4px; + flex-wrap: wrap; } .reaction { @@ -234,12 +841,184 @@ border-radius: var(--radius-sm); font-size: 0.85rem; cursor: pointer; + border: 1px solid var(--border); } .reaction:hover { background: var(--bg-hover); } + .thread-indicator { + background: none; + border: none; + color: var(--accent); + font-size: 0.8rem; + cursor: pointer; + padding: 2px 0; + margin-top: 2px; + } + + .thread-indicator:hover { + text-decoration: underline; + } + + .thread-panel { + width: 360px; + min-width: 360px; + border-left: 1px solid var(--border); + display: flex; + flex-direction: column; + background: var(--bg-primary); + } + + .thread-header { + height: 48px; + padding: 0 12px; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid var(--border); + background: var(--bg-secondary); + } + + .thread-title { + font-weight: 700; + font-size: 1rem; + } + + .thread-close { + background: none; + border: none; + color: var(--text-secondary); + cursor: pointer; + font-size: 1.1rem; + padding: 4px; + } + + .thread-close:hover { + color: var(--text-primary); + } + + .thread-messages { + flex: 1; + overflow-y: auto; + padding: 12px; + display: flex; + flex-direction: column; + gap: 8px; + } + + .thread-msg { + display: flex; + gap: 8px; + padding: 4px 0; + } + + .thread-msg-avatar { + width: 28px; + height: 28px; + border-radius: 50%; + background: var(--bg-tertiary); + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + font-weight: 700; + flex-shrink: 0; + } + + .thread-msg-content { + flex: 1; + min-width: 0; + } + + .thread-msg-author { + font-weight: 600; + font-size: 0.85rem; + margin-right: 6px; + } + + .thread-msg-time { + font-size: 0.7rem; + color: var(--text-muted); + } + + .thread-msg-body { + font-size: 0.9rem; + line-height: 1.4; + word-wrap: break-word; + } + + .thread-input { + padding: 8px 12px; + border-top: 1px solid var(--border); + display: flex; + gap: 6px; + } + + .thread-input input { + flex: 1; + border: 1px solid var(--border); + background: var(--bg-input); + color: var(--text-primary); + padding: 8px; + border-radius: var(--radius-sm); + font-size: 0.9rem; + outline: none; + } + + .thread-input input:focus { + border-color: var(--accent); + } + + .context-menu { + position: fixed; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 4px; + z-index: 100; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + } + + .context-menu button { + display: block; + width: 100%; + padding: 6px 12px; + background: none; + border: none; + color: var(--text-primary); + text-align: left; + cursor: pointer; + border-radius: var(--radius-sm); + font-size: 0.9rem; + } + + .context-menu button:hover { + background: var(--bg-hover); + } + + .reaction-picker { + display: flex; + gap: 2px; + padding: 4px 0; + border-top: 1px solid var(--border); + margin-top: 4px; + } + + .quick-reaction { + background: none; + border: none; + font-size: 1.1rem; + cursor: pointer; + padding: 4px 6px; + border-radius: var(--radius-sm); + } + + .quick-reaction:hover { + background: var(--bg-hover); + } + .input-area { padding: 0 16px 16px; flex-shrink: 0; @@ -247,13 +1026,50 @@ .input-container { display: flex; - align-items: center; + flex-direction: column; background: var(--bg-input); border-radius: var(--radius-md); padding: 4px 8px; } - .input-container input { + .reply-bar { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 4px 2px; + border-left: 3px solid var(--accent); + margin-top: 2px; + } + + .reply-bar-label { + font-size: 0.8rem; + font-weight: 600; + color: var(--accent); + } + + .reply-bar-preview { + font-size: 0.8rem; + color: var(--text-secondary); + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .reply-bar-close { + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + font-size: 0.9rem; + padding: 0 4px; + } + + .reply-bar-close:hover { + color: var(--text-primary); + } + + .input-container > input { flex: 1; border: none; background: transparent; @@ -276,6 +1092,29 @@ color: var(--text-primary); } + .cancel-btn { + color: var(--danger); + } + + .send-btn { + background: var(--accent); + border: none; + color: white; + cursor: pointer; + padding: 6px 10px; + border-radius: var(--radius-sm); + font-size: 0.9rem; + } + + .send-btn:hover { + background: var(--accent-hover); + } + + .send-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + } + .loading, .empty, .no-channel { text-align: center; color: var(--text-secondary); diff --git a/client/src-ui/src/components/EmojiPicker.svelte b/client/src-ui/src/components/EmojiPicker.svelte new file mode 100644 index 0000000..53b3b1c --- /dev/null +++ b/client/src-ui/src/components/EmojiPicker.svelte @@ -0,0 +1,187 @@ + + +{#if show} +
+ +
+ {#each categories as cat} +
+
{cat.name}
+
+ {#each cat.emojis.split('') as emoji} + + {/each} +
+
+ {/each} + {#if filteredCustom.length > 0} +
+
Custom
+
+ {#each filteredCustom as emoji} + + {/each} +
+
+ {/if} +
+
+{/if} + + \ No newline at end of file diff --git a/client/src-ui/src/components/LoginScreen.svelte b/client/src-ui/src/components/LoginScreen.svelte index 2b6cfab..82b578a 100644 --- a/client/src-ui/src/components/LoginScreen.svelte +++ b/client/src-ui/src/components/LoginScreen.svelte @@ -19,7 +19,7 @@ if (mode === 'login') { const result = await login(homeserver, username, password); if (result.success) { - dispatch('login', { userId: result.user_id }); + dispatch('login', { userId: result.user_id, homeserver }); } else { error = result.error || 'Login fehlgeschlagen'; } @@ -30,7 +30,7 @@ } const result = await register(homeserver, username, password); if (result.success) { - dispatch('login', { userId: result.user_id }); + dispatch('login', { userId: result.user_id, homeserver }); } else { error = result.error || 'Registrierung fehlgeschlagen'; } diff --git a/client/src-ui/src/components/ServerSidebar.svelte b/client/src-ui/src/components/ServerSidebar.svelte index 8045be5..ff6fbe6 100644 --- a/client/src-ui/src/components/ServerSidebar.svelte +++ b/client/src-ui/src/components/ServerSidebar.svelte @@ -1,29 +1,47 @@
- - -
- {#each $servers as server} {/each} -
+ + {#if $currentUser} +
+
{extractUsername($currentUser.id).charAt(0).toUpperCase()}
+
+ {/if}
+{#if showAddServer} + +{/if} + \ No newline at end of file diff --git a/client/src-ui/src/components/VoicePanel.svelte b/client/src-ui/src/components/VoicePanel.svelte index 095bcdf..a43ed34 100644 --- a/client/src-ui/src/components/VoicePanel.svelte +++ b/client/src-ui/src/components/VoicePanel.svelte @@ -1,57 +1,57 @@ -{#if $voiceState.channelId} +{#if $voiceRoom.connected || $voiceRoom.connecting}
- - - - - Sprachverbunden + {#if $voiceRoom.connecting} +
+ Verbinden... + {:else} + + + + +
+ Sprachverbunden + {extractUsername($voiceRoom.roomId || '')} +
+ {/if} +
+
+ {#each $voiceParticipants as participant} +
+ {extractUsername(participant.userId).charAt(0).toUpperCase()} +
+ {/each}
- -
{/if} +{#if $voiceRoom.error} +
+ {$voiceRoom.error} + +
+{/if} + \ No newline at end of file diff --git a/client/src-ui/src/lib/api.ts b/client/src-ui/src/lib/api.ts index 2970223..16672dc 100644 --- a/client/src-ui/src/lib/api.ts +++ b/client/src-ui/src/lib/api.ts @@ -12,6 +12,8 @@ export interface RoomInfo { is_encrypted: boolean; member_count: number; topic: string | null; + unread_notifications: number; + unread_messages: number; } export interface MessageInfo { @@ -20,6 +22,15 @@ export interface MessageInfo { body: string; timestamp: number; reply_to: string | null; + edited?: boolean; + reactions?: Record; + msgtype?: string; + media_url?: string | null; + filename?: string | null; + mimetype?: string | null; + width?: number | null; + height?: number | null; + size?: number | null; } export interface RoleInfo { @@ -50,6 +61,23 @@ export interface VoiceStateInfo { streaming: boolean; } +export interface VoiceJoinResult { + room_id: string; + livekit_url: string; + livekit_token: string; +} + +export interface VoiceToggleResult { + muted: boolean; + deafened: boolean; +} + +export interface VoiceParticipantInfo { + user_id: string; + muted: boolean; + deafened: boolean; +} + export interface PresenceInfo { user_id: string; status: string; @@ -196,7 +224,7 @@ export async function getPresence(userId: string): Promise { return httpGet(`/api/presence/${encodeURIComponent(userId)}`); } -export async function joinVoiceChannel(roomId: string): Promise { +export async function joinVoiceChannel(roomId: string): Promise { if (isTauri()) { return tauriInvoke('join_voice_channel', { roomId }); } @@ -210,20 +238,27 @@ export async function leaveVoiceChannel(roomId: string): Promise { return httpPost('/api/voice/leave', { room_id: roomId }); } -export async function toggleMute(roomId: string): Promise { +export async function toggleMute(roomId: string): Promise { if (isTauri()) { return tauriInvoke('toggle_mute', { roomId }); } return httpPost('/api/voice/toggle-mute', { room_id: roomId }); } -export async function toggleDeafen(roomId: string): Promise { +export async function toggleDeafen(roomId: string): Promise { if (isTauri()) { return tauriInvoke('toggle_deafen', { roomId }); } return httpPost('/api/voice/toggle-deafen', { room_id: roomId }); } +export async function getVoiceParticipants(): Promise { + if (isTauri()) { + return tauriInvoke('get_voice_participants', {}); + } + return httpGet('/api/voice/participants'); +} + export async function getRoles(roomId: string): Promise { if (isTauri()) { return tauriInvoke('get_roles', { roomId }); @@ -250,4 +285,343 @@ export async function getPermissions(roomId: string, userId: string): Promise { + if (isTauri()) { + return tauriInvoke('edit_message', { roomId, eventId, newContent }); + } + const result = await httpPost(`/api/rooms/${encodeURIComponent(roomId)}/edit`, { event_id: eventId, new_content: newContent }); + return result; +} + +export async function deleteMessage(roomId: string, eventId: string, reason?: string): Promise { + if (isTauri()) { + return tauriInvoke('delete_message', { roomId, eventId, reason }); + } + const result = await httpPost(`/api/rooms/${encodeURIComponent(roomId)}/delete/${encodeURIComponent(eventId)}`, { reason }); + return result; +} + +export async function reactToMessage(roomId: string, eventId: string, key: string): Promise { + if (isTauri()) { + return tauriInvoke('react_to_message', { roomId, eventId, key }); + } + const result = await httpPost(`/api/rooms/${encodeURIComponent(roomId)}/react`, { event_id: eventId, key }); + return result; +} + +export async function setTyping(roomId: string, typing: boolean): Promise { + if (isTauri()) { + return tauriInvoke('set_typing', { roomId, typing }); + } + return httpPost(`/api/rooms/${encodeURIComponent(roomId)}/typing`, { typing }); +} + +export type WsEventType = 'message' | 'message_edited' | 'message_deleted' | 'reaction' | 'room_joined' | 'room_left' | 'presence' | 'typing' | 'connected' | 'voice_state_update' | 'voice_user_joined' | 'voice_user_left' | 'thread_reply'; + +export interface WsMessage { + type: WsEventType; + room_id?: string; + event_id?: string; + sender?: string; + body?: string; + timestamp?: number; + reply_to?: string | null; + name?: string; + user_id?: string; + status?: string; + status_msg?: string | null; + typing?: boolean; + new_body?: string; + redacts?: string; + key?: string; + muted?: boolean; + deafened?: boolean; + root_event_id?: string; + msgtype?: string | null; + media_url?: string | null; + filename?: string | null; + mimetype?: string | null; +} + +export type WsEventHandler = (event: WsMessage) => void; + +let ws: WebSocket | null = null; +let wsReconnectTimer: ReturnType | null = null; +let wsHandler: WsEventHandler | null = null; + +export function connectWebSocket(handler: WsEventHandler): void { + wsHandler = handler; + doConnect(); +} + +function doConnect(): void { + if (isTauri()) return; + const token = getToken(); + if (!token) return; + + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const url = `${protocol}//${window.location.host}/api/ws?token=${encodeURIComponent(token)}`; + + ws = new WebSocket(url); + + ws.onopen = () => { + console.log('[WS] Connected'); + }; + + ws.onmessage = (event) => { + try { + const data: WsMessage = JSON.parse(event.data); + if (wsHandler) { + wsHandler(data); + } + } catch (e) { + console.error('[WS] Failed to parse message', e); + } + }; + + ws.onclose = () => { + console.log('[WS] Disconnected, reconnecting in 3s...'); + scheduleReconnect(); + }; + + ws.onerror = () => { + ws?.close(); + }; +} + +function scheduleReconnect(): void { + if (wsReconnectTimer) clearTimeout(wsReconnectTimer); + wsReconnectTimer = setTimeout(() => { + doConnect(); + }, 3000); +} + +export function disconnectWebSocket(): void { + if (wsReconnectTimer) { + clearTimeout(wsReconnectTimer); + wsReconnectTimer = null; + } + if (ws) { + ws.onclose = null; + ws.close(); + ws = null; + } + wsHandler = null; +} + +export interface ThreadInfo { + root_event_id: string; + root_sender: string; + root_body: string; + root_timestamp: number; + reply_count: number; + last_reply_event_id: string | null; + last_reply_sender: string | null; + last_reply_body: string | null; + last_reply_timestamp: number | null; +} + +export interface ThreadMessageInfo { + event_id: string; + sender: string; + body: string; + timestamp: number; +} + +export async function getThreads(roomId: string): Promise { + if (isTauri()) { + return tauriInvoke('get_threads', { roomId }); + } + return httpGet(`/api/rooms/${encodeURIComponent(roomId)}/threads`); +} + +export async function getThreadMessages(roomId: string, threadId: string, limit: number = 50): Promise { + if (isTauri()) { + return tauriInvoke('get_thread_messages', { roomId, threadId, limit }); + } + return httpGet(`/api/rooms/${encodeURIComponent(roomId)}/threads/${encodeURIComponent(threadId)}?limit=${limit}`); +} + +export async function sendThreadReply(roomId: string, threadId: string, message: string): Promise { + if (isTauri()) { + return tauriInvoke('send_thread_reply', { roomId, threadId, message }); + } + const result = await httpPost(`/api/rooms/${encodeURIComponent(roomId)}/threads/${encodeURIComponent(threadId)}/reply`, { message }); + return result; +} + +export async function sendReply(roomId: string, replyTo: string, message: string): Promise { + if (isTauri()) { + return tauriInvoke('send_reply', { roomId, replyTo, message }); + } + const result = await httpPost(`/api/rooms/${encodeURIComponent(roomId)}/reply`, { message, reply_to: replyTo }); + return result; +} + +export function getMediaUrl(mediaUrl: string | null | undefined): string | null { + if (!mediaUrl) return null; + const mxcMatch = mediaUrl.match(/^mxc:\/\/([^/]+)\/(.+)$/); + if (!mxcMatch) return null; + const serverName = mxcMatch[1]; + const mediaId = mxcMatch[2]; + const token = getToken(); + return `${getApiBase()}/api/media/${encodeURIComponent(serverName + '/' + mediaId)}`; +} + +export interface UploadResult { + event_id: string; + media_url: string | null; + filename: string; + mimetype: string | null; + size: number | null; +} + +export async function uploadFile(roomId: string, file: File): Promise { + const formData = new FormData(); + formData.append('file', file); + + if (isTauri()) { + const { invoke } = await import('@tauri-apps/api/tauri'); + const arrayBuffer = await file.arrayBuffer(); + const bytes = Array.from(new Uint8Array(arrayBuffer)); + return invoke('upload_file', { roomId, fileName: file.name, mimeType: file.type, bytes }); + } + + const res = await fetch(`${getApiBase()}/api/rooms/${encodeURIComponent(roomId)}/upload`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${getToken()}` }, + body: formData, + }); + if (res.status === 401) { clearToken(); throw new Error('Unauthorized'); } + return res.json(); +} + +// Profile API + +export interface UserProfile { + user_id: string; + display_name: string | null; + avatar_url: string | null; +} + +export async function getOwnProfile(): Promise { + if (isTauri()) { + return tauriInvoke('get_own_profile', {}); + } + return httpGet('/api/profile/me'); +} + +export async function getUserProfile(userId: string): Promise { + if (isTauri()) { + return tauriInvoke('get_user_profile', { userId }); + } + return httpGet(`/api/profile/${encodeURIComponent(userId)}`); +} + +export async function setDisplayName(displayName: string): Promise { + if (isTauri()) { + return tauriInvoke('set_display_name', { displayName }); + } + const res = await httpPost('/api/profile/displayname', { display_name: displayName }); + return res; +} + +export async function uploadAvatar(file: File): Promise { + const formData = new FormData(); + formData.append('file', file); + + if (isTauri()) { + const { invoke } = await import('@tauri-apps/api/tauri'); + const arrayBuffer = await file.arrayBuffer(); + const bytes = Array.from(new Uint8Array(arrayBuffer)); + return invoke('upload_avatar', { fileName: file.name, mimeType: file.type, bytes }); + } + + const res = await fetch(`${getApiBase()}/api/profile/avatar`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${getToken()}` }, + body: formData, + }); + if (res.status === 401) { clearToken(); throw new Error('Unauthorized'); } + return res.json(); +} + +// Room Settings API + +export async function setRoomName(roomId: string, name: string): Promise { + if (isTauri()) { + return tauriInvoke('set_room_name', { roomId, name }); + } + const res = await httpPost(`/api/rooms/${encodeURIComponent(roomId)}/name`, { name }); + return res; +} + +export async function setRoomTopic(roomId: string, topic: string): Promise { + if (isTauri()) { + return tauriInvoke('set_room_topic', { roomId, topic }); + } + const res = await httpPost(`/api/rooms/${encodeURIComponent(roomId)}/topic`, { topic }); + return res; +} + +export async function setRoomAvatar(roomId: string, file: File): Promise { + const formData = new FormData(); + formData.append('file', file); + + if (isTauri()) { + const { invoke } = await import('@tauri-apps/api/tauri'); + const arrayBuffer = await file.arrayBuffer(); + const bytes = Array.from(new Uint8Array(arrayBuffer)); + return invoke('set_room_avatar', { roomId, fileName: file.name, mimeType: file.type, bytes }); + } + + const res = await fetch(`${getApiBase()}/api/rooms/${encodeURIComponent(roomId)}/avatar`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${getToken()}` }, + body: formData, + }); + if (res.status === 401) { clearToken(); throw new Error('Unauthorized'); } + return res.json(); +} + +// Emoji API + +export interface CustomEmoji { + id: string; + name: string; + url: string; + category: string; + animated: boolean; +} + +export async function getCustomEmoji(roomId: string): Promise { + if (isTauri()) { + return tauriInvoke('get_custom_emoji', { roomId }); + } + return httpGet(`/api/rooms/${encodeURIComponent(roomId)}/emoji`); +} + +// Unread API + +export interface UnreadInfo { + room_id: string; + unread_notifications: number; + unread_messages: number; +} + +export async function getUnreadCounts(): Promise { + if (isTauri()) { + return tauriInvoke('get_joined_rooms', {}); + } + return httpGet('/api/rooms/unread'); +} + +export async function markRoomRead(roomId: string, eventId: string): Promise { + if (isTauri()) { + return tauriInvoke('mark_room_read', { roomId, eventId }); + } + const res = await httpPost(`/api/rooms/${encodeURIComponent(roomId)}/read`, { event_id: eventId }); + return res; } \ No newline at end of file diff --git a/client/src-ui/src/lib/store.ts b/client/src-ui/src/lib/store.ts index 2958771..a8813d1 100644 --- a/client/src-ui/src/lib/store.ts +++ b/client/src-ui/src/lib/store.ts @@ -1,5 +1,6 @@ import { writable, derived } from 'svelte/store'; -import { invoke } from '@tauri-apps/api/tauri'; +import * as api from './api'; +import type { UserProfile } from './api'; interface User { id: string; @@ -32,13 +33,23 @@ interface Message { replyTo: string | null; isBot: boolean; reactions: Record; + edited?: boolean; + threadRoot?: string; + threadReplyCount?: number; + msgtype?: string; + mediaUrl?: string | null; + filename?: string | null; + mimetype?: string | null; + width?: number | null; + height?: number | null; + size?: number | null; } interface Server { id: string; name: string; + homeserver: string; iconUrl: string | null; - roles: Role[]; } interface Role { @@ -60,20 +71,93 @@ interface Member { export const currentUser = writable(null); export const currentServer = writable(null); export const currentChannel = writable(null); -export const servers = writable([]); +export const servers = writable(loadServers()); export const channels = writable([]); +export const unreadCounts = writable>({}); export const messages = writable([]); export const members = writable([]); +export const userProfile = writable(null); + +function loadServers(): Server[] { + try { + const raw = localStorage.getItem('eifeldc_servers'); + if (raw) return JSON.parse(raw); + } catch {} + return []; +} + +function saveServers(list: Server[]) { + localStorage.setItem('eifeldc_servers', JSON.stringify(list)); +} + +export function addServer(homeserver: string, name?: string): Server { + const id = btoa(homeserver).replace(/[^a-zA-Z0-9]/g, '').slice(0, 12); + const server: Server = { + id, + name: name || new URL(homeserver).hostname, + homeserver, + iconUrl: null, + }; + servers.update(list => { + if (list.some(s => s.homeserver === homeserver)) return list; + const updated = [...list, server]; + saveServers(updated); + return updated; + }); + return server; +} + +export function removeServer(id: string) { + servers.update(list => { + const updated = list.filter(s => s.id !== id); + saveServers(updated); + return updated; + }); + currentServer.update(cs => { + if (cs && cs.id === id) return null; + return cs; + }); +} + +export function switchServer(server: Server) { + currentServer.set(server); + currentChannel.set(null); + messages.set([]); + members.set([]); + channels.set([]); + refreshChannels(); + refreshProfile(); +} + +export async function refreshProfile() { + try { + const profile = await api.getOwnProfile(); + userProfile.set(profile); + } catch (e) { + console.error('Failed to load profile', e); + } +} + +export async function updateDisplayName(name: string) { + try { + await api.setDisplayName(name); + await refreshProfile(); + } catch (e) { + console.error('Failed to update display name', e); + } +} export const voiceState = writable<{ channelId: string | null; muted: boolean; deafened: boolean; streaming: boolean; + participants: { userId: string; muted: boolean; deafened: boolean }[]; }>({ channelId: null, muted: false, deafened: false, streaming: false, + participants: [], }); export const sortedMembers = derived(members, ($members) => { @@ -91,21 +175,185 @@ export const voiceChannels = derived(channels, ($channels) => $channels.filter((c) => c.type === 'voice') ); -export async function getJoinedRooms(): Promise { - return invoke('get_joined_rooms'); -} - export async function refreshChannels() { try { - const rooms = await getJoinedRooms(); - channels.set(rooms.map(r => ({ - id: r.room_id, - name: r.name || r.room_id, - type: 'text' as const, - topic: null, - parentId: null, - }))); + const rooms = await api.getJoinedRooms(); + const unreads: Record = {}; + channels.set(rooms.map(r => { + const count = r.unread_notifications || r.unread_messages || 0; + if (count > 0) unreads[r.room_id] = count; + return { + id: r.room_id, + name: r.name || r.room_id, + type: (r.name || '').startsWith('🔊') || (r.name || '').toLowerCase().startsWith('voice') ? 'voice' as const : 'text' as const, + topic: r.topic || null, + parentId: null, + }; + })); + unreadCounts.set(unreads); } catch (e) { console.error('Failed to refresh channels', e); } +} + +export function handleWsEvent(event: api.WsMessage): void { + switch (event.type) { + case 'message': + if (!event.room_id || !event.event_id) break; + if (event.room_id !== $currentChannel?.id) { + unreadCounts.update(counts => { + counts[event.room_id!] = (counts[event.room_id!] || 0) + 1; + return counts; + }); + } + const newMsg: Message = { + id: event.event_id, + sender: event.sender || '', + content: event.body || '', + timestamp: event.timestamp || Date.now(), + replyTo: event.reply_to || null, + isBot: false, + reactions: {}, + msgtype: event.msgtype || 'm.text', + mediaUrl: event.media_url || null, + filename: event.filename || null, + mimetype: event.mimetype || null, + }; + messages.update(existing => { + if (existing.some(m => m.id === newMsg.id)) return existing; + return [...existing, newMsg]; + }); + break; + + case 'message_edited': + if (!event.event_id || !event.new_body) break; + messages.update(existing => + existing.map(m => + m.id === event.event_id ? { ...m, content: event.new_body!, edited: true } : m + ) + ); + break; + + case 'message_deleted': + if (!event.redacts) break; + messages.update(existing => existing.filter(m => m.id !== event.redacts)); + break; + + case 'reaction': + if (!event.event_id || !event.key || !event.sender) break; + messages.update(existing => + existing.map(m => { + if (m.id === event.event_id) { + const reactions = { ...m.reactions }; + if (!reactions[event.key!]) { + reactions[event.key!] = []; + } + if (!reactions[event.key!].includes(event.sender!)) { + reactions[event.key!].push(event.sender!); + } + return { ...m, reactions }; + } + return m; + }) + ); + break; + + case 'room_joined': + if (!event.room_id) break; + channels.update(existing => { + if (existing.some(c => c.id === event.room_id)) { + return existing.map(c => + c.id === event.room_id ? { ...c, name: event.name || c.name } : c + ); + } + return [...existing, { + id: event.room_id!, + name: event.name || event.room_id!, + type: 'text' as const, + topic: null, + parentId: null, + }]; + }); + break; + + case 'room_left': + if (!event.room_id) break; + channels.update(existing => existing.filter(c => c.id !== event.room_id)); + break; + + case 'presence': + if (!event.user_id) break; + const statusMap: Record = { + online: 'online', + idle: 'idle', + offline: 'offline', + }; + const newStatus = (event.status && statusMap[event.status]) || 'offline'; + members.update(existing => + existing.map(m => + m.id === event.user_id ? { ...m, status: newStatus } : m + ) + ); + currentUser.update(u => { + if (u && u.id === event.user_id) { + return { ...u, status: newStatus }; + } + return u; + }); + break; + + case 'typing': + break; + + case 'voice_user_joined': + if (!event.room_id || !event.user_id) break; + voiceState.update(s => { + if (s.channelId !== event.room_id) return s; + if (s.participants.some(p => p.userId === event.user_id)) return s; + return { + ...s, + participants: [...s.participants, { userId: event.user_id!, muted: false, deafened: false }], + }; + }); + break; + + case 'voice_user_left': + if (!event.room_id || !event.user_id) break; + voiceState.update(s => { + if (s.channelId !== event.room_id) return s; + return { + ...s, + participants: s.participants.filter(p => p.userId !== event.user_id), + }; + }); + break; + + case 'voice_state_update': + if (!event.room_id || !event.user_id) break; + voiceState.update(s => { + if (s.channelId !== event.room_id) return s; + return { + ...s, + participants: s.participants.map(p => + p.userId === event.user_id ? { ...p, muted: event.muted ?? p.muted, deafened: event.deafened ?? p.deafened } : p + ), + }; + }); + break; + + case 'thread_reply': + if (!event.room_id || !event.root_event_id) break; + messages.update(existing => + existing.map(m => { + if (m.id === event.root_event_id) { + return { ...m, threadReplyCount: (m.threadReplyCount || 0) + 1 }; + } + if (m.threadRoot === event.root_event_id) { + return { ...m, threadReplyCount: (m.threadReplyCount || 0) + 1 }; + } + return m; + }) + ); + break; + } } \ No newline at end of file diff --git a/client/src-ui/src/lib/voice.ts b/client/src-ui/src/lib/voice.ts new file mode 100644 index 0000000..467335a --- /dev/null +++ b/client/src-ui/src/lib/voice.ts @@ -0,0 +1,213 @@ +import { Room, Track, ConnectionState, LocalParticipant, RemoteParticipant, RemoteTrackPublication, TrackEvent, RoomEvent } from 'livekit-client'; +import { writable, derived, get } from 'svelte/store'; +import * as api from './api'; + +export interface VoiceParticipantState { + userId: string; + muted: boolean; + deafened: boolean; + speaking: boolean; +} + +interface VoiceState { + connected: boolean; + connecting: boolean; + roomId: string | null; + room: Room | null; + localMuted: boolean; + localDeafened: boolean; + participants: VoiceParticipantState[]; + error: string | null; +} + +export const voiceRoom = writable({ + connected: false, + connecting: false, + roomId: null, + room: null, + localMuted: false, + localDeafened: false, + participants: [], + error: null, +}); + +export const isConnected = derived(voiceRoom, ($v) => $v.connected); +export const voiceParticipants = derived(voiceRoom, ($v) => $v.participants); +export const voiceError = derived(voiceRoom, ($v) => $v.error); + +function updateParticipantsFromRoom(room: Room): VoiceParticipantState[] { + const participants: VoiceParticipantState[] = []; + + const local = room.localParticipant; + participants.push({ + userId: local.identity || local.name || 'unknown', + muted: local.isMicrophoneEnabled === false, + deafened: false, + speaking: local.isSpeaking, + }); + + for (const [, remote] of room.remoteParticipants) { + participants.push({ + userId: remote.identity || remote.name || 'unknown', + muted: !remote.isMicrophoneEnabled, + deafened: false, + speaking: remote.isSpeaking, + }); + } + + return participants; +} + +export async function connectToVoice(roomId: string): Promise { + const current = get(voiceRoom); + if (current.connecting || current.connected) { + if (current.roomId === roomId) return; + await disconnectFromVoice(); + } + + voiceRoom.update(s => ({ ...s, connecting: true, error: null })); + + try { + const result = await api.joinVoiceChannel(roomId); + + const room = new Room(); + + room.on(RoomEvent.TrackMuted, () => { + voiceRoom.update(s => ({ + ...s, + participants: updateParticipantsFromRoom(room), + })); + }); + + room.on(RoomEvent.TrackUnmuted, () => { + voiceRoom.update(s => ({ + ...s, + participants: updateParticipantsFromRoom(room), + })); + }); + + room.on(RoomEvent.ActiveSpeakersChanged, () => { + voiceRoom.update(s => ({ + ...s, + participants: updateParticipantsFromRoom(room), + })); + }); + + room.on(RoomEvent.ParticipantConnected, () => { + voiceRoom.update(s => ({ + ...s, + participants: updateParticipantsFromRoom(room), + })); + }); + + room.on(RoomEvent.ParticipantDisconnected, () => { + voiceRoom.update(s => ({ + ...s, + participants: updateParticipantsFromRoom(room), + })); + }); + + room.on(RoomEvent.Disconnected, () => { + voiceRoom.update(s => ({ + ...s, + connected: false, + roomId: null, + room: null, + participants: [], + })); + }); + + room.on(RoomEvent.Reconnecting, () => { + voiceRoom.update(s => ({ ...s, connecting: true })); + }); + + room.on(RoomEvent.Reconnected, () => { + voiceRoom.update(s => ({ ...s, connecting: false, connected: true })); + }); + + await room.connect(result.livekit_url, result.livekit_token); + + const local = room.localParticipant; + await local.setCameraEnabled(false); + await local.setMicrophoneEnabled(true); + + voiceRoom.update(s => ({ + ...s, + connected: true, + connecting: false, + roomId: result.room_id, + room, + participants: updateParticipantsFromRoom(room), + })); + } catch (e: any) { + voiceRoom.update(s => ({ + ...s, + connecting: false, + error: e.message || 'Verbindung fehlgeschlagen', + })); + } +} + +export async function disconnectFromVoice(): Promise { + const current = get(voiceRoom); + + if (current.room) { + await current.room.disconnect(); + } + + if (current.roomId) { + try { + await api.leaveVoiceChannel(current.roomId); + } catch (e) { + console.error('Failed to leave voice channel', e); + } + } + + voiceRoom.update(s => ({ + ...s, + connected: false, + connecting: false, + roomId: null, + room: null, + localMuted: false, + localDeafened: false, + participants: [], + error: null, + })); +} + +export async function toggleMute(): Promise { + const current = get(voiceRoom); + if (!current.room || !current.roomId) return; + + const result = await api.toggleMute(current.roomId); + + if (current.room) { + const local = current.room.localParticipant; + await local.setMicrophoneEnabled(!result.muted); + } + + voiceRoom.update(s => ({ + ...s, + localMuted: result.muted, + localDeafened: result.deafened, + })); +} + +export async function toggleDeafen(): Promise { + const current = get(voiceRoom); + if (!current.room || !current.roomId) return; + + const result = await api.toggleDeafen(current.roomId); + + if (current.room) { + const local = current.room.localParticipant; + await local.setMicrophoneEnabled(!result.muted); + } + + voiceRoom.update(s => ({ + ...s, + localMuted: result.muted, + localDeafened: result.deafened, + })); +} \ No newline at end of file diff --git a/client/src/commands/auth.rs b/client/src/commands/auth.rs index 6845329..71efe11 100644 --- a/client/src/commands/auth.rs +++ b/client/src/commands/auth.rs @@ -1,107 +1,151 @@ -use matrix_sdk::Client; -use serde::Serialize; -use tauri::State; use crate::state::AppState; +use serde::{Deserialize, Serialize}; +use tauri::State; #[derive(Serialize)] pub struct LoginResult { + pub success: bool, + pub user_id: String, + pub error: Option, +} + +#[derive(Deserialize, Serialize)] +struct ServerLoginRequest { + homeserver: String, + username: String, + password: String, +} + +#[derive(Deserialize)] +struct ServerLoginResponse { success: bool, user_id: String, + token: Option, error: Option, } +#[derive(Deserialize, Serialize)] +struct ServerRegisterRequest { + homeserver: String, + username: String, + password: String, +} + #[tauri::command] pub async fn login( - state: State<'_, crate::state::AppState>, + state: State<'_, AppState>, homeserver: String, username: String, password: String, ) -> Result { - let client = Client::builder() - .homeserver_url(&homeserver) - .build() - .await - .map_err(|e| e.to_string())?; + let server_url = format!("https://{}", homeserver.trim_end_matches('/')); + let client = reqwest::Client::new(); - client - .matrix_auth() - .login_username(&username, &password) + let res = client + .post(format!("{}/api/login", server_url.trim_end_matches('/'))) + .json(&ServerLoginRequest { + homeserver, + username, + password, + }) .send() .await .map_err(|e| e.to_string())?; - let user_id = client - .user_id() - .map(|u| u.to_string()) - .unwrap_or_default(); + let data: ServerLoginResponse = res.json().await.map_err(|e| e.to_string())?; + + if !data.success { + return Ok(LoginResult { + success: false, + user_id: String::new(), + error: data.error, + }); + } let mut s = state.write().await; - s.client = Some(client); s.logged_in = true; - s.user_id = Some(user_id.clone()); + s.user_id = Some(data.user_id.clone()); + s.auth_token = data.token.clone(); + s.server_url = Some(server_url.trim_end_matches('/').to_string()); Ok(LoginResult { success: true, - user_id, + user_id: data.user_id, error: None, }) } #[tauri::command] -pub async fn logout(state: State<'_, crate::state::AppState>) -> Result { - let mut s = state.write().await; - if let Some(client) = s.client.take() { - let _ = client.matrix_auth().logout().await; +pub async fn logout(state: State<'_, AppState>) -> Result { + let (server_url, token) = { + let s = state.read().await; + (s.server_url.clone(), s.auth_token.clone()) + }; + + if let (Some(url), Some(tok)) = (server_url, token) { + let client = reqwest::Client::new(); + let _ = client + .post(format!("{}/api/logout", url.trim_end_matches('/'))) + .header("Authorization", format!("Bearer {}", tok)) + .send() + .await; } + + let mut s = state.write().await; s.logged_in = false; s.user_id = None; + s.auth_token = None; + s.server_url = None; + Ok(true) } #[tauri::command] pub async fn register( - state: State<'_, crate::state::AppState>, + state: State<'_, AppState>, homeserver: String, username: String, password: String, ) -> Result { - let client = Client::builder() - .homeserver_url(&homeserver) - .build() + let server_url = format!("https://{}", homeserver.trim_end_matches('/')); + let client = reqwest::Client::new(); + + let res = client + .post(format!("{}/api/register", server_url.trim_end_matches('/'))) + .json(&ServerRegisterRequest { + homeserver, + username, + password, + }) + .send() .await .map_err(|e| e.to_string())?; - let mut request = matrix_sdk::ruma::api::client::account::register::v3::Request::new(); - request.username = Some(username); - request.password = Some(password); + let data: ServerLoginResponse = res.json().await.map_err(|e| e.to_string())?; - client - .matrix_auth() - .register(request) - .await - .map_err(|e| e.to_string())?; - - let user_id = client - .user_id() - .map(|u| u.to_string()) - .unwrap_or_default(); + if !data.success { + return Ok(LoginResult { + success: false, + user_id: String::new(), + error: data.error, + }); + } let mut s = state.write().await; - s.client = Some(client); s.logged_in = true; - s.user_id = Some(user_id.clone()); + s.user_id = Some(data.user_id.clone()); + s.auth_token = data.token.clone(); + s.server_url = Some(server_url.trim_end_matches('/').to_string()); Ok(LoginResult { success: true, - user_id, + user_id: data.user_id, error: None, }) } #[tauri::command] -pub async fn get_current_user( - state: State<'_, crate::state::AppState>, -) -> Result, String> { +pub async fn get_current_user(state: State<'_, AppState>) -> Result, String> { let s = state.read().await; Ok(s.user_id.clone()) -} \ No newline at end of file +} diff --git a/client/src/commands/emoji.rs b/client/src/commands/emoji.rs index 4d7486b..108296d 100644 --- a/client/src/commands/emoji.rs +++ b/client/src/commands/emoji.rs @@ -1,10 +1,8 @@ -use matrix_sdk::ruma::room_id; -use serde::Serialize; -use tauri::State; use crate::state::AppState; -use std::path::Path; +use serde::{Deserialize, Serialize}; +use tauri::State; -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct CustomEmoji { id: String, name: String, @@ -13,6 +11,76 @@ pub struct CustomEmoji { animated: bool, } +#[tauri::command] +pub async fn get_custom_emoji( + state: State<'_, AppState>, + room_id: String, +) -> Result, String> { + let s = state.read().await; + let server_url = s.server_url.clone().ok_or("Not logged in")?; + let token = s.auth_token.clone().ok_or("Not logged in")?; + drop(s); + + let client = reqwest::Client::new(); + let res = client + .get(format!( + "{}/api/rooms/{}/emoji", + server_url.trim_end_matches('/'), + room_id + )) + .header("Authorization", format!("Bearer {}", token)) + .send() + .await + .map_err(|e| e.to_string())?; + + res.json().await.map_err(|e| e.to_string()) +} + +#[derive(Deserialize)] +struct UploadEmojiResponse { + id: String, + name: String, + url: String, + category: String, + animated: bool, +} + +#[tauri::command] +pub async fn upload_emoji( + state: State<'_, AppState>, + room_id: String, + name: String, + image_path: String, +) -> Result { + let s = state.read().await; + let server_url = s.server_url.clone().ok_or("Not logged in")?; + let token = s.auth_token.clone().ok_or("Not logged in")?; + drop(s); + + let client = reqwest::Client::new(); + let res = client + .post(format!( + "{}/api/rooms/{}/emoji/upload", + server_url.trim_end_matches('/'), + room_id + )) + .header("Authorization", format!("Bearer {}", token)) + .json(&serde_json::json!({ "name": name, "image_path": image_path })) + .send() + .await + .map_err(|e| e.to_string())?; + + let data: UploadEmojiResponse = res.json().await.map_err(|e| e.to_string())?; + + Ok(CustomEmoji { + id: data.id, + name: data.name, + url: data.url, + category: data.category, + animated: data.animated, + }) +} + #[derive(Serialize)] pub struct StickerPack { id: String, @@ -28,66 +96,6 @@ pub struct Sticker { } #[tauri::command] -pub async fn get_custom_emoji( - state: State<'_, crate::state::AppState>, - room_id: String, -) -> Result, String> { - let s = state.read().await; - let client = s.client.as_ref().ok_or("Not logged in")?; - - let rid = room_id!(room_id.as_str()).map_err(|e: matrix_sdk::ruma::IdParseError| e.to_string())?; - let _room = client.get_room(&rid).ok_or("Room not found")?; - - let _emojis = Vec::new(); - +pub async fn get_sticker_packs(_state: State<'_, AppState>) -> Result, String> { Ok(Vec::new()) } - -#[tauri::command] -pub async fn upload_emoji( - state: State<'_, crate::state::AppState>, - room_id: String, - name: String, - image_path: String, -) -> Result { - let s = state.read().await; - let client = s.client.as_ref().ok_or("Not logged in")?; - - let path = Path::new(&image_path); - if !path.exists() { - return Err("Image file not found".to_string()); - } - - let mime_type = match path.extension().and_then(|e| e.to_str()) { - Some("png") => "image/png", - Some("jpg") | Some("jpeg") => "image/jpeg", - Some("gif") => "image/gif", - Some("webp") => "image/webp", - _ => "image/png", - }; - - let data = std::fs::read(path).map_err(|e| e.to_string())?; - let content_type = mime_type.parse::().map_err(|e| e.to_string())?; - - let response = client - .media() - .upload(&content_type, data) - .await - .map_err(|e| e.to_string())?; - - Ok(CustomEmoji { - id: format!("emoji_{}", chrono::Utc::now().timestamp()), - name, - url: response.content_uri.to_string(), - category: "custom".to_string(), - animated: mime_type == "image/gif", - }) -} - -#[tauri::command] -pub async fn get_sticker_packs( - state: State<'_, crate::state::AppState>, -) -> Result, String> { - let _s = state.read().await; - Ok(Vec::new()) -} \ No newline at end of file diff --git a/client/src/commands/mod.rs b/client/src/commands/mod.rs index 5b9f0e3..4177c84 100644 --- a/client/src/commands/mod.rs +++ b/client/src/commands/mod.rs @@ -1,7 +1,8 @@ pub mod auth; +pub mod emoji; +pub mod presence; +pub mod profile; +pub mod roles; pub mod rooms; pub mod threads; -pub mod presence; pub mod voice; -pub mod emoji; -pub mod roles; \ No newline at end of file diff --git a/client/src/commands/presence.rs b/client/src/commands/presence.rs index 228bcfd..c734a46 100644 --- a/client/src/commands/presence.rs +++ b/client/src/commands/presence.rs @@ -1,9 +1,8 @@ -use matrix_sdk::ruma::user_id; -use serde::Serialize; -use tauri::State; use crate::state::AppState; +use serde::{Deserialize, Serialize}; +use tauri::State; -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct PresenceInfo { user_id: String, status: String, @@ -13,63 +12,51 @@ pub struct PresenceInfo { #[tauri::command] pub async fn set_presence( - state: State<'_, crate::state::AppState>, + state: State<'_, AppState>, status: String, status_msg: Option, ) -> Result { let s = state.read().await; - let client = s.client.as_ref().ok_or("Not logged in")?; + let server_url = s.server_url.clone().ok_or("Not logged in")?; + let token = s.auth_token.clone().ok_or("Not logged in")?; + drop(s); - let presence_state = match status.as_str() { - "online" => matrix_sdk::ruma::presence::PresenceState::Online, - "away" => matrix_sdk::ruma::presence::PresenceState::Away, - "unavailable" => matrix_sdk::ruma::presence::PresenceState::Unavailable, - _ => matrix_sdk::ruma::presence::PresenceState::Online, - }; - - let request = matrix_sdk::ruma::api::client::presence::set_presence::v3::Request::new( - client.user_id().ok_or("No user ID")?.to_owned(), - ); - let mut request = request; - request.presence = presence_state; - request.status_msg = status_msg.clone(); - - client - .send(request, None) + let client = reqwest::Client::new(); + let res = client + .post(format!( + "{}/api/presence/set", + server_url.trim_end_matches('/') + )) + .header("Authorization", format!("Bearer {}", token)) + .json(&serde_json::json!({ "status": status, "status_msg": status_msg })) + .send() .await .map_err(|e| e.to_string())?; - Ok(true) + res.json().await.map_err(|e| e.to_string()) } #[tauri::command] pub async fn get_presence( - state: State<'_, crate::state::AppState>, - user_id_str: String, + state: State<'_, AppState>, + user_id: String, ) -> Result { let s = state.read().await; - let client = s.client.as_ref().ok_or("Not logged in")?; + let server_url = s.server_url.clone().ok_or("Not logged in")?; + let token = s.auth_token.clone().ok_or("Not logged in")?; + drop(s); - let uid = user_id!(user_id_str.as_str()).map_err(|e: matrix_sdk::ruma::IdParseError| e.to_string())?; - - let request = matrix_sdk::ruma::api::client::presence::get_presence::v3::Request::new(uid.to_owned()); - - let response = client - .send(request, None) + let client = reqwest::Client::new(); + let res = client + .get(format!( + "{}/api/presence/{}", + server_url.trim_end_matches('/'), + user_id + )) + .header("Authorization", format!("Bearer {}", token)) + .send() .await .map_err(|e| e.to_string())?; - let status_str = match response.presence { - matrix_sdk::ruma::presence::PresenceState::Online => "online", - matrix_sdk::ruma::presence::PresenceState::Away => "away", - matrix_sdk::ruma::presence::PresenceState::Unavailable => "unavailable", - _ => "offline", - }; - - Ok(PresenceInfo { - user_id: user_id_str, - status: status_str.to_string(), - status_msg: response.status_msg, - last_active: response.last_active_ago.map(|d| d.as_secs()), - }) -} \ No newline at end of file + res.json().await.map_err(|e| e.to_string()) +} diff --git a/client/src/commands/profile.rs b/client/src/commands/profile.rs new file mode 100644 index 0000000..9ceb321 --- /dev/null +++ b/client/src/commands/profile.rs @@ -0,0 +1,130 @@ +use crate::state::AppState; +use serde::{Deserialize, Serialize}; +use tauri::State; + +#[derive(Serialize, Deserialize)] +pub struct UserProfile { + user_id: String, + display_name: Option, + avatar_url: Option, +} + +#[tauri::command] +pub async fn get_own_profile(state: State<'_, AppState>) -> Result { + let s = state.read().await; + let server_url = s.server_url.clone().ok_or("Not logged in")?; + let token = s.auth_token.clone().ok_or("Not logged in")?; + drop(s); + + let client = reqwest::Client::new(); + let res = client + .get(format!( + "{}/api/profile/me", + server_url.trim_end_matches('/') + )) + .header("Authorization", format!("Bearer {}", token)) + .send() + .await + .map_err(|e| e.to_string())?; + + res.json().await.map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn get_user_profile( + state: State<'_, AppState>, + user_id: String, +) -> Result { + let s = state.read().await; + let server_url = s.server_url.clone().ok_or("Not logged in")?; + let token = s.auth_token.clone().ok_or("Not logged in")?; + drop(s); + + let client = reqwest::Client::new(); + let res = client + .get(format!( + "{}/api/profile/{}", + server_url.trim_end_matches('/'), + user_id + )) + .header("Authorization", format!("Bearer {}", token)) + .send() + .await + .map_err(|e| e.to_string())?; + + res.json().await.map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn set_display_name( + state: State<'_, AppState>, + display_name: String, +) -> Result { + let s = state.read().await; + let server_url = s.server_url.clone().ok_or("Not logged in")?; + let token = s.auth_token.clone().ok_or("Not logged in")?; + drop(s); + + let client = reqwest::Client::new(); + let res = client + .post(format!( + "{}/api/profile/displayname", + server_url.trim_end_matches('/') + )) + .header("Authorization", format!("Bearer {}", token)) + .json(&serde_json::json!({ "display_name": display_name })) + .send() + .await + .map_err(|e| e.to_string())?; + + res.json().await.map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn upload_avatar( + state: State<'_, AppState>, + file_path: String, +) -> Result { + let s = state.read().await; + let server_url = s.server_url.clone().ok_or("Not logged in")?; + let token = s.auth_token.clone().ok_or("Not logged in")?; + drop(s); + + let data = std::fs::read(&file_path).map_err(|e| e.to_string())?; + let file_name = std::path::Path::new(&file_path) + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("avatar") + .to_string(); + + let mime_type = match std::path::Path::new(&file_path) + .extension() + .and_then(|e| e.to_str()) + { + Some("png") => "image/png", + Some("jpg") | Some("jpeg") => "image/jpeg", + Some("gif") => "image/gif", + Some("webp") => "image/webp", + _ => "image/png", + }; + + let client = reqwest::Client::new(); + let part = reqwest::multipart::Part::bytes(data) + .file_name(file_name) + .mime_str(mime_type) + .map_err(|e| e.to_string())?; + let form = reqwest::multipart::Form::new().part("file", part); + + let res = client + .post(format!( + "{}/api/profile/avatar", + server_url.trim_end_matches('/') + )) + .header("Authorization", format!("Bearer {}", token)) + .multipart(form) + .send() + .await + .map_err(|e| e.to_string())?; + + res.json().await.map_err(|e| e.to_string()) +} diff --git a/client/src/commands/roles.rs b/client/src/commands/roles.rs index 9a3bdcd..a80361b 100644 --- a/client/src/commands/roles.rs +++ b/client/src/commands/roles.rs @@ -1,10 +1,8 @@ -use matrix_sdk::ruma::room_id; -use matrix_sdk::ruma::user_id; +use crate::state::AppState; use serde::{Deserialize, Serialize}; use tauri::State; -use crate::state::AppState; -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize)] pub struct Role { id: String, name: String, @@ -13,7 +11,7 @@ pub struct Role { position: i32, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct Permissions { can_send_messages: bool, can_delete_messages: bool, @@ -27,155 +25,107 @@ pub struct Permissions { can_voice_stream: bool, } -fn power_level_to_permissions(power_level: i64) -> Permissions { - Permissions { - can_send_messages: power_level >= 0, - can_delete_messages: power_level >= 50, - can_manage_channels: power_level >= 50, - can_manage_roles: power_level >= 75, - can_kick: power_level >= 50, - can_ban: power_level >= 75, - can_manage_emoji: power_level >= 50, - can_manage_threads: power_level >= 0, - can_voice_connect: power_level >= 0, - can_voice_stream: power_level >= 25, - } -} - -fn default_roles() -> Vec { - vec![ - Role { - id: "admin".to_string(), - name: "Admin".to_string(), - color: "#ed4245".to_string(), - permissions: vec!["*".to_string()], - position: 100, - }, - Role { - id: "moderator".to_string(), - name: "Moderator".to_string(), - color: "#fee75c".to_string(), - permissions: vec!["kick".to_string(), "ban".to_string(), "manage_channels".to_string()], - position: 50, - }, - Role { - id: "member".to_string(), - name: "Mitglied".to_string(), - color: "#5865f2".to_string(), - permissions: vec!["send_messages".to_string(), "voice_connect".to_string()], - position: 0, - }, - ] -} - #[tauri::command] -pub async fn get_roles( - state: State<'_, crate::state::AppState>, - room_id: String, -) -> Result, String> { +pub async fn get_roles(state: State<'_, AppState>, room_id: String) -> Result, String> { let s = state.read().await; - let client = s.client.as_ref().ok_or("Not logged in")?; - let rid = room_id!(room_id.as_str()).map_err(|e: matrix_sdk::ruma::IdParseError| e.to_string())?; - let room = client.get_room(&rid).ok_or("Room not found")?; + let server_url = s.server_url.clone().ok_or("Not logged in")?; + let token = s.auth_token.clone().ok_or("Not logged in")?; + drop(s); - let mut roles = Vec::new(); + let client = reqwest::Client::new(); + let res = client + .get(format!( + "{}/api/rooms/{}/roles", + server_url.trim_end_matches('/'), + room_id + )) + .header("Authorization", format!("Bearer {}", token)) + .send() + .await + .map_err(|e| e.to_string())?; - if let Ok(power_levels) = room.power_levels().await { - for (uid, power_level) in &power_levels.users { - roles.push(Role { - id: format!("role_{}", uid), - name: uid.to_string(), - color: if *power_level >= 100 { "#ed4245".to_string() } else if *power_level >= 50 { "#fee75c".to_string() } else { "#5865f2".to_string() }, - permissions: vec![], - position: *power_level as i32, - }); - } - } - - if roles.is_empty() { - roles = default_roles(); - } - - Ok(roles) + res.json().await.map_err(|e| e.to_string()) } #[tauri::command] pub async fn assign_role( - state: State<'_, crate::state::AppState>, + state: State<'_, AppState>, room_id: String, user_id: String, role_id: String, ) -> Result { let s = state.read().await; - let client = s.client.as_ref().ok_or("Not logged in")?; - let rid = room_id!(room_id.as_str()).map_err(|e: matrix_sdk::ruma::IdParseError| e.to_string())?; - let room = client.get_room(&rid).ok_or("Room not found")?; - let uid = user_id!(user_id.as_str()).map_err(|e: matrix_sdk::ruma::IdParseError| e.to_string())?; + let server_url = s.server_url.clone().ok_or("Not logged in")?; + let token = s.auth_token.clone().ok_or("Not logged in")?; + drop(s); - let power_level: i64 = match role_id.as_str() { - "admin" => 100, - "moderator" => 50, - _ => 0, - }; - - let content = matrix_sdk::ruma::events::room::power_levels::RoomPowerLevelsEventContent::new(); - let mut content = content; - content.users.insert(uid.to_owned(), power_level.into()); - - room.send_state_event(content) + let client = reqwest::Client::new(); + let res = client + .post(format!( + "{}/api/rooms/{}/roles/assign", + server_url.trim_end_matches('/'), + room_id + )) + .header("Authorization", format!("Bearer {}", token)) + .json(&serde_json::json!({ "user_id": user_id, "role_id": role_id })) + .send() .await .map_err(|e| e.to_string())?; - Ok(true) + res.json().await.map_err(|e| e.to_string()) } #[tauri::command] pub async fn remove_role( - state: State<'_, crate::state::AppState>, + state: State<'_, AppState>, room_id: String, user_id: String, - _role_id: String, + role_id: String, ) -> Result { let s = state.read().await; - let client = s.client.as_ref().ok_or("Not logged in")?; - let rid = room_id!(room_id.as_str()).map_err(|e: matrix_sdk::ruma::IdParseError| e.to_string())?; - let room = client.get_room(&rid).ok_or("Room not found")?; - let uid = user_id!(user_id.as_str()).map_err(|e: matrix_sdk::ruma::IdParseError| e.to_string())?; + let server_url = s.server_url.clone().ok_or("Not logged in")?; + let token = s.auth_token.clone().ok_or("Not logged in")?; + drop(s); - if let Ok(mut power_levels) = room.power_levels().await { - power_levels.users.remove(&uid); + let client = reqwest::Client::new(); + let res = client + .post(format!( + "{}/api/rooms/{}/roles/remove", + server_url.trim_end_matches('/'), + room_id + )) + .header("Authorization", format!("Bearer {}", token)) + .json(&serde_json::json!({ "user_id": user_id, "role_id": role_id })) + .send() + .await + .map_err(|e| e.to_string())?; - let content = matrix_sdk::ruma::events::room::power_levels::RoomPowerLevelsEventContent::from(power_levels); - room.send_state_event(content) - .await - .map_err(|e| e.to_string())?; - } - - Ok(true) + res.json().await.map_err(|e| e.to_string()) } #[tauri::command] pub async fn get_permissions( - state: State<'_, crate::state::AppState>, + state: State<'_, AppState>, room_id: String, user_id: String, ) -> Result { let s = state.read().await; - let client = s.client.as_ref().ok_or("Not logged in")?; - let rid = room_id!(room_id.as_str()).map_err(|e: matrix_sdk::ruma::IdParseError| e.to_string())?; - let room = client.get_room(&rid).ok_or("Room not found")?; - let uid = user_id!(user_id.as_str()).map_err(|e: matrix_sdk::ruma::IdParseError| e.to_string())?; + let server_url = s.server_url.clone().ok_or("Not logged in")?; + let token = s.auth_token.clone().ok_or("Not logged in")?; + drop(s); - let user_power = if let Ok(power_levels) = room.power_levels().await { - power_levels - .users - .get(&uid) - .copied() - .map(|p| p.into()) - .unwrap_or(power_levels.users_default as i64) - } else { - 0 - }; + let client = reqwest::Client::new(); + let res = client + .get(format!( + "{}/api/rooms/{}/permissions/{}", + server_url.trim_end_matches('/'), + room_id, + user_id + )) + .header("Authorization", format!("Bearer {}", token)) + .send() + .await + .map_err(|e| e.to_string())?; - Ok(power_level_to_permissions(user_power)) -} \ No newline at end of file + res.json().await.map_err(|e| e.to_string()) +} diff --git a/client/src/commands/rooms.rs b/client/src/commands/rooms.rs index ef72f90..149cd0f 100644 --- a/client/src/commands/rooms.rs +++ b/client/src/commands/rooms.rs @@ -1,9 +1,8 @@ -use matrix_sdk::ruma::room_id; -use serde::Serialize; -use tauri::State; use crate::state::AppState; +use serde::{Deserialize, Serialize}; +use tauri::State; -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct RoomInfo { room_id: String, name: String, @@ -13,165 +12,466 @@ pub struct RoomInfo { topic: Option, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct MessageInfo { event_id: String, sender: String, body: String, timestamp: u64, reply_to: Option, + edited: Option, + reactions: Option>>, + msgtype: Option, + media_url: Option, + filename: Option, + mimetype: Option, + width: Option, + height: Option, + size: Option, } #[tauri::command] -pub async fn get_joined_rooms( - state: State<'_, crate::state::AppState>, -) -> Result, String> { +pub async fn get_joined_rooms(state: State<'_, AppState>) -> Result, String> { let s = state.read().await; - let client = s.client.as_ref().ok_or("Not logged in")?; + let server_url = s.server_url.clone().ok_or("Not logged in")?; + let token = s.auth_token.clone().ok_or("Not logged in")?; + drop(s); - let rooms = client.joined_rooms(); - let mut result = Vec::new(); + let client = reqwest::Client::new(); + let res = client + .get(format!("{}/api/rooms", server_url.trim_end_matches('/'))) + .header("Authorization", format!("Bearer {}", token)) + .send() + .await + .map_err(|e| e.to_string())?; - for room in rooms { - let name = room.display_name().await.map(|n| n.to_string()).unwrap_or_default(); - let avatar_url = room.avatar_url().map(|u| u.to_string()); - let member_count = room.joined_members().len() as u64; - let topic = room.topic().map(|t| t.to_string()); - let is_encrypted = room.is_encrypted().await.unwrap_or(false); - - result.push(RoomInfo { - room_id: room.room_id().to_string(), - name, - avatar_url, - is_encrypted, - member_count, - topic, - }); - } - - Ok(result) + res.json().await.map_err(|e| e.to_string()) } #[tauri::command] pub async fn get_room_messages( - state: State<'_, crate::state::AppState>, + state: State<'_, AppState>, room_id: String, limit: u32, from: Option, ) -> Result, String> { let s = state.read().await; - let client = s.client.as_ref().ok_or("Not logged in")?; - let rid = room_id!(room_id.as_str()).map_err(|e: matrix_sdk::ruma::IdParseError| e.to_string())?; - let room = client.get_room(&rid).ok_or("Room not found")?; + let server_url = s.server_url.clone().ok_or("Not logged in")?; + let token = s.auth_token.clone().ok_or("Not logged in")?; + drop(s); - let mut options = matrix_sdk::ruma::api::client::message::get_message_events::v3::Request::new(); - options.limit = limit.into(); - options.from = from.map(|t| t.into()).or_else(|| None); + let client = reqwest::Client::new(); + let mut url = format!( + "{}/api/rooms/{}/messages?limit={}", + server_url.trim_end_matches('/'), + room_id, + limit + ); + if let Some(from) = from { + url = format!("{}&from={}", url, from); + } - let messages = room - .messages(options) + let res = client + .get(&url) + .header("Authorization", format!("Bearer {}", token)) + .send() .await .map_err(|e| e.to_string())?; - let mut result = Vec::new(); - for msg in messages.chunk { - if let matrix_sdk::ruma::events::AnySyncMessageLikeEvent::RoomMessage(ev) = msg { - let body = ev.content().body().to_string(); - result.push(MessageInfo { - event_id: ev.event_id().to_string(), - sender: ev.sender().to_string(), - body, - timestamp: ev.origin_server_ts().0, - reply_to: ev.content().in_reply_to().map(|r| r.event_id.to_string()), - }); - } - } - - Ok(result) + res.json().await.map_err(|e| e.to_string()) } #[tauri::command] pub async fn send_message( - state: State<'_, crate::state::AppState>, + state: State<'_, AppState>, room_id: String, message: String, ) -> Result { let s = state.read().await; - let client = s.client.as_ref().ok_or("Not logged in")?; - let rid = room_id!(room_id.as_str()).map_err(|e: matrix_sdk::ruma::IdParseError| e.to_string())?; - let room = client.get_room(&rid).ok_or("Room not found")?; + let server_url = s.server_url.clone().ok_or("Not logged in")?; + let token = s.auth_token.clone().ok_or("Not logged in")?; + drop(s); - let content = matrix_sdk::ruma::events::room::message::RoomMessageEventContent::text_plain(&message); - let txn_id = matrix_sdk::ruma::TransactionId::new(); - let response = room.send(content, Some(&txn_id)).await.map_err(|e| e.to_string())?; + let client = reqwest::Client::new(); + let res = client + .post(format!( + "{}/api/rooms/{}/send", + server_url.trim_end_matches('/'), + room_id + )) + .header("Authorization", format!("Bearer {}", token)) + .json(&serde_json::json!({ "message": message })) + .send() + .await + .map_err(|e| e.to_string())?; - Ok(response.event_id.to_string()) + res.json().await.map_err(|e| e.to_string()) } #[tauri::command] pub async fn create_room( - state: State<'_, crate::state::AppState>, + state: State<'_, AppState>, name: String, topic: Option, visibility: String, ) -> Result { let s = state.read().await; - let client = s.client.as_ref().ok_or("Not logged in")?; + let server_url = s.server_url.clone().ok_or("Not logged in")?; + let token = s.auth_token.clone().ok_or("Not logged in")?; + drop(s); - let vis = match visibility.as_str() { - "public" => matrix_sdk::ruma::Space::Public, - _ => matrix_sdk::ruma::Space::Private, - }; + let client = reqwest::Client::new(); + let res = client + .post(format!( + "{}/api/rooms/create", + server_url.trim_end_matches('/') + )) + .header("Authorization", format!("Bearer {}", token)) + .json(&serde_json::json!({ "name": name, "topic": topic, "visibility": visibility })) + .send() + .await + .map_err(|e| e.to_string())?; - let mut request = matrix_sdk::ruma::api::client::room::create_room::v3::Request::new(); - request.name = Some(name); - request.topic = topic; - request.visibility = vis; - - let response = client.create_room(request).await.map_err(|e| e.to_string())?; - Ok(response.room_id.to_string()) + res.json().await.map_err(|e| e.to_string()) } #[tauri::command] pub async fn join_room( - state: State<'_, crate::state::AppState>, + state: State<'_, AppState>, room_id_or_alias: String, ) -> Result { let s = state.read().await; - let client = s.client.as_ref().ok_or("Not logged in")?; + let server_url = s.server_url.clone().ok_or("Not logged in")?; + let token = s.auth_token.clone().ok_or("Not logged in")?; + drop(s); - let room_id = client.join_room_by_id_or_alias( - &room_id_or_alias.try_into().map_err(|e: matrix_sdk::ruma::IdParseError| e.to_string())?, - &[] - ).await.map_err(|e| e.to_string())?; + let client = reqwest::Client::new(); + let res = client + .post(format!( + "{}/api/rooms/join", + server_url.trim_end_matches('/') + )) + .header("Authorization", format!("Bearer {}", token)) + .json(&serde_json::json!({ "room_id_or_alias": room_id_or_alias })) + .send() + .await + .map_err(|e| e.to_string())?; - Ok(room_id.to_string()) + res.json().await.map_err(|e| e.to_string()) } #[tauri::command] -pub async fn leave_room( - state: State<'_, crate::state::AppState>, - room_id: String, -) -> Result { +pub async fn leave_room(state: State<'_, AppState>, room_id: String) -> Result { let s = state.read().await; - let client = s.client.as_ref().ok_or("Not logged in")?; - let rid = room_id!(room_id.as_str()).map_err(|e: matrix_sdk::ruma::IdParseError| e.to_string())?; - let room = client.get_room(&rid).ok_or("Room not found")?; + let server_url = s.server_url.clone().ok_or("Not logged in")?; + let token = s.auth_token.clone().ok_or("Not logged in")?; + drop(s); - room.leave().await.map_err(|e| e.to_string())?; - Ok(true) + let client = reqwest::Client::new(); + let res = client + .post(format!( + "{}/api/rooms/{}/leave", + server_url.trim_end_matches('/'), + room_id + )) + .header("Authorization", format!("Bearer {}", token)) + .send() + .await + .map_err(|e| e.to_string())?; + + res.json().await.map_err(|e| e.to_string()) } #[tauri::command] pub async fn get_room_members( - state: State<'_, crate::state::AppState>, + state: State<'_, AppState>, room_id: String, ) -> Result, String> { let s = state.read().await; - let client = s.client.as_ref().ok_or("Not logged in")?; - let rid = room_id!(room_id.as_str()).map_err(|e: matrix_sdk::ruma::IdParseError| e.to_string())?; - let room = client.get_room(&rid).ok_or("Room not found")?; + let server_url = s.server_url.clone().ok_or("Not logged in")?; + let token = s.auth_token.clone().ok_or("Not logged in")?; + drop(s); - let members = room.joined_members(); - Ok(members.iter().map(|m| m.user_id().to_string()).collect()) -} \ No newline at end of file + let client = reqwest::Client::new(); + let res = client + .get(format!( + "{}/api/rooms/{}/members", + server_url.trim_end_matches('/'), + room_id + )) + .header("Authorization", format!("Bearer {}", token)) + .send() + .await + .map_err(|e| e.to_string())?; + + res.json().await.map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn edit_message( + state: State<'_, AppState>, + room_id: String, + event_id: String, + new_body: String, +) -> Result { + let s = state.read().await; + let server_url = s.server_url.clone().ok_or("Not logged in")?; + let token = s.auth_token.clone().ok_or("Not logged in")?; + drop(s); + + let client = reqwest::Client::new(); + let res = client + .post(format!( + "{}/api/rooms/{}/edit", + server_url.trim_end_matches('/'), + room_id + )) + .header("Authorization", format!("Bearer {}", token)) + .json(&serde_json::json!({ "event_id": event_id, "new_body": new_body })) + .send() + .await + .map_err(|e| e.to_string())?; + + res.json().await.map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn delete_message( + state: State<'_, AppState>, + room_id: String, + event_id: String, +) -> Result { + let s = state.read().await; + let server_url = s.server_url.clone().ok_or("Not logged in")?; + let token = s.auth_token.clone().ok_or("Not logged in")?; + drop(s); + + let client = reqwest::Client::new(); + let res = client + .post(format!( + "{}/api/rooms/{}/delete/{}", + server_url.trim_end_matches('/'), + room_id, + event_id + )) + .header("Authorization", format!("Bearer {}", token)) + .send() + .await + .map_err(|e| e.to_string())?; + + res.json().await.map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn react_to_message( + state: State<'_, AppState>, + room_id: String, + event_id: String, + key: String, +) -> Result { + let s = state.read().await; + let server_url = s.server_url.clone().ok_or("Not logged in")?; + let token = s.auth_token.clone().ok_or("Not logged in")?; + drop(s); + + let client = reqwest::Client::new(); + let res = client + .post(format!( + "{}/api/rooms/{}/react", + server_url.trim_end_matches('/'), + room_id + )) + .header("Authorization", format!("Bearer {}", token)) + .json(&serde_json::json!({ "event_id": event_id, "key": key })) + .send() + .await + .map_err(|e| e.to_string())?; + + res.json().await.map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn set_typing( + state: State<'_, AppState>, + room_id: String, + typing: bool, +) -> Result { + let s = state.read().await; + let server_url = s.server_url.clone().ok_or("Not logged in")?; + let token = s.auth_token.clone().ok_or("Not logged in")?; + drop(s); + + let client = reqwest::Client::new(); + let res = client + .post(format!( + "{}/api/rooms/{}/typing", + server_url.trim_end_matches('/'), + room_id + )) + .header("Authorization", format!("Bearer {}", token)) + .json(&serde_json::json!({ "typing": typing })) + .send() + .await + .map_err(|e| e.to_string())?; + + res.json().await.map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn upload_file( + state: State<'_, AppState>, + room_id: String, + file_path: String, +) -> Result { + let s = state.read().await; + let server_url = s.server_url.clone().ok_or("Not logged in")?; + let token = s.auth_token.clone().ok_or("Not logged in")?; + drop(s); + + let data = std::fs::read(&file_path).map_err(|e| e.to_string())?; + let file_name = std::path::Path::new(&file_path) + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("upload") + .to_string(); + + let mime_type = match std::path::Path::new(&file_path) + .extension() + .and_then(|e| e.to_str()) + { + Some("png") => "image/png", + Some("jpg") | Some("jpeg") => "image/jpeg", + Some("gif") => "image/gif", + Some("webp") => "image/webp", + Some("pdf") => "application/pdf", + Some("mp4") => "video/mp4", + Some("mp3") => "audio/mpeg", + _ => "application/octet-stream", + }; + + let client = reqwest::Client::new(); + let part = reqwest::multipart::Part::bytes(data) + .file_name(file_name) + .mime_str(mime_type) + .map_err(|e| e.to_string())?; + let form = reqwest::multipart::Form::new().part("file", part); + + let res = client + .post(format!( + "{}/api/rooms/{}/upload", + server_url.trim_end_matches('/'), + room_id + )) + .header("Authorization", format!("Bearer {}", token)) + .multipart(form) + .send() + .await + .map_err(|e| e.to_string())?; + + res.text().await.map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn set_room_name( + state: State<'_, AppState>, + room_id: String, + name: String, +) -> Result { + let s = state.read().await; + let server_url = s.server_url.clone().ok_or("Not logged in")?; + let token = s.auth_token.clone().ok_or("Not logged in")?; + drop(s); + + let client = reqwest::Client::new(); + let res = client + .post(format!( + "{}/api/rooms/{}/name", + server_url.trim_end_matches('/'), + room_id + )) + .header("Authorization", format!("Bearer {}", token)) + .json(&serde_json::json!({ "name": name })) + .send() + .await + .map_err(|e| e.to_string())?; + + res.json().await.map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn set_room_topic( + state: State<'_, AppState>, + room_id: String, + topic: String, +) -> Result { + let s = state.read().await; + let server_url = s.server_url.clone().ok_or("Not logged in")?; + let token = s.auth_token.clone().ok_or("Not logged in")?; + drop(s); + + let client = reqwest::Client::new(); + let res = client + .post(format!( + "{}/api/rooms/{}/topic", + server_url.trim_end_matches('/'), + room_id + )) + .header("Authorization", format!("Bearer {}", token)) + .json(&serde_json::json!({ "topic": topic })) + .send() + .await + .map_err(|e| e.to_string())?; + + res.json().await.map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn set_room_avatar( + state: State<'_, AppState>, + room_id: String, + file_path: String, +) -> Result { + let s = state.read().await; + let server_url = s.server_url.clone().ok_or("Not logged in")?; + let token = s.auth_token.clone().ok_or("Not logged in")?; + drop(s); + + let data = std::fs::read(&file_path).map_err(|e| e.to_string())?; + let file_name = std::path::Path::new(&file_path) + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("avatar") + .to_string(); + + let mime_type = match std::path::Path::new(&file_path) + .extension() + .and_then(|e| e.to_str()) + { + Some("png") => "image/png", + Some("jpg") | Some("jpeg") => "image/jpeg", + Some("gif") => "image/gif", + Some("webp") => "image/webp", + _ => "image/png", + }; + + let client = reqwest::Client::new(); + let part = reqwest::multipart::Part::bytes(data) + .file_name(file_name) + .mime_str(mime_type) + .map_err(|e| e.to_string())?; + let form = reqwest::multipart::Form::new().part("file", part); + + let res = client + .post(format!( + "{}/api/rooms/{}/avatar", + server_url.trim_end_matches('/'), + room_id + )) + .header("Authorization", format!("Bearer {}", token)) + .multipart(form) + .send() + .await + .map_err(|e| e.to_string())?; + + res.json().await.map_err(|e| e.to_string()) +} diff --git a/client/src/commands/threads.rs b/client/src/commands/threads.rs index 2797302..feb7be9 100644 --- a/client/src/commands/threads.rs +++ b/client/src/commands/threads.rs @@ -1,87 +1,135 @@ -use matrix_sdk::ruma::room_id; -use serde::Serialize; -use tauri::State; use crate::state::AppState; +use serde::{Deserialize, Serialize}; +use tauri::State; -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct ThreadInfo { - thread_id: String, - root_event_id: String, - sender: String, - body: String, - reply_count: u32, + pub root_event_id: String, + pub root_sender: String, + pub root_body: String, + pub root_timestamp: u64, + pub reply_count: u32, + pub last_reply_event_id: Option, + pub last_reply_sender: Option, + pub last_reply_body: Option, + pub last_reply_timestamp: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct ThreadMessageInfo { + pub event_id: String, + pub sender: String, + pub body: String, + pub timestamp: u64, +} + +#[tauri::command] +pub async fn get_threads( + state: State<'_, AppState>, + room_id: String, +) -> Result, String> { + let s = state.read().await; + let server_url = s.server_url.clone().ok_or("No server URL")?; + let token = s.auth_token.clone().ok_or("No auth token")?; + drop(s); + + let client = reqwest::Client::new(); + let res = client + .get(format!( + "{}/api/rooms/{}/threads", + server_url.trim_end_matches('/'), + room_id + )) + .header("Authorization", format!("Bearer {}", token)) + .send() + .await + .map_err(|e| e.to_string())?; + + res.json().await.map_err(|e| e.to_string()) } #[tauri::command] pub async fn get_thread_messages( - state: State<'_, crate::state::AppState>, + state: State<'_, AppState>, room_id: String, thread_id: String, limit: u32, -) -> Result, String> { +) -> Result, String> { let s = state.read().await; - let client = s.client.as_ref().ok_or("Not logged in")?; - let rid = room_id!(room_id.as_str()).map_err(|e: matrix_sdk::ruma::IdParseError| e.to_string())?; - let room = client.get_room(&rid).ok_or("Room not found")?; + let server_url = s.server_url.clone().ok_or("No server URL")?; + let token = s.auth_token.clone().ok_or("No auth token")?; + drop(s); - let mut options = matrix_sdk::ruma::api::client::message::get_message_events::v3::Request::new(); - options.limit = limit.into(); + let client = reqwest::Client::new(); + let res = client + .get(format!( + "{}/api/rooms/{}/threads/{}?limit={}", + server_url.trim_end_matches('/'), + room_id, + thread_id, + limit + )) + .header("Authorization", format!("Bearer {}", token)) + .send() + .await + .map_err(|e| e.to_string())?; - let messages = room.messages(options).await.map_err(|e| e.to_string())?; - - let mut result = Vec::new(); - for msg in messages.chunk { - if let matrix_sdk::ruma::events::AnySyncMessageLikeEvent::RoomMessage(ev) = msg { - let relates_to = ev.content().relates_to(); - let is_thread = relates_to.as_ref().map_or(false, |r| { - matches!(r, matrix_sdk::ruma::events::room::message::Relation::Thread(_)) - }); - if is_thread { - if let Some(matrix_sdk::ruma::events::room::message::Relation::Thread(thread)) = relates_to { - result.push(ThreadInfo { - thread_id: thread.event_id.to_string(), - root_event_id: thread.event_id.to_string(), - sender: ev.sender().to_string(), - body: ev.content().body().to_string(), - reply_count: 0, - }); - } - } - } - } - - Ok(result) + res.json().await.map_err(|e| e.to_string()) } #[tauri::command] pub async fn send_thread_reply( - state: State<'_, crate::state::AppState>, + state: State<'_, AppState>, room_id: String, thread_id: String, message: String, ) -> Result { let s = state.read().await; - let client = s.client.as_ref().ok_or("Not logged in")?; - let rid = room_id!(room_id.as_str()).map_err(|e: matrix_sdk::ruma::IdParseError| e.to_string())?; - let room = client.get_room(&rid).ok_or("Room not found")?; + let server_url = s.server_url.clone().ok_or("No server URL")?; + let token = s.auth_token.clone().ok_or("No auth token")?; + drop(s); - let content = matrix_sdk::ruma::events::room::message::RoomMessageEventContent::text_plain(&message); - let txn_id = matrix_sdk::ruma::TransactionId::new(); - let response = room.send(content, Some(&txn_id)).await.map_err(|e| e.to_string())?; + let client = reqwest::Client::new(); + let res = client + .post(format!( + "{}/api/rooms/{}/threads/{}/reply", + server_url.trim_end_matches('/'), + room_id, + thread_id + )) + .header("Authorization", format!("Bearer {}", token)) + .json(&serde_json::json!({ "message": message })) + .send() + .await + .map_err(|e| e.to_string())?; - Ok(response.event_id.to_string()) + res.json().await.map_err(|e| e.to_string()) } #[tauri::command] -pub async fn create_thread( - state: State<'_, crate::state::AppState>, +pub async fn send_reply( + state: State<'_, AppState>, room_id: String, - root_event_id: String, + reply_to: String, + message: String, ) -> Result { let s = state.read().await; - let client = s.client.as_ref().ok_or("Not logged in")?; - let rid = room_id!(room_id.as_str()).map_err(|e: matrix_sdk::ruma::IdParseError| e.to_string())?; - let _room = client.get_room(&rid).ok_or("Room not found")?; + let server_url = s.server_url.clone().ok_or("No server URL")?; + let token = s.auth_token.clone().ok_or("No auth token")?; + drop(s); - Ok(root_event_id) -} \ No newline at end of file + let client = reqwest::Client::new(); + let res = client + .post(format!( + "{}/api/rooms/{}/reply", + server_url.trim_end_matches('/'), + room_id + )) + .header("Authorization", format!("Bearer {}", token)) + .json(&serde_json::json!({ "message": message, "reply_to": reply_to })) + .send() + .await + .map_err(|e| e.to_string())?; + + res.json().await.map_err(|e| e.to_string()) +} diff --git a/client/src/commands/voice.rs b/client/src/commands/voice.rs index 1da2612..598882b 100644 --- a/client/src/commands/voice.rs +++ b/client/src/commands/voice.rs @@ -1,68 +1,152 @@ -use serde::Serialize; -use tauri::State; use crate::state::AppState; +use serde::{Deserialize, Serialize}; +use tauri::State; -#[derive(Serialize)] -pub struct VoiceState { - room_id: String, - muted: bool, - deafened: bool, - streaming: bool, +#[derive(Serialize, Deserialize)] +pub struct VoiceJoinResult { + pub room_id: String, + pub livekit_url: String, + pub livekit_token: String, +} + +#[derive(Serialize, Deserialize)] +pub struct VoiceToggleResult { + pub muted: bool, + pub deafened: bool, +} + +#[derive(Serialize, Deserialize)] +pub struct VoiceParticipantInfo { + pub user_id: String, + pub muted: bool, + pub deafened: bool, } #[tauri::command] pub async fn join_voice_channel( - state: State<'_, crate::state::AppState>, + state: State<'_, AppState>, room_id: String, -) -> Result { +) -> Result { let s = state.read().await; - let user_id = s.user_id.clone().ok_or("Not logged in")?; + let server_url = s.server_url.clone().ok_or("No server URL")?; + let token = s.auth_token.clone().ok_or("No auth token")?; - let mut s = state.write().await; - s.voice_manager.join_channel(room_id.clone(), user_id); + drop(s); - Ok(VoiceState { - room_id, - muted: false, - deafened: false, - streaming: false, - }) + let client = reqwest::Client::new(); + let res = client + .post(format!( + "{}/api/voice/join", + server_url.trim_end_matches('/') + )) + .header("Authorization", format!("Bearer {}", token)) + .json(&serde_json::json!({ "room_id": room_id })) + .send() + .await + .map_err(|e| e.to_string())?; + + let result: VoiceJoinResult = res.json().await.map_err(|e| e.to_string())?; + Ok(result) } #[tauri::command] pub async fn leave_voice_channel( - state: State<'_, crate::state::AppState>, + state: State<'_, AppState>, room_id: String, ) -> Result { let s = state.read().await; - let user_id = s.user_id.clone().ok_or("Not logged in")?; + let server_url = s.server_url.clone().ok_or("No server URL")?; + let token = s.auth_token.clone().ok_or("No auth token")?; - let mut s = state.write().await; - Ok(s.voice_manager.leave_channel(&room_id, &user_id)) + drop(s); + + let client = reqwest::Client::new(); + let res = client + .post(format!( + "{}/api/voice/leave", + server_url.trim_end_matches('/') + )) + .header("Authorization", format!("Bearer {}", token)) + .json(&serde_json::json!({ "room_id": room_id })) + .send() + .await + .map_err(|e| e.to_string())?; + + res.json().await.map_err(|e| e.to_string()) } #[tauri::command] pub async fn toggle_mute( - state: State<'_, crate::state::AppState>, + state: State<'_, AppState>, room_id: String, -) -> Result { +) -> Result { let s = state.read().await; - let user_id = s.user_id.clone().ok_or("Not logged in")?; + let server_url = s.server_url.clone().ok_or("No server URL")?; + let token = s.auth_token.clone().ok_or("No auth token")?; - let mut s = state.write().await; - s.voice_manager.toggle_mute(&room_id, &user_id) - .ok_or("Not in voice channel".to_string()) + drop(s); + + let client = reqwest::Client::new(); + let res = client + .post(format!( + "{}/api/voice/toggle-mute", + server_url.trim_end_matches('/') + )) + .header("Authorization", format!("Bearer {}", token)) + .json(&serde_json::json!({ "room_id": room_id })) + .send() + .await + .map_err(|e| e.to_string())?; + + res.json().await.map_err(|e| e.to_string()) } #[tauri::command] pub async fn toggle_deafen( - state: State<'_, crate::state::AppState>, + state: State<'_, AppState>, room_id: String, -) -> Result { +) -> Result { let s = state.read().await; - let user_id = s.user_id.clone().ok_or("Not logged in")?; + let server_url = s.server_url.clone().ok_or("No server URL")?; + let token = s.auth_token.clone().ok_or("No auth token")?; - let mut s = state.write().await; - s.voice_manager.toggle_deafen(&room_id, &user_id) - .ok_or("Not in voice channel".to_string()) -} \ No newline at end of file + drop(s); + + let client = reqwest::Client::new(); + let res = client + .post(format!( + "{}/api/voice/toggle-deafen", + server_url.trim_end_matches('/') + )) + .header("Authorization", format!("Bearer {}", token)) + .json(&serde_json::json!({ "room_id": room_id })) + .send() + .await + .map_err(|e| e.to_string())?; + + res.json().await.map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn get_voice_participants( + state: State<'_, AppState>, +) -> Result, String> { + let s = state.read().await; + let server_url = s.server_url.clone().ok_or("No server URL")?; + let token = s.auth_token.clone().ok_or("No auth token")?; + + drop(s); + + let client = reqwest::Client::new(); + let res = client + .get(format!( + "{}/api/voice/participants", + server_url.trim_end_matches('/') + )) + .header("Authorization", format!("Bearer {}", token)) + .send() + .await + .map_err(|e| e.to_string())?; + + res.json().await.map_err(|e| e.to_string()) +} diff --git a/client/src/main.rs b/client/src/main.rs index 1715d2b..56de857 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,12 +1,13 @@ -pub mod matrix; pub mod commands; pub mod state; -use state::AppState; +use state::AppStateInner; fn main() { tauri::Builder::default() - .manage(AppState::new()) + .manage(AppStateInner::default()) + .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_dialog::init()) .invoke_handler(tauri::generate_handler![ commands::auth::login, commands::auth::logout, @@ -15,19 +16,29 @@ fn main() { commands::rooms::get_joined_rooms, commands::rooms::get_room_messages, commands::rooms::send_message, + commands::rooms::edit_message, + commands::rooms::delete_message, + commands::rooms::react_to_message, + commands::rooms::set_typing, + commands::rooms::upload_file, commands::rooms::create_room, commands::rooms::join_room, commands::rooms::leave_room, commands::rooms::get_room_members, + commands::rooms::set_room_name, + commands::rooms::set_room_topic, + commands::rooms::set_room_avatar, commands::threads::get_thread_messages, commands::threads::send_thread_reply, - commands::threads::create_thread, + commands::threads::get_threads, + commands::threads::send_reply, commands::presence::set_presence, commands::presence::get_presence, commands::voice::join_voice_channel, commands::voice::leave_voice_channel, commands::voice::toggle_mute, commands::voice::toggle_deafen, + commands::voice::get_voice_participants, commands::emoji::get_custom_emoji, commands::emoji::upload_emoji, commands::emoji::get_sticker_packs, @@ -35,7 +46,11 @@ fn main() { commands::roles::assign_role, commands::roles::remove_role, commands::roles::get_permissions, + commands::profile::get_own_profile, + commands::profile::get_user_profile, + commands::profile::set_display_name, + commands::profile::upload_avatar, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); -} \ No newline at end of file +} diff --git a/client/src/matrix/client.rs b/client/src/matrix/client.rs deleted file mode 100644 index 3e2e7e4..0000000 --- a/client/src/matrix/client.rs +++ /dev/null @@ -1,75 +0,0 @@ -use anyhow::Result; -use matrix_sdk::Client; -use std::sync::Arc; -use tokio::sync::RwLock; - -use super::room::RoomManager; -use super::event::EventHandler; -use super::sync::SyncService; -use super::presence::PresenceManager; -use super::voice::VoiceManager; - -pub struct MatrixClient { - pub client: Arc>>, - pub room_manager: Arc>, - pub event_handler: EventHandler, - pub sync_service: Arc>, - pub presence_manager: Arc>, - pub voice_manager: Arc>, -} - -impl MatrixClient { - pub fn new() -> Self { - Self { - client: Arc::new(RwLock::new(None)), - room_manager: Arc::new(RwLock::new(RoomManager::new())), - event_handler: EventHandler::new(), - sync_service: Arc::new(RwLock::new(SyncService::new())), - presence_manager: Arc::new(RwLock::new(PresenceManager::new())), - voice_manager: Arc::new(RwLock::new(VoiceManager::new())), - } - } - - pub async fn connect(&self, homeserver: &str) -> Result<()> { - let client = Client::builder() - .homeserver_url(homeserver) - .build() - .await?; - - let mut guard = self.client.write().await; - *guard = Some(client); - Ok(()) - } - - pub async fn login(&self, username: &str, password: &str) -> Result { - let guard = self.client.read().await; - let client = guard.as_ref().ok_or(anyhow::anyhow!("Not connected"))?; - - client - .matrix_auth() - .login_username(username, password) - .send() - .await?; - - let user_id = client - .user_id() - .ok_or(anyhow::anyhow!("No user ID"))? - .to_string(); - - Ok(user_id) - } - - pub async fn start_sync(&self) -> Result<()> { - let guard = self.client.read().await; - let client = guard.as_ref().ok_or(anyhow::anyhow!("Not connected"))?; - - let mut sync = self.sync_service.write().await; - sync.start(client.clone()).await?; - Ok(()) - } - - pub async fn stop_sync(&self) -> Result<()> { - let mut sync = self.sync_service.write().await; - sync.stop().await - } -} \ No newline at end of file diff --git a/client/src/matrix/event.rs b/client/src/matrix/event.rs deleted file mode 100644 index ac669c7..0000000 --- a/client/src/matrix/event.rs +++ /dev/null @@ -1,68 +0,0 @@ -use tokio::sync::mpsc; - -#[derive(Clone, Debug)] -pub enum ClientEvent { - NewMessage { - room_id: String, - sender: String, - body: String, - event_id: String, - }, - RoomJoined { - room_id: String, - name: String, - }, - RoomLeft { - room_id: String, - }, - Typing { - room_id: String, - user_id: String, - }, - PresenceChange { - user_id: String, - status: String, - }, - MemberJoined { - room_id: String, - user_id: String, - }, - MemberLeft { - room_id: String, - user_id: String, - }, - Receipt { - room_id: String, - event_id: String, - user_id: String, - }, - VoiceStateChange { - room_id: String, - user_id: String, - joined: bool, - }, -} - -pub struct EventHandler { - sender: mpsc::UnboundedSender, - receiver: tokio::sync::Mutex>, -} - -impl EventHandler { - pub fn new() -> Self { - let (sender, receiver) = mpsc::unbounded_channel(); - Self { - sender, - receiver: tokio::sync::Mutex::new(receiver), - } - } - - pub fn emit(&self, event: ClientEvent) { - let _ = self.sender.send(event); - } - - pub async fn next(&self) -> Option { - let mut guard = self.receiver.lock().await; - guard.recv().await - } -} \ No newline at end of file diff --git a/client/src/matrix/mod.rs b/client/src/matrix/mod.rs deleted file mode 100644 index 9b30de0..0000000 --- a/client/src/matrix/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod client; -pub mod room; -pub mod event; -pub mod sync; -pub mod voice; -pub mod presence; - -pub use client::MatrixClient; \ No newline at end of file diff --git a/client/src/matrix/presence.rs b/client/src/matrix/presence.rs deleted file mode 100644 index 9feaa89..0000000 --- a/client/src/matrix/presence.rs +++ /dev/null @@ -1,39 +0,0 @@ -use matrix_sdk::ruma::presence::PresenceState; - -pub struct PresenceManager { - current_presence: PresenceState, - status_msg: Option, -} - -impl PresenceManager { - pub fn new() -> Self { - Self { - current_presence: PresenceState::Online, - status_msg: None, - } - } - - pub fn set_online(&mut self) { - self.current_presence = PresenceState::Online; - self.status_msg = None; - } - - pub fn set_away(&mut self, msg: Option) { - self.current_presence = PresenceState::Away; - self.status_msg = msg; - } - - pub fn set_unavailable(&mut self) { - self.current_presence = PresenceState::Unavailable; - self.status_msg = None; - } - - pub fn get_status(&self) -> (PresenceState, &Option) { - (self.current_presence, &self.status_msg) - } - - pub fn set_presence(&mut self, presence: PresenceState, status_msg: Option) { - self.current_presence = presence; - self.status_msg = status_msg; - } -} \ No newline at end of file diff --git a/client/src/matrix/room.rs b/client/src/matrix/room.rs deleted file mode 100644 index 86b31b5..0000000 --- a/client/src/matrix/room.rs +++ /dev/null @@ -1,67 +0,0 @@ -use anyhow::Result; -use matrix_sdk::Client; -use std::collections::HashMap; - -pub struct RoomManager { - rooms: HashMap, -} - -#[derive(Clone, Debug)] -pub struct RoomState { - pub room_id: String, - pub name: String, - pub unread_count: u32, - pub is_voice: bool, - pub topic: Option, - pub avatar_url: Option, - pub member_count: u64, -} - -impl RoomManager { - pub fn new() -> Self { - Self { - rooms: HashMap::new(), - } - } - - pub async fn refresh_rooms(&mut self, client: &Client) -> Result<()> { - self.rooms.clear(); - let joined = client.joined_rooms(); - for room in joined { - let name = room.display_name().await.map(|n| n.to_string()).unwrap_or_default(); - let avatar_url = room.avatar_url().map(|u| u.to_string()); - let member_count = room.joined_members().len() as u64; - let topic = room.topic().map(|t| t.to_string()); - - self.rooms.insert( - room.room_id().to_string(), - RoomState { - room_id: room.room_id().to_string(), - name, - unread_count: 0, - is_voice: false, - topic, - avatar_url, - member_count, - }, - ); - } - Ok(()) - } - - pub fn get_rooms(&self) -> Vec<&RoomState> { - let mut rooms: Vec<&RoomState> = self.rooms.values().collect(); - rooms.sort_by(|a, b| a.name.cmp(&b.name)); - rooms - } - - pub fn get_room(&self, room_id: &str) -> Option<&RoomState> { - self.rooms.get(room_id) - } - - pub fn update_unread(&mut self, room_id: &str, count: u32) { - if let Some(room) = self.rooms.get_mut(room_id) { - room.unread_count = count; - } - } -} \ No newline at end of file diff --git a/client/src/matrix/sync.rs b/client/src/matrix/sync.rs deleted file mode 100644 index a414dfb..0000000 --- a/client/src/matrix/sync.rs +++ /dev/null @@ -1,44 +0,0 @@ -use anyhow::Result; -use matrix_sdk::Client; -use matrix_sdk::config::SyncSettings; -use tokio::task::JoinHandle; - -pub struct SyncService { - handle: Option>, -} - -impl SyncService { - pub fn new() -> Self { - Self { handle: None } - } - - pub async fn start(&mut self, client: Client) -> Result<()> { - let handle = tokio::spawn(async move { - let mut sync_token: Option = None; - loop { - let mut settings = SyncSettings::new(); - if let Some(token) = sync_token.as_ref() { - settings = settings.token(token.clone()); - } - match client.sync_once(settings).await { - Ok(response) => { - sync_token = Some(response.next_batch); - } - Err(e) => { - tracing::error!("Sync error: {}", e); - tokio::time::sleep(std::time::Duration::from_secs(5)).await; - } - } - } - }); - self.handle = Some(handle); - Ok(()) - } - - pub async fn stop(&mut self) -> Result<()> { - if let Some(handle) = self.handle.take() { - handle.abort(); - } - Ok(()) - } -} \ No newline at end of file diff --git a/client/src/matrix/voice.rs b/client/src/matrix/voice.rs deleted file mode 100644 index 3f3879c..0000000 --- a/client/src/matrix/voice.rs +++ /dev/null @@ -1,100 +0,0 @@ -use std::collections::HashMap; - -pub struct VoiceManager { - channels: HashMap, - active_channel: Option, -} - -pub struct VoiceChannel { - pub room_id: String, - pub participants: Vec, -} - -pub struct VoiceParticipant { - pub user_id: String, - pub muted: bool, - pub deafened: bool, - pub streaming: bool, -} - -impl VoiceManager { - pub fn new() -> Self { - Self { - channels: HashMap::new(), - active_channel: None, - } - } - - pub fn join_channel(&mut self, room_id: String, user_id: String) -> bool { - if let Some(ref old_channel) = self.active_channel { - self.leave_channel_internal(old_channel, &user_id); - } - - let channel = self.channels.entry(room_id.clone()).or_insert_with(|| VoiceChannel { - room_id: room_id.clone(), - participants: Vec::new(), - }); - if channel.participants.iter().any(|p| p.user_id == user_id) { - return false; - } - channel.participants.push(VoiceParticipant { - user_id, - muted: false, - deafened: false, - streaming: false, - }); - self.active_channel = Some(room_id); - true - } - - fn leave_channel_internal(&mut self, room_id: &str, user_id: &str) { - if let Some(channel) = self.channels.get_mut(room_id) { - channel.participants.retain(|p| p.user_id != user_id); - if channel.participants.is_empty() { - self.channels.remove(room_id); - } - } - } - - pub fn leave_channel(&mut self, room_id: &str, user_id: &str) -> bool { - self.leave_channel_internal(room_id, user_id); - if self.active_channel.as_deref() == Some(room_id) { - self.active_channel = None; - } - true - } - - pub fn toggle_mute(&mut self, room_id: &str, user_id: &str) -> Option { - if let Some(channel) = self.channels.get_mut(room_id) { - if let Some(participant) = channel.participants.iter_mut().find(|p| p.user_id == user_id) { - participant.muted = !participant.muted; - return Some(participant.muted); - } - } - None - } - - pub fn toggle_deafen(&mut self, room_id: &str, user_id: &str) -> Option { - if let Some(channel) = self.channels.get_mut(room_id) { - if let Some(participant) = channel.participants.iter_mut().find(|p| p.user_id == user_id) { - participant.deafened = !participant.deafened; - if participant.deafened { - participant.muted = true; - } - return Some(participant.deafened); - } - } - None - } - - pub fn get_active_channel(&self) -> Option<&str> { - self.active_channel.as_deref() - } - - pub fn get_participants(&self, room_id: &str) -> Vec<&VoiceParticipant> { - self.channels - .get(room_id) - .map(|c| c.participants.iter().collect()) - .unwrap_or_default() - } -} \ No newline at end of file diff --git a/client/src/state/mod.rs b/client/src/state/mod.rs index 3e338f5..5454392 100644 --- a/client/src/state/mod.rs +++ b/client/src/state/mod.rs @@ -1,21 +1,13 @@ -use std::sync::Arc; -use tokio::sync::RwLock; -use matrix_sdk::Client; - -use crate::matrix::voice::VoiceManager; - #[derive(Default)] pub struct AppStateInner { - pub client: Option, pub logged_in: bool, pub user_id: Option, - pub voice_manager: VoiceManager, + pub auth_token: Option, + pub server_url: Option, } -pub type AppState = Arc>; +pub type AppState = tauri::async_runtime::RwLock; -impl AppState { - pub fn new() -> Self { - Arc::new(RwLock::new(AppStateInner::default())) - } -} \ No newline at end of file +pub fn get_client() -> reqwest::Client { + reqwest::Client::new() +} diff --git a/client/tauri.conf.json b/client/tauri.conf.json new file mode 100644 index 0000000..966a67a --- /dev/null +++ b/client/tauri.conf.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-cli/schema.json", + "productName": "EifelDC", + "version": "0.1.0", + "identifier": "de.eifeldc", + "build": { + "beforeDevCommand": "npm run dev", + "beforeBuildCommand": "npm run build", + "devUrl": "http://localhost:5173", + "frontendDist": "../src-ui/dist" + }, + "app": { + "windows": [ + { + "title": "EifelDC", + "width": 1280, + "height": 720, + "minWidth": 800, + "minHeight": 600, + "resizable": true, + "fullscreen": false, + "decorations": true, + "transparent": false + } + ], + "security": { + "csp": "default-src 'self'; connect-src https: wss: http://localhost:*; img-src https: data:; style-src 'self' 'unsafe-inline'; script-src 'self'" + } + }, + "bundle": { + "active": true, + "icon": [ + "src-tauri/icons/32x32.png", + "src-tauri/icons/128x128.png", + "src-tauri/icons/128x128@2x.png", + "src-tauri/icons/icon.icns", + "src-tauri/icons/icon.ico" + ], + "category": "SocialNetworking", + "shortDescription": "EifelDC - Matrix Chat Client", + "longDescription": "EifelDC is a Discord-like Matrix chat client built with Tauri, Svelte and Rust.", + "macOS": { + "minimumSystemVersion": "10.15", + "entitlements": "macos/EifelDC.entitlements" + }, + "windows": { + "digestAlgorithm": "sha256" + } + }, + "plugins": { + "shell": { + "open": true + } + } +} \ No newline at end of file diff --git a/infra/.env.example b/infra/.env.example index 2e39694..4f5f7c9 100644 --- a/infra/.env.example +++ b/infra/.env.example @@ -2,4 +2,7 @@ DOMAIN=eifeldc.local POSTGRES_PASSWORD=changeme_postgres_password TURN_SECRET=changeme_turn_secret MACAROON_SECRET=changeme_macaroon_secret -FORM_SECRET=changeme_form_secret \ No newline at end of file +FORM_SECRET=changeme_form_secret +LIVEKIT_API_KEY=devkey +LIVEKIT_API_SECRET=devsecret +LIVEKIT_NODE_IP=127.0.0.1 \ No newline at end of file diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index 01dd791..7018d54 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.8" - services: eifeldc: build: @@ -12,12 +10,21 @@ services: environment: - EIFELDC_STATIC_DIR=/usr/share/eifeldc/client - RUST_LOG=eifeldc_server=info,tower_http=info + - LIVEKIT_API_KEY=${LIVEKIT_API_KEY:-devkey} + - LIVEKIT_API_SECRET=${LIVEKIT_API_SECRET:-devsecret} + - LIVEKIT_URL=ws://livekit:7880 + - SYNAPSE_URL=http://synapse:8008 healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/api/current-user"] interval: 15s timeout: 5s retries: 3 start_period: 10s + deploy: + resources: + limits: + memory: 512M + cpus: "1.0" logging: driver: json-file options: @@ -48,6 +55,11 @@ services: timeout: 5s retries: 5 start_period: 30s + deploy: + resources: + limits: + memory: 1G + cpus: "2.0" logging: driver: json-file options: @@ -72,6 +84,11 @@ services: interval: 5s timeout: 5s retries: 5 + deploy: + resources: + limits: + memory: 512M + cpus: "1.0" logging: driver: json-file options: @@ -95,6 +112,32 @@ services: max-size: "10m" max-file: "3" + livekit: + image: livekit/livekit-server:latest + container_name: eifeldc-livekit + restart: unless-stopped + ports: + - "7880:7880" + - "7881:7881" + - "7882:7882/udp" + - "50000-50200:50000-50200/udp" + environment: + - LIVEKIT_KEYS=${LIVEKIT_API_KEY:-devkey}: ${LIVEKIT_API_SECRET:-devsecret} + command: --dev --node-ip ${LIVEKIT_NODE_IP:-127.0.0.1} + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:7880"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 5s + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + networks: + - eifeldc + nginx: image: nginx:alpine container_name: eifeldc-nginx @@ -110,6 +153,8 @@ services: condition: service_healthy synapse: condition: service_healthy + livekit: + condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost:80/"] interval: 15s diff --git a/infra/nginx/eifeldc.conf b/infra/nginx/eifeldc.conf index e52ad0e..5545e05 100644 --- a/infra/nginx/eifeldc.conf +++ b/infra/nginx/eifeldc.conf @@ -6,6 +6,10 @@ upstream synapse_server { server synapse:8008; } +upstream livekit_server { + server livekit:7880; +} + server { listen 80; server_name eifeldc.local; @@ -60,6 +64,18 @@ server { proxy_set_header X-Forwarded-Proto $scheme; } + location /livekit/ { + proxy_pass http://livekit_server/; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_read_timeout 86400s; + proxy_send_timeout 86400s; + } + location /api/ { proxy_pass http://eifeldc_server; proxy_set_header Host $host; diff --git a/server/Cargo.toml b/server/Cargo.toml index 79ae3d6..418a591 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -15,8 +15,17 @@ tracing = { workspace = true } tracing-subscriber = { workspace = true } reqwest = { workspace = true } url = { workspace = true } -axum = { version = "0.7", features = ["ws"] } +axum = { version = "0.7", features = ["ws", "multipart"] } tower = "0.4" tower-http = { version = "0.5", features = ["cors", "fs", "trace"] } chrono = { version = "0.4", features = ["serde"] } -uuid = { version = "1", features = ["v4"] } \ No newline at end of file +uuid = { version = "1", features = ["v4"] } +mime = "0.3" +futures = "0.3" +rusqlite = { version = "0.30", features = ["bundled"] } +livekit-api = { version = "0.4", features = ["access-token"] } +prometheus = "0.13" +lazy_static = "1" + +[dev-dependencies] +tempfile = "3" \ No newline at end of file diff --git a/server/src/lib.rs b/server/src/lib.rs index 44a1cf9..8dcf175 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -1,4 +1,6 @@ pub mod routes; +pub mod session_store; pub mod state; -pub use state::ServerState; \ No newline at end of file +pub use session_store::SessionStore; +pub use state::ServerState; diff --git a/server/src/main.rs b/server/src/main.rs index 79097c3..37e937e 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,20 +1,66 @@ use eifeldc_server::routes::api_router; +use eifeldc_server::routes::metrics; +use eifeldc_server::session_store::SessionStore; +use eifeldc_server::state::{LiveKitConfig, Session}; use eifeldc_server::ServerState; -use tower_http::services::ServeDir; use std::net::SocketAddr; +use std::sync::Arc; +use tower_http::services::ServeDir; #[tokio::main] async fn main() { - tracing_subscriber::fmt::init(); + init_tracing(); + + metrics::init_metrics(); + tracing::info!("Metrics initialized"); + + let db_path = std::env::var("EIFELDC_DB").unwrap_or_else(|_| "eifeldc.db".to_string()); + + let session_store = + Arc::new(SessionStore::new(&db_path).expect("Failed to open session database")); + + let livekit = LiveKitConfig { + api_key: std::env::var("LIVEKIT_API_KEY").unwrap_or_else(|_| "devkey".to_string()), + api_secret: std::env::var("LIVEKIT_API_SECRET").unwrap_or_else(|_| "devsecret".to_string()), + url: std::env::var("LIVEKIT_URL").unwrap_or_else(|_| "ws://localhost:7880".to_string()), + }; + tracing::info!("LiveKit URL: {}", livekit.url); + + let session_ttl: Option = + std::env::var("EIFELDC_SESSION_TTL").ok().map(|v| { + let secs: u64 = v.parse().unwrap_or(86400); + std::time::Duration::from_secs(secs) + }); + if let Some(ttl) = session_ttl { + tracing::info!("Session TTL: {}s", ttl.as_secs()); + } else { + tracing::info!("Session TTL: unlimited"); + } + + let state = ServerState::new(session_store.clone(), livekit, session_ttl); + + { + let stored_sessions = session_store.get_all_sessions().await.unwrap_or_default(); + if !stored_sessions.is_empty() { + tracing::info!("Restoring {} session(s)...", stored_sessions.len()); + for stored in &stored_sessions { + match restore_session(stored, &state).await { + Ok(()) => tracing::info!("Restored session for user {}", stored.user_id), + Err(e) => { + tracing::warn!("Failed to restore session for {}: {}", stored.user_id, e); + session_store.delete_session(&stored.token).await.ok(); + } + } + } + } + } - let state = ServerState::new(); let api = api_router(state); - let static_dir = std::env::var("EIFELDC_STATIC_DIR") - .unwrap_or_else(|_| "client/src-ui/dist".to_string()); + let static_dir = + std::env::var("EIFELDC_STATIC_DIR").unwrap_or_else(|_| "client/src-ui/dist".to_string()); - let app = api - .fallback_service(ServeDir::new(&static_dir)); + let app = api.fallback_service(ServeDir::new(&static_dir)); let addr: SocketAddr = ([0, 0, 0, 0], 3000).into(); tracing::info!("EifelDC Web Server listening on http://{}", addr); @@ -22,4 +68,80 @@ async fn main() { let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); axum::serve(listener, app).await.unwrap(); -} \ No newline at end of file +} + +async fn restore_session( + stored: &eifeldc_server::session_store::StoredSession, + state: &ServerState, +) -> Result<(), Box> { + use matrix_sdk::matrix_auth::{MatrixSession, MatrixSessionTokens}; + use matrix_sdk::ruma::device_id; + use matrix_sdk::Client; + + let client = Client::builder() + .homeserver_url(&stored.homeserver) + .build() + .await?; + + let user_id: matrix_sdk::ruma::OwnedUserId = stored.user_id.parse()?; + let device_id = stored + .device_id + .as_deref() + .map(matrix_sdk::ruma::OwnedDeviceId::from) + .unwrap_or_else(|| device_id!("EIFELDC").to_owned()); + + let session = MatrixSession { + meta: matrix_sdk::SessionMeta { user_id, device_id }, + tokens: MatrixSessionTokens { + access_token: stored.access_token.clone(), + refresh_token: stored.refresh_token.clone(), + }, + }; + + client.matrix_auth().restore_session(session).await?; + + let mut s = Session::new( + client.clone(), + stored.user_id.clone(), + stored.homeserver.clone(), + ); + + let sender = s.event_sender.clone(); + let sync_client = client.clone(); + let handle = tokio::spawn(async move { + eifeldc_server::routes::auth::start_sync(sync_client, sender).await; + }); + s.sync_handle = Some(handle); + + let mut state_inner = state.write().await; + state_inner.sessions.insert(stored.token.clone(), s); + + Ok(()) +} + +fn init_tracing() { + use tracing_subscriber::EnvFilter; + + let log_format = std::env::var("EIFELDC_LOG_FORMAT").unwrap_or_else(|_| "pretty".to_string()); + let env_filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("eifeldc_server=info,tower_http=info")); + + match log_format.as_str() { + "json" => { + tracing_subscriber::fmt() + .json() + .with_env_filter(env_filter) + .with_target(true) + .with_thread_ids(false) + .with_file(false) + .with_line_number(false) + .init(); + } + _ => { + tracing_subscriber::fmt() + .with_env_filter(env_filter) + .with_target(true) + .init(); + } + } +} diff --git a/server/src/routes/auth.rs b/server/src/routes/auth.rs index 20e6309..89365ad 100644 --- a/server/src/routes/auth.rs +++ b/server/src/routes/auth.rs @@ -1,11 +1,9 @@ -use axum::{ - extract::State, - http::{HeaderMap, StatusCode}, - Json, -}; +use crate::session_store::StoredSession; +use crate::state::{Session, WsEvent}; +use axum::{extract::State, http::HeaderMap, Json}; +use matrix_sdk::config::SyncSettings; use matrix_sdk::Client; use serde::{Deserialize, Serialize}; -use crate::state::VoiceManager; #[derive(Deserialize)] pub struct LoginRequest { @@ -14,6 +12,21 @@ pub struct LoginRequest { pub password: String, } +impl LoginRequest { + pub fn validate(&self) -> Result<(), &'static str> { + if self.homeserver.is_empty() || self.homeserver.len() > 2048 { + return Err("Invalid homeserver"); + } + if self.username.is_empty() || self.username.len() > 256 { + return Err("Invalid username"); + } + if self.password.is_empty() || self.password.len() > 4096 { + return Err("Invalid password"); + } + Ok(()) + } +} + #[derive(Serialize)] pub struct LoginResult { pub success: bool, @@ -29,36 +42,83 @@ pub struct RegisterRequest { pub password: String, } +impl RegisterRequest { + pub fn validate(&self) -> Result<(), &'static str> { + if self.homeserver.is_empty() || self.homeserver.len() > 2048 { + return Err("Invalid homeserver"); + } + if self.username.is_empty() || self.username.len() > 256 { + return Err("Invalid username"); + } + if self.password.is_empty() || self.password.len() > 4096 { + return Err("Invalid password"); + } + Ok(()) + } +} + pub async fn login( State(state): State, Json(req): Json, -) -> Result, StatusCode> { +) -> Result, axum::http::StatusCode> { + req.validate() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; let client = Client::builder() .homeserver_url(&req.homeserver) .build() .await - .map_err(|_| StatusCode::BAD_REQUEST)?; + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; client .matrix_auth() .login_username(&req.username, &req.password) .send() .await - .map_err(|_| StatusCode::UNAUTHORIZED)?; + .map_err(|_| axum::http::StatusCode::UNAUTHORIZED)?; - let user_id = client - .user_id() - .map(|u| u.to_string()) - .unwrap_or_default(); + let user_id = client.user_id().map(|u| u.to_string()).unwrap_or_default(); let token = uuid::Uuid::new_v4().to_string(); - let mut s = state.write().await; - s.sessions.insert(token.clone(), crate::state::Session { - client, - user_id: user_id.clone(), - voice_manager: VoiceManager::new(), + let matrix_session = client + .matrix_auth() + .session() + .ok_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + + let mut session = Session::new(client.clone(), user_id.clone(), req.homeserver.clone()); + { + let s = state.read().await; + if let Some(ttl) = s.session_ttl { + session = session.with_ttl(ttl); + } + } + + let sender = session.event_sender.clone(); + let sync_client = client.clone(); + let handle = tokio::spawn(async move { + start_sync(sync_client, sender).await; }); + session.sync_handle = Some(handle); + + let stored = StoredSession { + token: token.clone(), + user_id: user_id.clone(), + homeserver: req.homeserver.clone(), + access_token: matrix_session.tokens.access_token.clone(), + device_id: Some(matrix_session.meta.device_id.to_string()), + refresh_token: matrix_session.tokens.refresh_token.clone(), + }; + + { + let s = state.read().await; + s.session_store + .save_session(&stored) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + } + + let mut s = state.write().await; + s.sessions.insert(token.clone(), session); Ok(Json(LoginResult { success: true, @@ -71,12 +131,14 @@ pub async fn login( pub async fn register( State(state): State, Json(req): Json, -) -> Result, StatusCode> { +) -> Result, axum::http::StatusCode> { + req.validate() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; let client = Client::builder() .homeserver_url(&req.homeserver) .build() .await - .map_err(|_| StatusCode::BAD_REQUEST)?; + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; let mut request = matrix_sdk::ruma::api::client::account::register::v3::Request::new(); request.username = Some(req.username); @@ -86,21 +148,51 @@ pub async fn register( .matrix_auth() .register(request) .await - .map_err(|_| StatusCode::BAD_REQUEST)?; + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; - let user_id = client - .user_id() - .map(|u| u.to_string()) - .unwrap_or_default(); + let user_id = client.user_id().map(|u| u.to_string()).unwrap_or_default(); let token = uuid::Uuid::new_v4().to_string(); - let mut s = state.write().await; - s.sessions.insert(token.clone(), crate::state::Session { - client, - user_id: user_id.clone(), - voice_manager: VoiceManager::new(), + let matrix_session = client + .matrix_auth() + .session() + .ok_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + + let mut session = Session::new(client.clone(), user_id.clone(), req.homeserver.clone()); + { + let s = state.read().await; + if let Some(ttl) = s.session_ttl { + session = session.with_ttl(ttl); + } + } + + let sender = session.event_sender.clone(); + let sync_client = client.clone(); + let handle = tokio::spawn(async move { + start_sync(sync_client, sender).await; }); + session.sync_handle = Some(handle); + + let stored = StoredSession { + token: token.clone(), + user_id: user_id.clone(), + homeserver: req.homeserver.clone(), + access_token: matrix_session.tokens.access_token.clone(), + device_id: Some(matrix_session.meta.device_id.to_string()), + refresh_token: matrix_session.tokens.refresh_token.clone(), + }; + + { + let s = state.read().await; + s.session_store + .save_session(&stored) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + } + + let mut s = state.write().await; + s.sessions.insert(token.clone(), session); Ok(Json(LoginResult { success: true, @@ -113,22 +205,34 @@ pub async fn register( pub async fn logout( State(state): State, headers: HeaderMap, -) -> Result, StatusCode> { - let token = extract_token(&headers).ok_or(StatusCode::UNAUTHORIZED)?; +) -> Result, axum::http::StatusCode> { + let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; let mut s = state.write().await; - if let Some(session) = s.sessions.remove(&token) { + if let Some(mut session) = s.sessions.remove(&token) { + if let Some(handle) = session.sync_handle.take() { + handle.abort(); + } let _ = session.client.matrix_auth().logout().await; } + { + let s_read = state.read().await; + s_read + .session_store + .delete_session(&token) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + } + Ok(Json(true)) } pub async fn get_current_user( State(state): State, headers: HeaderMap, -) -> Result>, StatusCode> { - let token = extract_token(&headers).ok_or(StatusCode::UNAUTHORIZED)?; +) -> Result>, axum::http::StatusCode> { + let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; let s = state.read().await; let user_id = s.sessions.get(&token).map(|s| s.user_id.clone()); @@ -147,14 +251,377 @@ pub async fn auth_middleware( headers: HeaderMap, State(state): State, request: axum::extract::Request, - next: middleware::Next, -) -> Result { - let token = extract_token(&headers).ok_or(StatusCode::UNAUTHORIZED)?; + next: axum::middleware::Next, +) -> Result { + let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; - let s = state.read().await; - if !s.sessions.contains_key(&token) { - return Err(StatusCode::UNAUTHORIZED); + let expired = { + let s = state.read().await; + match s.sessions.get(&token) { + Some(session) => session.is_expired(), + None => return Err(axum::http::StatusCode::UNAUTHORIZED), + } + }; + + if expired { + let mut s = state.write().await; + if let Some(mut session) = s.sessions.remove(&token) { + if let Some(handle) = session.sync_handle.take() { + handle.abort(); + } + } + drop(s); + { + let s = state.read().await; + let _ = s.session_store.delete_session(&token).await; + } + return Err(axum::http::StatusCode::UNAUTHORIZED); } Ok(next.run(request).await) -} \ No newline at end of file +} + +pub async fn start_sync(client: Client, sender: tokio::sync::broadcast::Sender) { + let mut sync_token: Option = None; + loop { + let mut settings = SyncSettings::new(); + if let Some(token) = sync_token.as_ref() { + settings = settings.token(token.clone()); + } + match client.sync_once(settings).await { + Ok(response) => { + sync_token = Some(response.next_batch); + + for (room_id, joined) in &response.rooms.join { + let room = match client.get_room(room_id) { + Some(r) => r, + None => continue, + }; + let name = match room.display_name().await { + Ok(n) => n.to_string(), + Err(_) => room_id.to_string(), + }; + let _ = sender.send(WsEvent::RoomJoined { + room_id: room_id.to_string(), + name, + }); + + for event in &joined.timeline.events { + if let Some(event_id) = event.event_id() { + let raw_json: serde_json::Value = match event.event.deserialize_as() { + Ok(v) => v, + Err(_) => continue, + }; + let sender_user = raw_json + .get("sender") + .and_then(|v| v.as_str()) + .unwrap_or(""); + let event_type = + raw_json.get("type").and_then(|v| v.as_str()).unwrap_or(""); + + if event_type == "m.room.message" { + let content = + raw_json.get("content").unwrap_or(&serde_json::Value::Null); + let new_content = content.get("m.new_content"); + + if new_content.is_some() { + let relates_to = content.get("m.relates_to"); + let original_id = relates_to + .and_then(|r| r.get("event_id")) + .and_then(|v| v.as_str()) + .unwrap_or(""); + let new_body = new_content + .and_then(|nc| nc.get("body")) + .and_then(|v| v.as_str()) + .unwrap_or(""); + if !original_id.is_empty() && !new_body.is_empty() { + let _ = sender.send(WsEvent::MessageEdited { + room_id: room_id.to_string(), + event_id: original_id.to_string(), + new_body: new_body.to_string(), + }); + } + } else { + let body = + content.get("body").and_then(|v| v.as_str()).unwrap_or(""); + let timestamp = raw_json + .get("origin_server_ts") + .and_then(|v| v.as_u64()) + .unwrap_or(0); + + let msgtype = content + .get("msgtype") + .and_then(|v| v.as_str()) + .unwrap_or("m.text"); + + let media_url = content + .get("url") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + .or_else(|| { + content + .get("file") + .and_then(|f| f.get("url")) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + }); + + let filename = if msgtype == "m.image" + || msgtype == "m.file" + || msgtype == "m.video" + || msgtype == "m.audio" + { + Some(body.to_string()) + } else { + None + }; + + let mimetype = content + .get("info") + .and_then(|i| i.get("mimetype")) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + let relates_to = content.get("m.relates_to"); + let reply_to = relates_to + .and_then(|r| r.get("m.in_reply_to")) + .and_then(|r| r.get("event_id")) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + let is_thread = relates_to + .and_then(|r| r.get("rel_type")) + .and_then(|v| v.as_str()) + == Some("m.thread"); + + if !body.is_empty() || media_url.is_some() { + if is_thread { + if let Some(root_id) = relates_to + .and_then(|r| r.get("event_id")) + .and_then(|v| v.as_str()) + { + let _ = sender.send(WsEvent::ThreadReply { + room_id: room_id.to_string(), + root_event_id: root_id.to_string(), + event_id: event_id.to_string(), + sender: sender_user.to_string(), + body: body.to_string(), + timestamp, + }); + } + } else { + let _ = sender.send(WsEvent::Message { + room_id: room_id.to_string(), + event_id: event_id.to_string(), + sender: sender_user.to_string(), + body: body.to_string(), + timestamp, + reply_to, + msgtype: Some(msgtype.to_string()), + media_url: media_url.clone(), + filename, + mimetype, + }); + } + } + } + } else if event_type == "m.reaction" { + let content = + raw_json.get("content").unwrap_or(&serde_json::Value::Null); + if let Some(relates_to) = content.get("m.relates_to") { + let target_id = relates_to + .get("event_id") + .and_then(|v| v.as_str()) + .unwrap_or(""); + let key = relates_to + .get("key") + .and_then(|v| v.as_str()) + .unwrap_or(""); + if !target_id.is_empty() && !key.is_empty() { + let _ = sender.send(WsEvent::Reaction { + room_id: room_id.to_string(), + event_id: target_id.to_string(), + key: key.to_string(), + sender: sender_user.to_string(), + }); + } + } + } else if event_type == "m.room.redaction" { + let redacts = raw_json + .get("redacts") + .and_then(|v| v.as_str()) + .unwrap_or(""); + if !redacts.is_empty() { + let _ = sender.send(WsEvent::MessageDeleted { + room_id: room_id.to_string(), + redacts: redacts.to_string(), + }); + } + } + } + } + + for raw_ephemeral in &joined.ephemeral { + let val: serde_json::Value = match raw_ephemeral.deserialize_as() { + Ok(v) => v, + Err(_) => continue, + }; + let event_type = val.get("type").and_then(|v| v.as_str()).unwrap_or(""); + if event_type == "m.typing" { + let user_ids = val + .get("content") + .and_then(|c| c.get("user_ids")) + .and_then(|v| v.as_array()); + if let Some(ids) = user_ids { + for uid in ids { + if let Some(user_str) = uid.as_str() { + let _ = sender.send(WsEvent::Typing { + room_id: room_id.to_string(), + user_id: user_str.to_string(), + typing: true, + }); + } + } + } + } + } + } + + for room_id in response.rooms.leave.keys() { + let _ = sender.send(WsEvent::RoomLeft { + room_id: room_id.to_string(), + }); + } + + for raw_presence in &response.presence { + let val: serde_json::Value = match raw_presence.deserialize_as() { + Ok(v) => v, + Err(_) => continue, + }; + let user_id = val.get("sender").and_then(|v| v.as_str()).unwrap_or(""); + let presence = val + .get("content") + .and_then(|c| c.get("presence")) + .and_then(|v| v.as_str()) + .unwrap_or("offline"); + let status_msg = val + .get("content") + .and_then(|c| c.get("status_msg")) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + if !user_id.is_empty() { + let status = match presence { + "online" => "online", + "unavailable" => "idle", + _ => "offline", + }; + let _ = sender.send(WsEvent::Presence { + user_id: user_id.to_string(), + status: status.to_string(), + status_msg, + }); + } + } + } + Err(e) => { + tracing::error!("Sync error: {}", e); + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn login_request_validate_ok() { + let req = LoginRequest { + homeserver: "https://matrix.example.org".to_string(), + username: "alice".to_string(), + password: "secret123".to_string(), + }; + assert!(req.validate().is_ok()); + } + + #[test] + fn login_request_reject_empty_homeserver() { + let req = LoginRequest { + homeserver: "".to_string(), + username: "alice".to_string(), + password: "secret".to_string(), + }; + assert!(req.validate().is_err()); + } + + #[test] + fn login_request_reject_empty_username() { + let req = LoginRequest { + homeserver: "https://matrix.example.org".to_string(), + username: "".to_string(), + password: "secret".to_string(), + }; + assert!(req.validate().is_err()); + } + + #[test] + fn login_request_reject_empty_password() { + let req = LoginRequest { + homeserver: "https://matrix.example.org".to_string(), + username: "alice".to_string(), + password: "".to_string(), + }; + assert!(req.validate().is_err()); + } + + #[test] + fn login_request_reject_long_username() { + let req = LoginRequest { + homeserver: "https://matrix.example.org".to_string(), + username: "a".repeat(300), + password: "secret".to_string(), + }; + assert!(req.validate().is_err()); + } + + #[test] + fn register_request_validate_ok() { + let req = RegisterRequest { + homeserver: "https://matrix.example.org".to_string(), + username: "bob".to_string(), + password: "password".to_string(), + }; + assert!(req.validate().is_ok()); + } + + #[test] + fn register_request_reject_empty_fields() { + let req = RegisterRequest { + homeserver: "".to_string(), + username: "".to_string(), + password: "".to_string(), + }; + assert!(req.validate().is_err()); + } + + #[test] + fn extract_token_from_bearer() { + let mut headers = HeaderMap::new(); + headers.insert("Authorization", "Bearer my-token-123".parse().unwrap()); + assert_eq!(extract_token(&headers), Some("my-token-123".to_string())); + } + + #[test] + fn extract_token_missing_header() { + let headers = HeaderMap::new(); + assert_eq!(extract_token(&headers), None); + } + + #[test] + fn extract_token_invalid_scheme() { + let mut headers = HeaderMap::new(); + headers.insert("Authorization", "Basic dXNlcjpwYXNz".parse().unwrap()); + assert_eq!(extract_token(&headers), None); + } +} diff --git a/server/src/routes/emoji.rs b/server/src/routes/emoji.rs index 883207d..12ea5de 100644 --- a/server/src/routes/emoji.rs +++ b/server/src/routes/emoji.rs @@ -1,13 +1,13 @@ +use super::auth::extract_token; +use crate::ServerState; use axum::{ extract::{Path, State}, http::HeaderMap, Json, }; use serde::{Deserialize, Serialize}; -use crate::ServerState; -use super::auth::extract_token; -#[derive(Serialize)] +#[derive(Serialize, Clone)] pub struct CustomEmoji { pub id: String, pub name: String, @@ -33,10 +33,69 @@ pub struct Sticker { pub async fn get_custom_emoji( State(state): State, headers: HeaderMap, - Path(_room_id): Path, + Path(room_id): Path, ) -> Result>, axum::http::StatusCode> { - let _token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; - Ok(Json(Vec::new())) + let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let s = state.read().await; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + + let rid: matrix_sdk::ruma::OwnedRoomId = room_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + let room = session + .client + .get_room(&rid) + .ok_or(axum::http::StatusCode::NOT_FOUND)?; + + let mut emojis = Vec::new(); + + let event_type: matrix_sdk::ruma::events::StateEventType = "im.ponies.room_emotes".into(); + + if let Ok(Some(raw_event)) = room.get_state_event(event_type, "").await { + let raw_json_str = match &raw_event { + matrix_sdk::deserialized_responses::RawAnySyncOrStrippedState::Sync(s) => { + s.json().get().to_string() + } + matrix_sdk::deserialized_responses::RawAnySyncOrStrippedState::Stripped(s) => { + s.json().get().to_string() + } + }; + if let Ok(value) = serde_json::from_str::(&raw_json_str) { + if let Some(content) = value.get("content") { + if let Some(images) = content.get("images").and_then(|v| v.as_object()) { + for (key, entry) in images { + let url = entry + .get("url") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + if url.is_empty() { + continue; + } + let name = entry + .get("body") + .and_then(|v| v.as_str()) + .unwrap_or(key) + .to_string(); + let animated = url.contains(".gif"); + emojis.push(CustomEmoji { + id: key.clone(), + name, + url, + category: "custom".to_string(), + animated, + }); + } + } + } + } + } + + Ok(Json(emojis)) } #[derive(Deserialize)] @@ -53,34 +112,50 @@ pub async fn upload_emoji( ) -> Result, axum::http::StatusCode> { let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; let s = state.read().await; - let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; - let rid: matrix_sdk::ruma::OwnedRoomId = room_id.as_str().try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; - let _room = session.client.get_room(&rid).ok_or(axum::http::StatusCode::NOT_FOUND)?; + let rid: matrix_sdk::ruma::OwnedRoomId = room_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + let _room = session + .client + .get_room(&rid) + .ok_or(axum::http::StatusCode::NOT_FOUND)?; let path = std::path::Path::new(&req.image_path); if !path.exists() { return Err(axum::http::StatusCode::BAD_REQUEST); } - let mime_type = match path.extension().and_then(|e| e.to_str()) { + let mime_type: mime::Mime = match path.extension().and_then(|e| e.to_str()) { Some("png") => "image/png", Some("jpg") | Some("jpeg") => "image/jpeg", Some("gif") => "image/gif", Some("webp") => "image/webp", _ => "image/png", - }; + } + .parse() + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; let data = std::fs::read(path).map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; - let content_type = mime_type.parse::().map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + let is_animated = mime_type.subtype() == mime::IMAGE_GIF.subtype(); - let response = session.client.media().upload(&content_type, data).await.map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + let response = session + .client + .media() + .upload(&mime_type, data) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(CustomEmoji { id: format!("emoji_{}", chrono::Utc::now().timestamp()), name: req.name, url: response.content_uri.to_string(), category: "custom".to_string(), - animated: mime_type == "image/gif", + animated: is_animated, })) -} \ No newline at end of file +} diff --git a/server/src/routes/media.rs b/server/src/routes/media.rs new file mode 100644 index 0000000..d643c91 --- /dev/null +++ b/server/src/routes/media.rs @@ -0,0 +1,47 @@ +use super::auth::extract_token; +use crate::ServerState; +use axum::{ + body::Body, + extract::{Path, State}, + http::HeaderMap, + response::Response, +}; +use matrix_sdk::ruma::OwnedMxcUri; + +pub async fn get_media( + State(state): State, + headers: HeaderMap, + Path(mxc_path): Path, +) -> Result, axum::http::StatusCode> { + let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let s = state.read().await; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + + let mxc_uri: OwnedMxcUri = mxc_path.as_str().into(); + + let request = + matrix_sdk::ruma::api::client::media::get_content::v3::Request::from_url(&mxc_uri) + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + + let response = session + .client + .send(request, None) + .await + .map_err(|_| axum::http::StatusCode::NOT_FOUND)?; + + let content_type = response + .content_type + .unwrap_or_else(|| "application/octet-stream".to_string()); + + let body = response.file; + + Ok(Response::builder() + .status(200) + .header("Content-Type", content_type) + .header("Cache-Control", "public, max-age=86400") + .body(Body::from(body)) + .unwrap()) +} diff --git a/server/src/routes/messages.rs b/server/src/routes/messages.rs index f93a251..4bb23f1 100644 --- a/server/src/routes/messages.rs +++ b/server/src/routes/messages.rs @@ -1,11 +1,13 @@ +use super::auth::extract_token; +use crate::routes::metrics::MESSAGES_SENT_TOTAL; +use crate::ServerState; use axum::{ - extract::{Path, State, Query}, + extract::{Path, Query, State}, http::HeaderMap, Json, }; -use serde::{Serialize, Deserialize}; -use crate::ServerState; -use super::auth::extract_token; +use matrix_sdk::room::MessagesOptions; +use serde::{Deserialize, Serialize}; #[derive(Serialize)] pub struct MessageInfo { @@ -14,6 +16,15 @@ pub struct MessageInfo { pub body: String, pub timestamp: u64, pub reply_to: Option, + pub edited: bool, + pub reactions: std::collections::HashMap>, + pub msgtype: String, + pub media_url: Option, + pub filename: Option, + pub mimetype: Option, + pub width: Option, + pub height: Option, + pub size: Option, } #[derive(Deserialize)] @@ -30,29 +41,196 @@ pub async fn get_room_messages( ) -> Result>, axum::http::StatusCode> { let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; let s = state.read().await; - let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; - let rid: matrix_sdk::ruma::OwnedRoomId = room_id.as_str().try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; - let room = session.client.get_room(&rid).ok_or(axum::http::StatusCode::NOT_FOUND)?; + let rid: matrix_sdk::ruma::OwnedRoomId = room_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + let room = session + .client + .get_room(&rid) + .ok_or(axum::http::StatusCode::NOT_FOUND)?; - let limit = query.limit.unwrap_or(50); - let mut options = matrix_sdk::ruma::api::client::message::get_message_events::v3::Request::new(); - options.limit = limit.into(); - options.from = query.from.map(|t| t.into()); + let mut options = MessagesOptions::backward(); + options.limit = matrix_sdk::ruma::uint!(50); + if let Some(from) = query.from { + options.from = Some(from); + } - let messages = room.messages(options).await.map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + let messages = room + .messages(options) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + let mut reactions_map: std::collections::HashMap< + String, + std::collections::HashMap>, + > = std::collections::HashMap::new(); let mut result = Vec::new(); - for msg in messages.chunk { - if let matrix_sdk::ruma::events::AnySyncMessageLikeEvent::RoomMessage(ev) = msg { - result.push(MessageInfo { - event_id: ev.event_id().to_string(), - sender: ev.sender().to_string(), - body: ev.content().body().to_string(), - timestamp: ev.origin_server_ts().0, - reply_to: ev.content().in_reply_to().map(|r| r.event_id.to_string()), - }); + + for msg in &messages.chunk { + let event_value: serde_json::Value = match msg.event.deserialize_as() { + Ok(v) => v, + Err(_) => continue, + }; + + let event_type = event_value + .get("type") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + if event_type == "m.reaction" { + if let Some(content) = event_value.get("content") { + if let Some(relates_to) = content.get("m.relates_to") { + if let (Some(target_id), Some(key)) = ( + relates_to.get("event_id").and_then(|v| v.as_str()), + relates_to.get("key").and_then(|v| v.as_str()), + ) { + let sender = event_value + .get("sender") + .and_then(|v| v.as_str()) + .unwrap_or(""); + reactions_map + .entry(target_id.to_string()) + .or_default() + .entry(key.to_string()) + .or_default() + .push(sender.to_string()); + } + } + } + continue; } + + if event_type == "m.room.redaction" { + continue; + } + + let event_id = match event_value.get("event_id").and_then(|v| v.as_str()) { + Some(id) => id.to_string(), + None => continue, + }; + + let sender = match event_value.get("sender").and_then(|v| v.as_str()) { + Some(s) => s.to_string(), + None => continue, + }; + + let timestamp = event_value + .get("origin_server_ts") + .and_then(|v| v.as_u64()) + .unwrap_or(0); + + let content = event_value + .get("content") + .unwrap_or(&serde_json::Value::Null); + let msgtype = content + .get("msgtype") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + if msgtype != "m.text" + && msgtype != "m.notice" + && msgtype != "m.emote" + && msgtype != "m.image" + && msgtype != "m.file" + && msgtype != "m.video" + && msgtype != "m.audio" + { + continue; + } + + let body = content + .get("body") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + + let new_content = content.get("m.new_content"); + let (display_body, edited) = if let Some(nc) = new_content { + let new_body = nc.get("body").and_then(|v| v.as_str()).unwrap_or(&body); + (new_body.to_string(), true) + } else { + (body.clone(), false) + }; + + if body.is_empty() && (msgtype == "m.text" || msgtype == "m.notice" || msgtype == "m.emote") + { + continue; + } + + let reply_to = content + .get("m.relates_to") + .and_then(|r| r.get("m.in_reply_to")) + .and_then(|r| r.get("event_id")) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + let reactions = reactions_map.remove(&event_id).unwrap_or_default(); + + let media_url = content + .get("url") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + .or_else(|| { + content + .get("file") + .and_then(|f| f.get("url")) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + }); + + let filename = if msgtype == "m.image" + || msgtype == "m.file" + || msgtype == "m.video" + || msgtype == "m.audio" + { + Some(body.clone()) + } else { + None + }; + + let mimetype = content + .get("info") + .and_then(|i| i.get("mimetype")) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + let width = content + .get("info") + .and_then(|i| i.get("w")) + .and_then(|v| v.as_u64()); + + let height = content + .get("info") + .and_then(|i| i.get("h")) + .and_then(|v| v.as_u64()); + + let size = content + .get("info") + .and_then(|i| i.get("size")) + .and_then(|v| v.as_u64()); + + result.push(MessageInfo { + event_id, + sender, + body: display_body, + timestamp, + reply_to, + edited, + reactions, + msgtype: msgtype.to_string(), + media_url, + filename, + mimetype, + width, + height, + size, + }); } Ok(Json(result)) @@ -71,14 +249,192 @@ pub async fn send_message( ) -> Result, axum::http::StatusCode> { let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; let s = state.read().await; - let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; - let rid: matrix_sdk::ruma::OwnedRoomId = room_id.as_str().try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; - let room = session.client.get_room(&rid).ok_or(axum::http::StatusCode::NOT_FOUND)?; + let rid: matrix_sdk::ruma::OwnedRoomId = room_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + let room = session + .client + .get_room(&rid) + .ok_or(axum::http::StatusCode::NOT_FOUND)?; - let content = matrix_sdk::ruma::events::room::message::RoomMessageEventContent::text_plain(&req.message); - let txn_id = matrix_sdk::ruma::TransactionId::new(); - let response = room.send(content, Some(&txn_id)).await.map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + let content = + matrix_sdk::ruma::events::room::message::RoomMessageEventContent::text_plain(&req.message); + let response = room + .send(content) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + + MESSAGES_SENT_TOTAL.inc(); + Ok(Json(response.event_id.to_string())) +} + +#[derive(Deserialize)] +pub struct EditMessageRequest { + pub event_id: String, + pub new_content: String, +} + +pub async fn edit_message( + State(state): State, + headers: HeaderMap, + Path(room_id): Path, + Json(req): Json, +) -> Result, axum::http::StatusCode> { + let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let s = state.read().await; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + + let rid: matrix_sdk::ruma::OwnedRoomId = room_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + let room = session + .client + .get_room(&rid) + .ok_or(axum::http::StatusCode::NOT_FOUND)?; + + let original_event_id: matrix_sdk::ruma::OwnedEventId = req + .event_id + .parse() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + + let content = matrix_sdk::ruma::events::room::message::RoomMessageEventContent::text_plain( + &req.new_content, + ) + .make_replacement( + matrix_sdk::ruma::events::room::message::ReplacementMetadata::new(original_event_id, None), + None, + ); + + let response = room + .send(content) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + Ok(Json(response.event_id.to_string())) +} + +#[derive(Deserialize)] +pub struct DeleteMessageRequest { + pub reason: Option, +} + +pub async fn delete_message( + State(state): State, + headers: HeaderMap, + Path((room_id, event_id)): Path<(String, String)>, + Json(req): Json, +) -> Result, axum::http::StatusCode> { + let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let s = state.read().await; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + + let rid: matrix_sdk::ruma::OwnedRoomId = room_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + let room = session + .client + .get_room(&rid) + .ok_or(axum::http::StatusCode::NOT_FOUND)?; + + let eid: matrix_sdk::ruma::OwnedEventId = event_id + .parse() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + + let response = room + .redact(&eid, req.reason.as_deref(), None) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(response.event_id.to_string())) -} \ No newline at end of file +} + +#[derive(Deserialize)] +pub struct ReactRequest { + pub event_id: String, + pub key: String, +} + +pub async fn react_to_message( + State(state): State, + headers: HeaderMap, + Path(room_id): Path, + Json(req): Json, +) -> Result, axum::http::StatusCode> { + let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let s = state.read().await; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + + let rid: matrix_sdk::ruma::OwnedRoomId = room_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + let room = session + .client + .get_room(&rid) + .ok_or(axum::http::StatusCode::NOT_FOUND)?; + + let event_id: matrix_sdk::ruma::OwnedEventId = req + .event_id + .parse() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + + let content = matrix_sdk::ruma::events::reaction::ReactionEventContent::new( + matrix_sdk::ruma::events::relation::Annotation::new(event_id, req.key), + ); + + let response = room + .send(content) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + Ok(Json(response.event_id.to_string())) +} + +#[derive(Deserialize)] +pub struct TypingRequest { + pub typing: bool, +} + +pub async fn set_typing( + State(state): State, + headers: HeaderMap, + Path(room_id): Path, + Json(req): Json, +) -> Result, axum::http::StatusCode> { + let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let s = state.read().await; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + + let rid: matrix_sdk::ruma::OwnedRoomId = room_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + let room = session + .client + .get_room(&rid) + .ok_or(axum::http::StatusCode::NOT_FOUND)?; + + room.typing_notice(req.typing) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(Json(true)) +} diff --git a/server/src/routes/metrics.rs b/server/src/routes/metrics.rs new file mode 100644 index 0000000..3ad749b --- /dev/null +++ b/server/src/routes/metrics.rs @@ -0,0 +1,82 @@ +use axum::{body::Body, extract::State, http::HeaderMap, response::IntoResponse}; +use lazy_static::lazy_static; +use prometheus::{Encoder, IntCounter, IntGauge, Registry, TextEncoder}; + +lazy_static! { + pub static ref REGISTRY: Registry = Registry::new(); + pub static ref HTTP_REQUESTS_TOTAL: IntCounter = + IntCounter::new("eifeldc_http_requests_total", "Total HTTP requests") + .expect("metric can be created"); + pub static ref ACTIVE_SESSIONS: IntGauge = + IntGauge::new("eifeldc_active_sessions", "Number of active sessions") + .expect("metric can be created"); + pub static ref ACTIVE_WEBSOCKETS: IntGauge = IntGauge::new( + "eifeldc_active_websockets", + "Number of active WebSocket connections" + ) + .expect("metric can be created"); + pub static ref MESSAGES_SENT_TOTAL: IntCounter = + IntCounter::new("eifeldc_messages_sent_total", "Total messages sent") + .expect("metric can be created"); + pub static ref ROOMS_JOINED_TOTAL: IntCounter = + IntCounter::new("eifeldc_rooms_joined_total", "Total room join operations") + .expect("metric can be created"); + pub static ref UPLOADS_TOTAL: IntCounter = + IntCounter::new("eifeldc_uploads_total", "Total file uploads") + .expect("metric can be created"); + pub static ref VOICE_PARTICIPANTS: IntGauge = IntGauge::new( + "eifeldc_voice_participants", + "Current voice channel participants" + ) + .expect("metric can be created"); +} + +pub fn init_metrics() { + REGISTRY + .register(Box::new(HTTP_REQUESTS_TOTAL.clone())) + .expect("metric can be registered"); + REGISTRY + .register(Box::new(ACTIVE_SESSIONS.clone())) + .expect("metric can be registered"); + REGISTRY + .register(Box::new(ACTIVE_WEBSOCKETS.clone())) + .expect("metric can be registered"); + REGISTRY + .register(Box::new(MESSAGES_SENT_TOTAL.clone())) + .expect("metric can be registered"); + REGISTRY + .register(Box::new(ROOMS_JOINED_TOTAL.clone())) + .expect("metric can be registered"); + REGISTRY + .register(Box::new(UPLOADS_TOTAL.clone())) + .expect("metric can be registered"); + REGISTRY + .register(Box::new(VOICE_PARTICIPANTS.clone())) + .expect("metric can be registered"); +} + +pub async fn get_metrics( + State(state): State, + _headers: HeaderMap, +) -> impl IntoResponse { + ACTIVE_SESSIONS.set(state.read().await.sessions.len() as i64); + + let mut participants: i64 = 0; + let s = state.read().await; + for room in s.voice_rooms.rooms.values() { + participants += room.participants.len() as i64; + } + VOICE_PARTICIPANTS.set(participants); + drop(s); + + let metric_families = REGISTRY.gather(); + let mut buffer = Vec::new(); + let encoder = TextEncoder::new(); + encoder.encode(&metric_families, &mut buffer).unwrap(); + + axum::http::Response::builder() + .status(200) + .header("Content-Type", "text/plain; version=0.0.4; charset=utf-8") + .body(Body::from(buffer)) + .unwrap() +} diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index 8781236..52b1b9f 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -1,37 +1,79 @@ pub mod auth; -pub mod rooms; -pub mod messages; -pub mod presence; -pub mod voice; pub mod emoji; +pub mod media; +pub mod messages; +pub mod metrics; +pub mod presence; +pub mod profile; +pub mod rate_limit; pub mod roles; +pub mod rooms; +pub mod threads; +pub mod upload; +pub mod voice; +pub mod ws; -use axum::{ - Router, - routing::{get, post}, - middleware, - http::HeaderMap, -}; -use tower_http::cors::{CorsLayer, Any}; -use crate::ServerState; use crate::routes::auth::auth_middleware; +use crate::routes::rate_limit::{RateLimitConfig, RateLimiter}; +use crate::ServerState; +use axum::{ + routing::{get, post}, + Router, +}; +use std::time::Duration; +use tower_http::cors::{AllowHeaders, AllowMethods, CorsLayer}; pub fn api_router(state: ServerState) -> Router { - let cors = CorsLayer::new() - .allow_origin(Any) - .allow_methods(Any) - .allow_headers(Any); + let allowed_origins = std::env::var("EIFELDC_CORS_ORIGINS").unwrap_or_else(|_| "*".to_string()); + + let cors = if allowed_origins == "*" { + CorsLayer::permissive() + } else { + let origins: Vec<_> = allowed_origins + .split(',') + .filter_map(|s| s.trim().parse().ok()) + .collect(); + CorsLayer::new() + .allow_origin(origins) + .allow_methods(AllowMethods::any()) + .allow_headers(AllowHeaders::any()) + .max_age(Duration::from_secs(3600)) + }; + + let max_rpm = std::env::var("EIFELDC_RATE_LIMIT") + .ok() + .map(|v| v.parse().unwrap_or(60)) + .unwrap_or(60); + let rate_limiter = RateLimiter::new(RateLimitConfig { + max_requests: max_rpm, + window_secs: 60, + }); Router::new() .nest("/api", api_routes(state)) .layer(cors) + .layer(axum::middleware::from_fn_with_state( + rate_limiter, + rate_limit::rate_limit_middleware, + )) } fn api_routes(state: ServerState) -> Router { + let max_upload_bytes: usize = std::env::var("EIFELDC_MAX_UPLOAD_MB") + .ok() + .map(|v| v.parse().unwrap_or(50)) + .unwrap_or(50) + * 1024 + * 1024; + + let default_body_limit = axum::extract::DefaultBodyLimit::max(max_upload_bytes); + let public = Router::new() .route("/login", post(auth::login)) .route("/register", post(auth::register)) - .route("/current-user", get(auth::get_current_user)); + .route("/current-user", get(auth::get_current_user)) + .route("/ws", get(ws::ws_handler)) + .route("/metrics", get(metrics::get_metrics)); let protected = Router::new() .route("/logout", post(auth::logout)) @@ -40,24 +82,63 @@ fn api_routes(state: ServerState) -> Router { .route("/rooms/join", post(rooms::join_room)) .route("/rooms/{room_id}/leave", post(rooms::leave_room)) .route("/rooms/{room_id}/members", get(rooms::get_room_members)) - .route("/rooms/{room_id}/messages", get(messages::get_room_messages)) + .route("/rooms/{room_id}/name", post(rooms::set_room_name)) + .route("/rooms/{room_id}/topic", post(rooms::set_room_topic)) + .route("/rooms/{room_id}/avatar", post(rooms::set_room_avatar)) + .route( + "/rooms/{room_id}/messages", + get(messages::get_room_messages), + ) .route("/rooms/{room_id}/send", post(messages::send_message)) + .route("/rooms/{room_id}/edit", post(messages::edit_message)) + .route( + "/rooms/{room_id}/delete/{event_id}", + post(messages::delete_message), + ) + .route("/rooms/{room_id}/react", post(messages::react_to_message)) + .route("/rooms/{room_id}/typing", post(messages::set_typing)) .route("/presence/set", post(presence::set_presence)) .route("/presence/{user_id}", get(presence::get_presence)) .route("/voice/join", post(voice::join_voice_channel)) .route("/voice/leave", post(voice::leave_voice_channel)) .route("/voice/toggle-mute", post(voice::toggle_mute)) .route("/voice/toggle-deafen", post(voice::toggle_deafen)) + .route("/voice/participants", get(voice::get_voice_participants)) .route("/rooms/{room_id}/roles", get(roles::get_roles)) .route("/rooms/{room_id}/roles/assign", post(roles::assign_role)) .route("/rooms/{room_id}/roles/remove", post(roles::remove_role)) - .route("/rooms/{room_id}/permissions/{user_id}", get(roles::get_permissions)) + .route( + "/rooms/{room_id}/permissions/{user_id}", + get(roles::get_permissions), + ) .route("/rooms/{room_id}/emoji", get(emoji::get_custom_emoji)) .route("/rooms/{room_id}/emoji/upload", post(emoji::upload_emoji)) - .layer(middleware::from_fn_with_state(state.clone(), auth_middleware)); + .route("/rooms/{room_id}/threads", get(threads::get_threads)) + .route( + "/rooms/{room_id}/threads/{thread_id}", + get(threads::get_thread_messages), + ) + .route( + "/rooms/{room_id}/threads/{thread_id}/reply", + post(threads::send_thread_reply), + ) + .route("/rooms/{room_id}/reply", post(threads::send_reply)) + .route("/rooms/{room_id}/upload", post(upload::upload_file)) + .route("/rooms/unread", get(rooms::get_unread_counts)) + .route("/rooms/{room_id}/read", post(rooms::mark_room_read)) + .route("/media/{mxc_path}", get(media::get_media)) + .route("/profile/me", get(profile::get_own_profile)) + .route("/profile/{user_id}", get(profile::get_profile)) + .route("/profile/displayname", post(profile::set_display_name)) + .route("/profile/avatar", post(profile::upload_avatar)) + .layer(axum::middleware::from_fn_with_state( + state.clone(), + auth_middleware, + )); Router::new() .merge(public) .merge(protected) .with_state(state) -} \ No newline at end of file + .layer(default_body_limit) +} diff --git a/server/src/routes/presence.rs b/server/src/routes/presence.rs index 6074e4a..347fb51 100644 --- a/server/src/routes/presence.rs +++ b/server/src/routes/presence.rs @@ -1,11 +1,11 @@ +use super::auth::extract_token; +use crate::ServerState; use axum::{ extract::{Path, State}, http::HeaderMap, Json, }; use serde::{Deserialize, Serialize}; -use crate::ServerState; -use super::auth::extract_token; #[derive(Deserialize)] pub struct SetPresenceRequest { @@ -28,21 +28,31 @@ pub async fn set_presence( ) -> Result, axum::http::StatusCode> { let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; let s = state.read().await; - let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; let presence_state = match req.status.as_str() { "online" => matrix_sdk::ruma::presence::PresenceState::Online, - "away" => matrix_sdk::ruma::presence::PresenceState::Away, "unavailable" => matrix_sdk::ruma::presence::PresenceState::Unavailable, _ => matrix_sdk::ruma::presence::PresenceState::Online, }; - let user_id = session.client.user_id().ok_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; - let mut request = matrix_sdk::ruma::api::client::presence::set_presence::v3::Request::new(user_id.to_owned()); - request.presence = presence_state; - request.status_msg = req.status_msg; + let user_id = session + .client + .user_id() + .ok_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + let request = matrix_sdk::ruma::api::client::presence::set_presence::v3::Request::new( + user_id.to_owned(), + presence_state, + ); - session.client.send(request, None).await.map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + session + .client + .send(request, None) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(true)) } @@ -54,16 +64,26 @@ pub async fn get_presence( ) -> Result, axum::http::StatusCode> { let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; let s = state.read().await; - let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; - let uid: matrix_sdk::ruma::OwnedUserId = user_id.as_str().try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; - let request = matrix_sdk::ruma::api::client::presence::get_presence::v3::Request::new(uid.to_owned()); + let uid: matrix_sdk::ruma::OwnedUserId = user_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + let request = + matrix_sdk::ruma::api::client::presence::get_presence::v3::Request::new(uid.to_owned()); - let response = session.client.send(request, None).await.map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + let response = session + .client + .send(request, None) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; let status_str = match response.presence { matrix_sdk::ruma::presence::PresenceState::Online => "online", - matrix_sdk::ruma::presence::PresenceState::Away => "away", matrix_sdk::ruma::presence::PresenceState::Unavailable => "unavailable", _ => "offline", }; @@ -74,4 +94,4 @@ pub async fn get_presence( status_msg: response.status_msg, last_active: response.last_active_ago.map(|d| d.as_secs()), })) -} \ No newline at end of file +} diff --git a/server/src/routes/profile.rs b/server/src/routes/profile.rs new file mode 100644 index 0000000..4bdec8a --- /dev/null +++ b/server/src/routes/profile.rs @@ -0,0 +1,156 @@ +use super::auth::extract_token; +use crate::ServerState; +use axum::{ + extract::{Multipart, Path, State}, + http::HeaderMap, + Json, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize)] +pub struct UserProfile { + pub user_id: String, + pub display_name: Option, + pub avatar_url: Option, +} + +pub async fn get_profile( + State(state): State, + headers: HeaderMap, + Path(user_id): Path, +) -> Result, axum::http::StatusCode> { + let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let s = state.read().await; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + + let uid: matrix_sdk::ruma::OwnedUserId = user_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + + let profile = session + .client + .get_profile(&uid) + .await + .map_err(|_| axum::http::StatusCode::NOT_FOUND)?; + + Ok(Json(UserProfile { + user_id, + display_name: profile.displayname, + avatar_url: profile.avatar_url.map(|u| u.to_string()), + })) +} + +pub async fn get_own_profile( + State(state): State, + headers: HeaderMap, +) -> Result, axum::http::StatusCode> { + let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let s = state.read().await; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + + let user_id = session.user_id.clone(); + let uid: matrix_sdk::ruma::OwnedUserId = user_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + + let profile = session + .client + .get_profile(&uid) + .await + .map_err(|_| axum::http::StatusCode::NOT_FOUND)?; + + Ok(Json(UserProfile { + user_id, + display_name: profile.displayname, + avatar_url: profile.avatar_url.map(|u| u.to_string()), + })) +} + +#[derive(Deserialize)] +pub struct SetDisplayNameRequest { + pub display_name: String, +} + +pub async fn set_display_name( + State(state): State, + headers: HeaderMap, + Json(req): Json, +) -> Result, axum::http::StatusCode> { + let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let s = state.read().await; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + + session + .client + .account() + .set_display_name(Some(&req.display_name)) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(Json(true)) +} + +pub async fn upload_avatar( + State(state): State, + headers: HeaderMap, + mut multipart: Multipart, +) -> Result, axum::http::StatusCode> { + let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let s = state.read().await; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + + let field = multipart + .next_field() + .await + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)? + .ok_or(axum::http::StatusCode::BAD_REQUEST)?; + + let content_type = field + .content_type() + .unwrap_or("application/octet-stream") + .to_string(); + let data = field + .bytes() + .await + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + + let mime_type: mime::Mime = content_type.parse().unwrap_or(mime::IMAGE_PNG); + + let upload = session + .client + .media() + .upload(&mime_type, data.to_vec()) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + + let mxc_uri = upload.content_uri; + + session + .client + .account() + .set_avatar_url(Some(&mxc_uri)) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + + let user_id = session.user_id.clone(); + + Ok(Json(UserProfile { + user_id, + display_name: None, + avatar_url: Some(mxc_uri.to_string()), + })) +} diff --git a/server/src/routes/rate_limit.rs b/server/src/routes/rate_limit.rs new file mode 100644 index 0000000..7a14796 --- /dev/null +++ b/server/src/routes/rate_limit.rs @@ -0,0 +1,155 @@ +use axum::{ + body::Body, + extract::State, + http::{Request, StatusCode}, + middleware::Next, + response::Response, +}; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Instant; +use tokio::sync::RwLock; + +struct RateLimitEntry { + count: u32, + window_start: Instant, +} + +#[derive(Clone)] +pub struct RateLimitConfig { + pub max_requests: u32, + pub window_secs: u64, +} + +impl Default for RateLimitConfig { + fn default() -> Self { + Self { + max_requests: 60, + window_secs: 60, + } + } +} + +#[derive(Clone)] +pub struct RateLimiter { + entries: Arc>>, + config: RateLimitConfig, +} + +impl RateLimiter { + pub fn new(config: RateLimitConfig) -> Self { + Self { + entries: Arc::new(RwLock::new(HashMap::new())), + config, + } + } + + pub async fn check(&self, key: &str) -> bool { + let mut entries = self.entries.write().await; + let now = Instant::now(); + let window = std::time::Duration::from_secs(self.config.window_secs); + + match entries.get_mut(key) { + Some(entry) => { + if now.duration_since(entry.window_start) > window { + entry.count = 1; + entry.window_start = now; + true + } else if entry.count < self.config.max_requests { + entry.count += 1; + true + } else { + false + } + } + None => { + entries.insert( + key.to_string(), + RateLimitEntry { + count: 1, + window_start: now, + }, + ); + true + } + } + } +} + +pub async fn rate_limit_middleware( + State(limiter): State, + headers: axum::http::HeaderMap, + request: Request, + next: Next, +) -> Result { + let key = headers + .get("x-forwarded-for") + .or_else(|| headers.get("x-real-ip")) + .or_else(|| headers.get("authorization")) + .and_then(|v| v.to_str().ok()) + .unwrap_or("unknown") + .to_string(); + + if limiter.check(&key).await { + Ok(next.run(request).await) + } else { + Err(StatusCode::TOO_MANY_REQUESTS) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn rate_limit_allows_within_limit() { + let limiter = RateLimiter::new(RateLimitConfig { + max_requests: 5, + window_secs: 60, + }); + for _ in 0..5 { + assert!(limiter.check("user1").await); + } + } + + #[tokio::test] + async fn rate_limit_blocks_over_limit() { + let limiter = RateLimiter::new(RateLimitConfig { + max_requests: 3, + window_secs: 60, + }); + assert!(limiter.check("user1").await); + assert!(limiter.check("user1").await); + assert!(limiter.check("user1").await); + assert!(!limiter.check("user1").await); + } + + #[tokio::test] + async fn rate_limit_per_key() { + let limiter = RateLimiter::new(RateLimitConfig { + max_requests: 2, + window_secs: 60, + }); + assert!(limiter.check("user1").await); + assert!(limiter.check("user1").await); + assert!(!limiter.check("user1").await); + assert!(limiter.check("user2").await); + } + + #[tokio::test] + async fn rate_limit_default_config() { + let config = RateLimitConfig::default(); + assert_eq!(config.max_requests, 60); + assert_eq!(config.window_secs, 60); + } + + #[tokio::test] + async fn rate_limit_new_key_always_allowed() { + let limiter = RateLimiter::new(RateLimitConfig { + max_requests: 1, + window_secs: 60, + }); + assert!(limiter.check("new_user").await); + assert!(!limiter.check("new_user").await); + } +} diff --git a/server/src/routes/roles.rs b/server/src/routes/roles.rs index a1f6cb3..b5303d9 100644 --- a/server/src/routes/roles.rs +++ b/server/src/routes/roles.rs @@ -1,11 +1,12 @@ +use super::auth::extract_token; +use crate::ServerState; use axum::{ extract::{Path, State}, http::HeaderMap, Json, }; +use matrix_sdk::ruma::Int; use serde::{Deserialize, Serialize}; -use crate::ServerState; -use super::auth::extract_token; #[derive(Serialize, Deserialize, Clone)] pub struct Role { @@ -52,22 +53,65 @@ pub async fn get_roles( ) -> Result>, axum::http::StatusCode> { let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; let s = state.read().await; - let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; - let rid: matrix_sdk::ruma::OwnedRoomId = room_id.as_str().try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; - let room = session.client.get_room(&rid).ok_or(axum::http::StatusCode::NOT_FOUND)?; + let rid: matrix_sdk::ruma::OwnedRoomId = room_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + let room = session + .client + .get_room(&rid) + .ok_or(axum::http::StatusCode::NOT_FOUND)?; let mut roles = Vec::new(); - if let Ok(power_levels) = room.power_levels().await { - for (uid, power_level) in &power_levels.users { - roles.push(Role { - id: format!("role_{}", uid), - name: uid.to_string(), - color: if *power_level >= 100 { "#ed4245".to_string() } else if *power_level >= 50 { "#fee75c".to_string() } else { "#5865f2".to_string() }, - permissions: vec![], - position: *power_level as i32, - }); - } + let members = room + .members(matrix_sdk::RoomMemberships::JOIN) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + + let power_levels_content: Option< + matrix_sdk::ruma::events::room::power_levels::RoomPowerLevelsEventContent, + > = room + .get_state_event_static() + .await + .ok() + .flatten() + .and_then(|e| e.deserialize().ok()) + .and_then(|e| e.as_sync().cloned()) + .and_then(|e| e.as_original().cloned()) + .map(|e| e.content); + + for member in members { + let pl: i64 = if let Some(ref pl_content) = power_levels_content { + pl_content + .users + .get(member.user_id()) + .map_or(0, |v| i64::from(*v)) + } else { + 0 + }; + let color = if pl >= 100 { + "#ed4245".to_string() + } else if pl >= 50 { + "#fee75c".to_string() + } else { + "#5865f2".to_string() + }; + let display_name = member + .display_name() + .map(|s| s.to_string()) + .unwrap_or_else(|| member.user_id().to_string()); + roles.push(Role { + id: format!("role_{}", member.user_id()), + name: display_name, + color, + permissions: vec![], + position: pl as i32, + }); } Ok(Json(roles)) @@ -87,22 +131,34 @@ pub async fn assign_role( ) -> Result, axum::http::StatusCode> { let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; let s = state.read().await; - let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; - let rid: matrix_sdk::ruma::OwnedRoomId = room_id.as_str().try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; - let room = session.client.get_room(&rid).ok_or(axum::http::StatusCode::NOT_FOUND)?; - let uid: matrix_sdk::ruma::OwnedUserId = req.user_id.as_str().try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + let rid: matrix_sdk::ruma::OwnedRoomId = room_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + let room = session + .client + .get_room(&rid) + .ok_or(axum::http::StatusCode::NOT_FOUND)?; + let uid: matrix_sdk::ruma::OwnedUserId = req + .user_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; - let power_level: i64 = match req.role_id.as_str() { + let power_level: i32 = match req.role_id.as_str() { "admin" => 100, "moderator" => 50, _ => 0, }; - let mut content = matrix_sdk::ruma::events::room::power_levels::RoomPowerLevelsEventContent::new(); - content.users.insert(uid.to_owned(), power_level.into()); - - room.send_state_event(content).await.map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + room.update_power_levels(vec![(uid.as_ref(), Int::from(power_level))]) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(true)) } @@ -121,17 +177,28 @@ pub async fn remove_role( ) -> Result, axum::http::StatusCode> { let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; let s = state.read().await; - let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; - let rid: matrix_sdk::ruma::OwnedRoomId = room_id.as_str().try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; - let room = session.client.get_room(&rid).ok_or(axum::http::StatusCode::NOT_FOUND)?; - let uid: matrix_sdk::ruma::OwnedUserId = req.user_id.as_str().try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + let rid: matrix_sdk::ruma::OwnedRoomId = room_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + let room = session + .client + .get_room(&rid) + .ok_or(axum::http::StatusCode::NOT_FOUND)?; + let uid: matrix_sdk::ruma::OwnedUserId = req + .user_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; - if let Ok(mut power_levels) = room.power_levels().await { - power_levels.users.remove(&uid); - let content = matrix_sdk::ruma::events::room::power_levels::RoomPowerLevelsEventContent::from(power_levels); - room.send_state_event(content).await.map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; - } + room.update_power_levels(vec![(uid.as_ref(), Int::from(0))]) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(true)) } @@ -143,17 +210,41 @@ pub async fn get_permissions( ) -> Result, axum::http::StatusCode> { let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; let s = state.read().await; - let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; - let rid: matrix_sdk::ruma::OwnedRoomId = room_id.as_str().try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; - let room = session.client.get_room(&rid).ok_or(axum::http::StatusCode::NOT_FOUND)?; - let uid: matrix_sdk::ruma::OwnedUserId = user_id.as_str().try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + let rid: matrix_sdk::ruma::OwnedRoomId = room_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + let room = session + .client + .get_room(&rid) + .ok_or(axum::http::StatusCode::NOT_FOUND)?; + let uid: matrix_sdk::ruma::OwnedUserId = user_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; - let user_power = if let Ok(power_levels) = room.power_levels().await { - power_levels.users.get(&uid).copied().map(|p| p.into()).unwrap_or(power_levels.users_default as i64) + let power_levels_content: Option< + matrix_sdk::ruma::events::room::power_levels::RoomPowerLevelsEventContent, + > = room + .get_state_event_static() + .await + .ok() + .flatten() + .and_then(|e| e.deserialize().ok()) + .and_then(|e| e.as_sync().cloned()) + .and_then(|e| e.as_original().cloned()) + .map(|e| e.content); + + let user_power = if let Some(pl) = power_levels_content { + pl.users.get(&uid).map_or(0, |v| i64::from(*v)) } else { 0 }; Ok(Json(power_level_to_permissions(user_power))) -} \ No newline at end of file +} diff --git a/server/src/routes/rooms.rs b/server/src/routes/rooms.rs index f7a0764..e0ac925 100644 --- a/server/src/routes/rooms.rs +++ b/server/src/routes/rooms.rs @@ -1,11 +1,15 @@ +use super::auth::extract_token; +use crate::routes::metrics::ROOMS_JOINED_TOTAL; +use crate::ServerState; use axum::{ - extract::{Path, State}, + extract::{Multipart, Path, State}, http::HeaderMap, Json, }; -use serde::Serialize; -use crate::ServerState; -use super::auth::extract_token; +use matrix_sdk::ruma::api::client::receipt::create_receipt::v3::ReceiptType; +use matrix_sdk::ruma::events::receipt::ReceiptThread; +use matrix_sdk::RoomMemberships; +use serde::{Deserialize, Serialize}; #[derive(Serialize)] pub struct RoomInfo { @@ -15,6 +19,8 @@ pub struct RoomInfo { pub is_encrypted: bool, pub member_count: u64, pub topic: Option, + pub unread_notifications: u64, + pub unread_messages: u64, } pub async fn get_joined_rooms( @@ -23,16 +29,29 @@ pub async fn get_joined_rooms( ) -> Result>, axum::http::StatusCode> { let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; let s = state.read().await; - let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; let rooms = session.client.joined_rooms(); let mut result = Vec::new(); for room in rooms { - let name = room.display_name().await.map(|n| n.to_string()).unwrap_or_default(); + let name = room + .display_name() + .await + .map(|n| n.to_string()) + .unwrap_or_default(); let avatar_url = room.avatar_url().map(|u| u.to_string()); - let member_count = room.joined_members().len() as u64; + let member_count = room + .members(RoomMemberships::JOIN) + .await + .map(|m| m.len() as u64) + .unwrap_or(0); let topic = room.topic().map(|t| t.to_string()); let is_encrypted = room.is_encrypted().await.unwrap_or(false); + let unread_notifications = room.unread_notification_counts().notification_count; + let unread_messages = room.num_unread_messages(); result.push(RoomInfo { room_id: room.room_id().to_string(), name, @@ -40,6 +59,8 @@ pub async fn get_joined_rooms( is_encrypted, member_count, topic, + unread_notifications, + unread_messages, }); } @@ -60,11 +81,14 @@ pub async fn create_room( ) -> Result, axum::http::StatusCode> { let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; let s = state.read().await; - let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; let vis = match req.visibility.as_deref() { - Some("public") => matrix_sdk::ruma::Space::Public, - _ => matrix_sdk::ruma::Space::Private, + Some("public") => matrix_sdk::ruma::api::client::room::Visibility::Public, + _ => matrix_sdk::ruma::api::client::room::Visibility::Private, }; let mut request = matrix_sdk::ruma::api::client::room::create_room::v3::Request::new(); @@ -72,8 +96,12 @@ pub async fn create_room( request.topic = req.topic; request.visibility = vis; - let response = session.client.create_room(request).await.map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; - Ok(Json(response.room_id.to_string())) + let response = session + .client + .create_room(request) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + Ok(Json(response.room_id().to_string())) } #[derive(serde::Deserialize)] @@ -88,17 +116,24 @@ pub async fn join_room( ) -> Result, axum::http::StatusCode> { let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; let s = state.read().await; - let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; - let room_id = session.client - .join_room_by_id_or_alias( - &req.room_id_or_alias.try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?, - &[], - ) + let alias: matrix_sdk::ruma::OwnedRoomOrAliasId = req + .room_id_or_alias + .parse() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + + let room = session + .client + .join_room_by_id_or_alias(&alias, &[]) .await .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; - Ok(Json(room_id.to_string())) + ROOMS_JOINED_TOTAL.inc(); + Ok(Json(room.room_id().to_string())) } pub async fn leave_room( @@ -108,11 +143,22 @@ pub async fn leave_room( ) -> Result, axum::http::StatusCode> { let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; let s = state.read().await; - let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; - let rid: matrix_sdk::ruma::OwnedRoomId = room_id.as_str().try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; - let room = session.client.get_room(&rid).ok_or(axum::http::StatusCode::NOT_FOUND)?; - room.leave().await.map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + let rid: matrix_sdk::ruma::OwnedRoomId = room_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + let room = session + .client + .get_room(&rid) + .ok_or(axum::http::StatusCode::NOT_FOUND)?; + room.leave() + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(true)) } @@ -124,11 +170,219 @@ pub async fn get_room_members( ) -> Result>, axum::http::StatusCode> { let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; let s = state.read().await; - let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; - let rid: matrix_sdk::ruma::OwnedRoomId = room_id.as_str().try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; - let room = session.client.get_room(&rid).ok_or(axum::http::StatusCode::NOT_FOUND)?; + let rid: matrix_sdk::ruma::OwnedRoomId = room_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + let room = session + .client + .get_room(&rid) + .ok_or(axum::http::StatusCode::NOT_FOUND)?; - let members = room.joined_members(); - Ok(Json(members.iter().map(|m| m.user_id().to_string()).collect())) -} \ No newline at end of file + let members = room + .members(RoomMemberships::JOIN) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + Ok(Json( + members.iter().map(|m| m.user_id().to_string()).collect(), + )) +} + +#[derive(Deserialize)] +pub struct SetRoomNameRequest { + pub name: String, +} + +pub async fn set_room_name( + State(state): State, + headers: HeaderMap, + Path(room_id): Path, + Json(req): Json, +) -> Result, axum::http::StatusCode> { + let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let s = state.read().await; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + + let rid: matrix_sdk::ruma::OwnedRoomId = room_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + let room = session + .client + .get_room(&rid) + .ok_or(axum::http::StatusCode::NOT_FOUND)?; + + room.set_name(req.name) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(Json(true)) +} + +#[derive(Deserialize)] +pub struct SetRoomTopicRequest { + pub topic: String, +} + +pub async fn set_room_topic( + State(state): State, + headers: HeaderMap, + Path(room_id): Path, + Json(req): Json, +) -> Result, axum::http::StatusCode> { + let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let s = state.read().await; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + + let rid: matrix_sdk::ruma::OwnedRoomId = room_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + let room = session + .client + .get_room(&rid) + .ok_or(axum::http::StatusCode::NOT_FOUND)?; + + room.set_room_topic(&req.topic) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(Json(true)) +} + +pub async fn set_room_avatar( + State(state): State, + headers: HeaderMap, + Path(room_id): Path, + mut multipart: Multipart, +) -> Result, axum::http::StatusCode> { + let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let s = state.read().await; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + + let rid: matrix_sdk::ruma::OwnedRoomId = room_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + let room = session + .client + .get_room(&rid) + .ok_or(axum::http::StatusCode::NOT_FOUND)?; + + let field = multipart + .next_field() + .await + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)? + .ok_or(axum::http::StatusCode::BAD_REQUEST)?; + + let content_type = field + .content_type() + .unwrap_or("application/octet-stream") + .to_string(); + let data = field + .bytes() + .await + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + + let mime_type: mime::Mime = content_type.parse().unwrap_or(mime::IMAGE_PNG); + + let upload = session + .client + .media() + .upload(&mime_type, data.to_vec()) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + + room.set_avatar_url(&upload.content_uri, None) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(Json(true)) +} + +#[derive(Serialize)] +pub struct UnreadInfo { + pub room_id: String, + pub unread_notifications: u64, + pub unread_messages: u64, +} + +pub async fn get_unread_counts( + State(state): State, + headers: HeaderMap, +) -> Result>, axum::http::StatusCode> { + let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let s = state.read().await; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + + let rooms = session.client.joined_rooms(); + let mut result = Vec::new(); + for room in rooms { + let unread_notifications = room.unread_notification_counts().notification_count; + let unread_messages = room.num_unread_messages(); + result.push(UnreadInfo { + room_id: room.room_id().to_string(), + unread_notifications, + unread_messages, + }); + } + + Ok(Json(result)) +} + +#[derive(Deserialize)] +pub struct MarkReadRequest { + pub event_id: String, +} + +pub async fn mark_room_read( + State(state): State, + headers: HeaderMap, + Path(room_id): Path, + Json(req): Json, +) -> Result, axum::http::StatusCode> { + let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let s = state.read().await; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + + let rid: matrix_sdk::ruma::OwnedRoomId = room_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + let room = session + .client + .get_room(&rid) + .ok_or(axum::http::StatusCode::NOT_FOUND)?; + + let event_id: matrix_sdk::ruma::OwnedEventId = req + .event_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + + room.send_single_receipt(ReceiptType::Read, ReceiptThread::Main, event_id) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(Json(true)) +} diff --git a/server/src/routes/threads.rs b/server/src/routes/threads.rs new file mode 100644 index 0000000..2f13e83 --- /dev/null +++ b/server/src/routes/threads.rs @@ -0,0 +1,379 @@ +use super::auth::extract_token; +use crate::ServerState; +use axum::{ + extract::{Path, Query, State}, + http::HeaderMap, + Json, +}; +use matrix_sdk::room::MessagesOptions; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize)] +pub struct ThreadInfo { + pub root_event_id: String, + pub root_sender: String, + pub root_body: String, + pub root_timestamp: u64, + pub reply_count: u32, + pub last_reply_event_id: Option, + pub last_reply_sender: Option, + pub last_reply_body: Option, + pub last_reply_timestamp: Option, +} + +#[derive(Serialize)] +pub struct ThreadMessageInfo { + pub event_id: String, + pub sender: String, + pub body: String, + pub timestamp: u64, +} + +#[derive(Deserialize)] +pub struct ThreadQuery { + pub limit: Option, + pub from: Option, +} + +pub async fn get_threads( + State(state): State, + headers: HeaderMap, + Path(room_id): Path, +) -> Result>, axum::http::StatusCode> { + let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let s = state.read().await; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + + let rid: matrix_sdk::ruma::OwnedRoomId = room_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + let room = session + .client + .get_room(&rid) + .ok_or(axum::http::StatusCode::NOT_FOUND)?; + + let mut options = MessagesOptions::backward(); + options.limit = matrix_sdk::ruma::uint!(100); + + let messages = room + .messages(options) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + + let mut thread_roots: std::collections::HashMap = + std::collections::HashMap::new(); + + for msg in &messages.chunk { + let event_value: serde_json::Value = match msg.event.deserialize_as() { + Ok(v) => v, + Err(_) => continue, + }; + + let event_type = event_value + .get("type") + .and_then(|v| v.as_str()) + .unwrap_or(""); + if event_type != "m.room.message" { + continue; + } + + let event_id = match event_value.get("event_id").and_then(|v| v.as_str()) { + Some(id) => id.to_string(), + None => continue, + }; + let sender = event_value + .get("sender") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let timestamp = event_value + .get("origin_server_ts") + .and_then(|v| v.as_u64()) + .unwrap_or(0); + let content = event_value + .get("content") + .unwrap_or(&serde_json::Value::Null); + let body = content + .get("body") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + + let new_content = content.get("m.new_content"); + let display_body = if let Some(nc) = new_content { + nc.get("body") + .and_then(|v| v.as_str()) + .unwrap_or(&body) + .to_string() + } else { + body.clone() + }; + + let relates_to = content.get("m.relates_to"); + + let is_thread = relates_to + .and_then(|r| r.get("rel_type")) + .and_then(|v| v.as_str()) + == Some("m.thread"); + + if is_thread { + if let Some(thread_info) = relates_to { + let root_id = thread_info + .get("event_id") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + if root_id.is_empty() { + continue; + } + + let is_falling_back = thread_info + .get("is_falling_back") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + if is_falling_back { + let reply_count = thread_roots + .get(&root_id) + .map(|t: &ThreadInfo| t.reply_count) + .unwrap_or(0); + thread_roots + .entry(root_id.clone()) + .or_insert_with(|| ThreadInfo { + root_event_id: root_id.clone(), + root_sender: sender.clone(), + root_body: display_body.clone(), + root_timestamp: timestamp, + reply_count, + last_reply_event_id: None, + last_reply_sender: None, + last_reply_body: None, + last_reply_timestamp: None, + }); + } else { + let entry = thread_roots + .entry(root_id.clone()) + .or_insert_with(|| ThreadInfo { + root_event_id: root_id.clone(), + root_sender: String::new(), + root_body: String::new(), + root_timestamp: 0, + reply_count: 0, + last_reply_event_id: None, + last_reply_sender: None, + last_reply_body: None, + last_reply_timestamp: None, + }); + entry.reply_count += 1; + entry.last_reply_event_id = Some(event_id); + entry.last_reply_sender = Some(sender.clone()); + entry.last_reply_body = Some(display_body); + entry.last_reply_timestamp = Some(timestamp); + } + } + } + } + + Ok(Json(thread_roots.into_values().collect())) +} + +pub async fn get_thread_messages( + State(state): State, + headers: HeaderMap, + Path((room_id, thread_id)): Path<(String, String)>, + Query(query): Query, +) -> Result>, axum::http::StatusCode> { + let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let s = state.read().await; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + + let rid: matrix_sdk::ruma::OwnedRoomId = room_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + let room = session + .client + .get_room(&rid) + .ok_or(axum::http::StatusCode::NOT_FOUND)?; + + let limit = query.limit.unwrap_or(50); + + let mut options = MessagesOptions::backward(); + options.limit = matrix_sdk::ruma::uint!(100); + + let messages = room + .messages(options) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + + let mut result = Vec::new(); + + for msg in &messages.chunk { + let event_value: serde_json::Value = match msg.event.deserialize_as() { + Ok(v) => v, + Err(_) => continue, + }; + + let event_type = event_value + .get("type") + .and_then(|v| v.as_str()) + .unwrap_or(""); + if event_type != "m.room.message" { + continue; + } + + let content = event_value + .get("content") + .unwrap_or(&serde_json::Value::Null); + let relates_to = content.get("m.relates_to"); + + let is_in_thread = relates_to + .and_then(|r| { + let rel_type = r.get("rel_type").and_then(|v| v.as_str()); + let event_id = r.get("event_id").and_then(|v| v.as_str()); + if rel_type == Some("m.thread") && event_id == Some(thread_id.as_str()) { + Some(true) + } else { + None + } + }) + .unwrap_or(false); + + let is_root = + event_value.get("event_id").and_then(|v| v.as_str()) == Some(thread_id.as_str()); + + if !is_in_thread && !is_root { + continue; + } + + let event_id = match event_value.get("event_id").and_then(|v| v.as_str()) { + Some(id) => id.to_string(), + None => continue, + }; + let sender = event_value + .get("sender") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let timestamp = event_value + .get("origin_server_ts") + .and_then(|v| v.as_u64()) + .unwrap_or(0); + let body = content + .get("body") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + + if body.is_empty() { + continue; + } + + result.push(ThreadMessageInfo { + event_id, + sender, + body, + timestamp, + }); + } + + result.truncate(limit as usize); + Ok(Json(result)) +} + +#[derive(Deserialize)] +pub struct ThreadReplyRequest { + pub message: String, +} + +pub async fn send_thread_reply( + State(state): State, + headers: HeaderMap, + Path((room_id, thread_id)): Path<(String, String)>, + Json(req): Json, +) -> Result, axum::http::StatusCode> { + let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let s = state.read().await; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + + let rid: matrix_sdk::ruma::OwnedRoomId = room_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + let room = session + .client + .get_room(&rid) + .ok_or(axum::http::StatusCode::NOT_FOUND)?; + + let root_event_id: matrix_sdk::ruma::OwnedEventId = thread_id + .parse() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + + let mut content = + matrix_sdk::ruma::events::room::message::RoomMessageEventContent::text_plain(&req.message); + content.relates_to = Some(matrix_sdk::ruma::events::room::message::Relation::Thread( + matrix_sdk::ruma::events::relation::Thread::without_fallback(root_event_id), + )); + + let response = room + .send(content) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + Ok(Json(response.event_id.to_string())) +} + +#[derive(Deserialize)] +pub struct SendReplyRequest { + pub message: String, + pub reply_to: String, +} + +pub async fn send_reply( + State(state): State, + headers: HeaderMap, + Path(room_id): Path, + Json(req): Json, +) -> Result, axum::http::StatusCode> { + let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let s = state.read().await; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + + let rid: matrix_sdk::ruma::OwnedRoomId = room_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + let room = session + .client + .get_room(&rid) + .ok_or(axum::http::StatusCode::NOT_FOUND)?; + + let reply_to_event_id: matrix_sdk::ruma::OwnedEventId = req + .reply_to + .parse() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + + let mut content = + matrix_sdk::ruma::events::room::message::RoomMessageEventContent::text_plain(&req.message); + content.relates_to = Some(matrix_sdk::ruma::events::room::message::Relation::Reply { + in_reply_to: matrix_sdk::ruma::events::relation::InReplyTo::new(reply_to_event_id), + }); + + let response = room + .send(content) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + Ok(Json(response.event_id.to_string())) +} diff --git a/server/src/routes/upload.rs b/server/src/routes/upload.rs new file mode 100644 index 0000000..bc0e3f7 --- /dev/null +++ b/server/src/routes/upload.rs @@ -0,0 +1,97 @@ +use super::auth::extract_token; +use crate::routes::metrics::UPLOADS_TOTAL; +use crate::ServerState; +use axum::{ + extract::{Multipart, Path, State}, + http::HeaderMap, + Json, +}; +use matrix_sdk::ruma::events::room::message::{ + FileMessageEventContent, ImageMessageEventContent, MessageType, RoomMessageEventContent, +}; +use matrix_sdk::ruma::OwnedMxcUri; +use serde::Serialize; + +#[derive(Serialize)] +pub struct UploadResult { + pub event_id: String, + pub media_url: Option, + pub filename: String, + pub mimetype: Option, + pub size: Option, +} + +pub async fn upload_file( + State(state): State, + headers: HeaderMap, + Path(room_id): Path, + mut multipart: Multipart, +) -> Result, axum::http::StatusCode> { + let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let s = state.read().await; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + + let rid: matrix_sdk::ruma::OwnedRoomId = room_id + .as_str() + .try_into() + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + let room = session + .client + .get_room(&rid) + .ok_or(axum::http::StatusCode::NOT_FOUND)?; + + let field = multipart + .next_field() + .await + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)? + .ok_or(axum::http::StatusCode::BAD_REQUEST)?; + + let filename = field.file_name().unwrap_or("upload").to_string(); + let content_type = field + .content_type() + .unwrap_or("application/octet-stream") + .to_string(); + let data = field + .bytes() + .await + .map_err(|_| axum::http::StatusCode::BAD_REQUEST)?; + + let mime_type: mime::Mime = content_type + .parse() + .unwrap_or(mime::APPLICATION_OCTET_STREAM); + + let media_response = session + .client + .media() + .upload(&mime_type, data.to_vec()) + .await + .map_err(|e| { + tracing::error!("Media upload failed: {}", e); + axum::http::StatusCode::INTERNAL_SERVER_ERROR + })?; + + let media_uri: OwnedMxcUri = media_response.content_uri.clone(); + + let content: RoomMessageEventContent = if mime_type.type_() == mime::IMAGE { + MessageType::Image(ImageMessageEventContent::plain(filename.clone(), media_uri)).into() + } else { + MessageType::File(FileMessageEventContent::plain(filename.clone(), media_uri)).into() + }; + + let response = room + .send(content) + .await + .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?; + + UPLOADS_TOTAL.inc(); + Ok(Json(UploadResult { + event_id: response.event_id.to_string(), + media_url: Some(media_response.content_uri.to_string()), + filename, + mimetype: Some(content_type), + size: Some(data.len() as u64), + })) +} diff --git a/server/src/routes/voice.rs b/server/src/routes/voice.rs index 60c562c..1412b04 100644 --- a/server/src/routes/voice.rs +++ b/server/src/routes/voice.rs @@ -1,80 +1,318 @@ -use axum::{ - extract::State, - http::HeaderMap, - Json, -}; -use serde::{Deserialize, Serialize}; -use crate::ServerState; use super::auth::extract_token; +use crate::state::WsEvent; +use crate::ServerState; +use axum::{extract::State, http::HeaderMap, Json}; +use livekit_api::access_token::{AccessToken, VideoGrants}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; #[derive(Deserialize)] -pub struct VoiceRequest { +pub struct VoiceJoinRequest { pub room_id: String, } #[derive(Serialize)] -pub struct VoiceStateInfo { +pub struct VoiceJoinResponse { pub room_id: String, + pub livekit_url: String, + pub livekit_token: String, +} + +#[derive(Serialize)] +pub struct VoiceToggleResponse { pub muted: bool, pub deafened: bool, - pub streaming: bool, +} + +#[derive(Serialize)] +pub struct VoiceParticipantInfo { + pub user_id: String, + pub muted: bool, + pub deafened: bool, +} + +fn sanitize_room_id(room_id: &str) -> String { + room_id + .replace(':', "-") + .replace("!", "") + .replace("/", "-") + .replace(" ", "_") } pub async fn join_voice_channel( State(state): State, headers: HeaderMap, - Json(req): Json, -) -> Result, axum::http::StatusCode> { + Json(req): Json, +) -> Result, axum::http::StatusCode> { let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; - let mut s = state.write().await; - let session = s.sessions.get_mut(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; - session.voice_manager.join_channel(req.room_id.clone(), session.user_id.clone()); + let (user_id, livekit_url, livekit_api_key, livekit_api_secret) = { + let s = state.read().await; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + ( + session.user_id.clone(), + s.livekit.url.clone(), + s.livekit.api_key.clone(), + s.livekit.api_secret.clone(), + ) + }; - Ok(Json(VoiceStateInfo { + let lk_room = sanitize_room_id(&req.room_id); + let lk_identity = sanitize_room_id(&user_id); + + let grants = VideoGrants { + room_join: true, + room: lk_room.clone(), + can_publish: true, + can_subscribe: true, + can_publish_data: true, + ..Default::default() + }; + + let lk_token = AccessToken::with_api_key(&livekit_api_key, &livekit_api_secret) + .with_identity(&lk_identity) + .with_name(&user_id) + .with_grants(grants) + .to_jwt() + .map_err(|e| { + tracing::error!("Failed to generate LiveKit token: {}", e); + axum::http::StatusCode::INTERNAL_SERVER_ERROR + })?; + + let old_channel; + { + let mut s = state.write().await; + let session = s + .sessions + .get_mut(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + old_channel = session.voice_manager.active_channel.clone(); + session.voice_manager.join_channel(&req.room_id); + } + + if let Some(ref old) = old_channel { + if old != &req.room_id { + let mut s = state.write().await; + if let Some(room) = s.voice_rooms.rooms.get_mut(old) { + room.participants.remove(&user_id); + if room.participants.is_empty() { + s.voice_rooms.rooms.remove(old); + } + } + drop(s); + + let s = state.read().await; + if let Some(session) = s.sessions.get(&token) { + let _ = session.event_sender.send(WsEvent::VoiceUserLeft { + room_id: old.clone(), + user_id: user_id.clone(), + }); + } + } + } + + { + let mut s = state.write().await; + let room_entry = s + .voice_rooms + .rooms + .entry(req.room_id.clone()) + .or_insert_with(|| crate::state::VoiceRoom { + participants: HashMap::new(), + }); + room_entry.participants.insert( + user_id.clone(), + crate::state::VoiceParticipant { + user_id: user_id.clone(), + muted: false, + deafened: false, + }, + ); + } + + { + let s = state.read().await; + if let Some(session) = s.sessions.get(&token) { + let _ = session.event_sender.send(WsEvent::VoiceUserJoined { + room_id: req.room_id.clone(), + user_id: user_id.clone(), + }); + } + } + + Ok(Json(VoiceJoinResponse { room_id: req.room_id, - muted: false, - deafened: false, - streaming: false, + livekit_url, + livekit_token: lk_token, })) } pub async fn leave_voice_channel( State(state): State, headers: HeaderMap, - Json(req): Json, + Json(req): Json, ) -> Result, axum::http::StatusCode> { let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; - let mut s = state.write().await; - let session = s.sessions.get_mut(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; - Ok(Json(session.voice_manager.leave_channel(&req.room_id, &session.user_id))) + let user_id; + { + let mut s = state.write().await; + let session = s + .sessions + .get_mut(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + user_id = session.user_id.clone(); + session.voice_manager.leave_channel(); + + if let Some(room) = s.voice_rooms.rooms.get_mut(&req.room_id) { + room.participants.remove(&user_id); + if room.participants.is_empty() { + s.voice_rooms.rooms.remove(&req.room_id); + } + } + } + + { + let s = state.read().await; + if let Some(session) = s.sessions.get(&token) { + let _ = session.event_sender.send(WsEvent::VoiceUserLeft { + room_id: req.room_id, + user_id, + }); + } + } + + Ok(Json(true)) } pub async fn toggle_mute( State(state): State, headers: HeaderMap, - Json(req): Json, -) -> Result, axum::http::StatusCode> { + Json(_req): Json, +) -> Result, axum::http::StatusCode> { let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; - let mut s = state.write().await; - let session = s.sessions.get_mut(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; - session.voice_manager.toggle_mute(&req.room_id, &session.user_id) - .ok_or(axum::http::StatusCode::BAD_REQUEST) - .map(Json) + let (user_id, room_id, new_muted, deafened); + { + let mut s = state.write().await; + let session = s + .sessions + .get_mut(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + new_muted = session.voice_manager.toggle_mute(); + deafened = session.voice_manager.deafened; + room_id = session + .voice_manager + .active_channel + .clone() + .ok_or(axum::http::StatusCode::BAD_REQUEST)?; + user_id = session.user_id.clone(); + + if let Some(room) = s.voice_rooms.rooms.get_mut(&room_id) { + if let Some(participant) = room.participants.get_mut(&user_id) { + participant.muted = new_muted; + } + } + } + + { + let s = state.read().await; + if let Some(session) = s.sessions.get(&token) { + let _ = session.event_sender.send(WsEvent::VoiceStateUpdate { + room_id, + user_id, + muted: new_muted, + deafened, + }); + } + } + + Ok(Json(VoiceToggleResponse { + muted: new_muted, + deafened, + })) } pub async fn toggle_deafen( State(state): State, headers: HeaderMap, - Json(req): Json, -) -> Result, axum::http::StatusCode> { + Json(_req): Json, +) -> Result, axum::http::StatusCode> { let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; - let mut s = state.write().await; - let session = s.sessions.get_mut(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; - session.voice_manager.toggle_deafen(&req.room_id, &session.user_id) - .ok_or(axum::http::StatusCode::BAD_REQUEST) - .map(Json) -} \ No newline at end of file + let (user_id, room_id, muted, new_deafened); + { + let mut s = state.write().await; + let session = s + .sessions + .get_mut(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + new_deafened = session.voice_manager.toggle_deafen(); + muted = session.voice_manager.muted; + room_id = session + .voice_manager + .active_channel + .clone() + .ok_or(axum::http::StatusCode::BAD_REQUEST)?; + user_id = session.user_id.clone(); + + if let Some(room) = s.voice_rooms.rooms.get_mut(&room_id) { + if let Some(participant) = room.participants.get_mut(&user_id) { + participant.deafened = new_deafened; + participant.muted = muted; + } + } + } + + { + let s = state.read().await; + if let Some(session) = s.sessions.get(&token) { + let _ = session.event_sender.send(WsEvent::VoiceStateUpdate { + room_id, + user_id, + muted, + deafened: new_deafened, + }); + } + } + + Ok(Json(VoiceToggleResponse { + muted, + deafened: new_deafened, + })) +} + +pub async fn get_voice_participants( + State(state): State, + headers: HeaderMap, +) -> Result>, axum::http::StatusCode> { + let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + let s = state.read().await; + let session = s + .sessions + .get(&token) + .ok_or(axum::http::StatusCode::UNAUTHORIZED)?; + + let room_id = match &session.voice_manager.active_channel { + Some(r) => r.clone(), + None => return Ok(Json(Vec::new())), + }; + + let participants = match s.voice_rooms.rooms.get(&room_id) { + Some(room) => room + .participants + .values() + .map(|p| VoiceParticipantInfo { + user_id: p.user_id.clone(), + muted: p.muted, + deafened: p.deafened, + }) + .collect(), + None => Vec::new(), + }; + + Ok(Json(participants)) +} diff --git a/server/src/routes/ws.rs b/server/src/routes/ws.rs new file mode 100644 index 0000000..c9d2622 --- /dev/null +++ b/server/src/routes/ws.rs @@ -0,0 +1,84 @@ +use crate::ServerState; +use axum::{ + extract::{ + ws::{Message, WebSocket}, + Query, State, WebSocketUpgrade, + }, + response::IntoResponse, +}; +use futures::{SinkExt, StreamExt}; +use serde::Deserialize; + +#[derive(Deserialize)] +pub struct WsQuery { + pub token: Option, +} + +pub async fn ws_handler( + ws: WebSocketUpgrade, + Query(query): Query, + State(state): State, +) -> impl IntoResponse { + ws.on_upgrade(move |socket| handle_ws(socket, state, query.token.unwrap_or_default())) +} + +async fn handle_ws(socket: WebSocket, state: ServerState, token: String) { + let (user_id, mut event_rx) = { + let s = state.read().await; + match s.sessions.get(&token) { + Some(session) => (session.user_id.clone(), session.event_sender.subscribe()), + None => { + let _ = socket.close().await; + return; + } + } + }; + + let connected_msg = serde_json::json!({"type": "connected", "user_id": user_id}).to_string(); + let (mut ws_sender, mut ws_receiver) = socket.split(); + + if ws_sender.send(Message::Text(connected_msg)).await.is_err() { + return; + } + + let (out_tx, mut out_rx) = tokio::sync::mpsc::channel::(64); + + let sender_task = tokio::spawn(async move { + while let Some(text) = out_rx.recv().await { + if ws_sender.send(Message::Text(text)).await.is_err() { + break; + } + } + }); + + let receiver_task = tokio::spawn(async move { + while let Some(msg) = ws_receiver.next().await { + match msg { + Ok(Message::Text(text)) if text.as_str() == "ping" => {} + Ok(Message::Close(_)) | Err(_) => break, + _ => {} + } + } + }); + + let forward_task = tokio::spawn(async move { + while let Ok(event) = event_rx.recv().await { + match serde_json::to_string(&event) { + Ok(json) => { + if out_tx.send(json).await.is_err() { + break; + } + } + Err(e) => { + tracing::error!("Failed to serialize WS event: {}", e); + } + } + } + }); + + tokio::select! { + _ = sender_task => {}, + _ = receiver_task => {}, + _ = forward_task => {}, + } +} diff --git a/server/src/session_store.rs b/server/src/session_store.rs new file mode 100644 index 0000000..1ad97b0 --- /dev/null +++ b/server/src/session_store.rs @@ -0,0 +1,181 @@ +use rusqlite::Connection; +use std::path::Path; +use std::sync::Arc; +use tokio::sync::Mutex; + +pub struct SessionStore { + conn: Arc>, +} + +#[derive(Clone)] +pub struct StoredSession { + pub token: String, + pub user_id: String, + pub homeserver: String, + pub access_token: String, + pub device_id: Option, + pub refresh_token: Option, +} + +impl SessionStore { + pub fn new(path: &str) -> Result { + let conn = if Path::new(path).exists() { + Connection::open(path)? + } else { + let conn = Connection::open(path)?; + conn.execute_batch( + "CREATE TABLE IF NOT EXISTS sessions ( + token TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + homeserver TEXT NOT NULL, + access_token TEXT NOT NULL, + device_id TEXT, + refresh_token TEXT + );", + )?; + conn + }; + Ok(Self { + conn: Arc::new(Mutex::new(conn)), + }) + } + + pub async fn save_session(&self, session: &StoredSession) -> Result<(), rusqlite::Error> { + let conn = self.conn.lock().await; + conn.execute( + "INSERT OR REPLACE INTO sessions (token, user_id, homeserver, access_token, device_id, refresh_token) + VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + rusqlite::params![ + session.token, + session.user_id, + session.homeserver, + session.access_token, + session.device_id, + session.refresh_token, + ], + )?; + Ok(()) + } + + pub async fn get_all_sessions(&self) -> Result, rusqlite::Error> { + let conn = self.conn.lock().await; + let mut stmt = conn.prepare( + "SELECT token, user_id, homeserver, access_token, device_id, refresh_token FROM sessions" + )?; + let rows = stmt.query_map([], |row| { + Ok(StoredSession { + token: row.get(0)?, + user_id: row.get(1)?, + homeserver: row.get(2)?, + access_token: row.get(3)?, + device_id: row.get(4)?, + refresh_token: row.get(5)?, + }) + })?; + rows.collect() + } + + pub async fn delete_session(&self, token: &str) -> Result<(), rusqlite::Error> { + let conn = self.conn.lock().await; + conn.execute("DELETE FROM sessions WHERE token = ?1", [token])?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + async fn create_test_store() -> (SessionStore, tempfile::TempDir) { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("test.db"); + let path_str = path.to_str().unwrap().to_string(); + let store = SessionStore::new(&path_str).unwrap(); + (store, dir) + } + + #[tokio::test] + async fn save_and_retrieve_session() { + let (store, _dir) = create_test_store().await; + let session = StoredSession { + token: "test-token".to_string(), + user_id: "@alice:server".to_string(), + homeserver: "https://matrix.server".to_string(), + access_token: "access-123".to_string(), + device_id: Some("DEVICE1".to_string()), + refresh_token: Some("refresh-456".to_string()), + }; + + store.save_session(&session).await.unwrap(); + let sessions = store.get_all_sessions().await.unwrap(); + assert_eq!(sessions.len(), 1); + assert_eq!(sessions[0].token, "test-token"); + assert_eq!(sessions[0].user_id, "@alice:server"); + assert_eq!(sessions[0].access_token, "access-123"); + assert_eq!(sessions[0].device_id, Some("DEVICE1".to_string())); + } + + #[tokio::test] + async fn save_session_upserts() { + let (store, _dir) = create_test_store().await; + let session = StoredSession { + token: "test-token".to_string(), + user_id: "@alice:server".to_string(), + homeserver: "https://matrix.server".to_string(), + access_token: "access-123".to_string(), + device_id: None, + refresh_token: None, + }; + + store.save_session(&session).await.unwrap(); + let mut updated = session.clone(); + updated.access_token = "access-456".to_string(); + store.save_session(&updated).await.unwrap(); + + let sessions = store.get_all_sessions().await.unwrap(); + assert_eq!(sessions.len(), 1); + assert_eq!(sessions[0].access_token, "access-456"); + } + + #[tokio::test] + async fn delete_session_removes_it() { + let (store, _dir) = create_test_store().await; + let session = StoredSession { + token: "test-token".to_string(), + user_id: "@alice:server".to_string(), + homeserver: "https://matrix.server".to_string(), + access_token: "access-123".to_string(), + device_id: None, + refresh_token: None, + }; + + store.save_session(&session).await.unwrap(); + store.delete_session("test-token").await.unwrap(); + let sessions = store.get_all_sessions().await.unwrap(); + assert!(sessions.is_empty()); + } + + #[tokio::test] + async fn delete_nonexistent_session_is_ok() { + let (store, _dir) = create_test_store().await; + store.delete_session("nonexistent").await.unwrap(); + } + + #[tokio::test] + async fn save_multiple_sessions() { + let (store, _dir) = create_test_store().await; + for i in 0..3 { + let session = StoredSession { + token: format!("token-{}", i), + user_id: format!("@user{}:server", i), + homeserver: "https://matrix.server".to_string(), + access_token: format!("access-{}", i), + device_id: None, + refresh_token: None, + }; + store.save_session(&session).await.unwrap(); + } + let sessions = store.get_all_sessions().await.unwrap(); + assert_eq!(sessions.len(), 3); + } +} diff --git a/server/src/state.rs b/server/src/state.rs index c0505ae..69ec41d 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -1,117 +1,381 @@ -use std::sync::Arc; -use tokio::sync::RwLock; +use crate::session_store::SessionStore; use matrix_sdk::Client; use std::collections::HashMap; +use std::sync::Arc; +use std::time::Instant; +use tokio::sync::broadcast; +use tokio::sync::RwLock; + +#[derive(Clone, Debug, serde::Serialize)] +#[serde(tag = "type")] +pub enum WsEvent { + #[serde(rename = "message")] + Message { + room_id: String, + event_id: String, + sender: String, + body: String, + timestamp: u64, + reply_to: Option, + msgtype: Option, + media_url: Option, + filename: Option, + mimetype: Option, + }, + #[serde(rename = "message_edited")] + MessageEdited { + room_id: String, + event_id: String, + new_body: String, + }, + #[serde(rename = "message_deleted")] + MessageDeleted { room_id: String, redacts: String }, + #[serde(rename = "reaction")] + Reaction { + room_id: String, + event_id: String, + key: String, + sender: String, + }, + #[serde(rename = "room_joined")] + RoomJoined { room_id: String, name: String }, + #[serde(rename = "room_left")] + RoomLeft { room_id: String }, + #[serde(rename = "presence")] + Presence { + user_id: String, + status: String, + status_msg: Option, + }, + #[serde(rename = "typing")] + Typing { + room_id: String, + user_id: String, + typing: bool, + }, + #[serde(rename = "voice_state_update")] + VoiceStateUpdate { + room_id: String, + user_id: String, + muted: bool, + deafened: bool, + }, + #[serde(rename = "voice_user_joined")] + VoiceUserJoined { room_id: String, user_id: String }, + #[serde(rename = "voice_user_left")] + VoiceUserLeft { room_id: String, user_id: String }, + #[serde(rename = "thread_reply")] + ThreadReply { + room_id: String, + root_event_id: String, + event_id: String, + sender: String, + body: String, + timestamp: u64, + }, +} + +pub struct LiveKitConfig { + pub api_key: String, + pub api_secret: String, + pub url: String, +} pub struct Session { pub client: Client, pub user_id: String, + pub homeserver: String, pub voice_manager: VoiceManager, + pub event_sender: broadcast::Sender, + pub sync_handle: Option>, + pub created_at: Instant, + pub expires_at: Option, } +impl Session { + pub fn new(client: Client, user_id: String, homeserver: String) -> Self { + let (sender, _) = broadcast::channel(256); + Self { + client, + user_id, + homeserver, + voice_manager: VoiceManager::new(), + event_sender: sender, + sync_handle: None, + created_at: Instant::now(), + expires_at: None, + } + } + + pub fn with_ttl(mut self, ttl: std::time::Duration) -> Self { + self.expires_at = Some(self.created_at + ttl); + self + } + + pub fn is_expired(&self) -> bool { + self.expires_at + .map(|expires| Instant::now() > expires) + .unwrap_or(false) + } +} + +#[derive(Default)] pub struct VoiceManager { - channels: HashMap, - active_channel: Option, -} - -pub struct VoiceChannel { - pub room_id: String, - pub participants: Vec, -} - -pub struct VoiceParticipant { - pub user_id: String, + pub active_channel: Option, pub muted: bool, pub deafened: bool, - pub streaming: bool, } impl VoiceManager { pub fn new() -> Self { Self { - channels: HashMap::new(), active_channel: None, - } - } - - pub fn join_channel(&mut self, room_id: String, user_id: String) -> bool { - if let Some(ref old_channel) = self.active_channel { - self.leave_channel_internal(old_channel, &user_id); - } - let channel = self.channels.entry(room_id.clone()).or_insert_with(|| VoiceChannel { - room_id: room_id.clone(), - participants: Vec::new(), - }); - if channel.participants.iter().any(|p| p.user_id == user_id) { - return false; - } - channel.participants.push(VoiceParticipant { - user_id, muted: false, deafened: false, - streaming: false, - }); - self.active_channel = Some(room_id); - true - } - - fn leave_channel_internal(&mut self, room_id: &str, user_id: &str) { - if let Some(channel) = self.channels.get_mut(room_id) { - channel.participants.retain(|p| p.user_id != user_id); - if channel.participants.is_empty() { - self.channels.remove(room_id); - } } } - pub fn leave_channel(&mut self, room_id: &str, user_id: &str) -> bool { - self.leave_channel_internal(room_id, user_id); - if self.active_channel.as_deref() == Some(room_id) { - self.active_channel = None; - } - true + pub fn join_channel(&mut self, room_id: &str) { + self.active_channel = Some(room_id.to_string()); + self.muted = false; + self.deafened = false; } - pub fn toggle_mute(&mut self, room_id: &str, user_id: &str) -> Option { - if let Some(channel) = self.channels.get_mut(room_id) { - if let Some(participant) = channel.participants.iter_mut().find(|p| p.user_id == user_id) { - participant.muted = !participant.muted; - return Some(participant.muted); - } - } - None + pub fn leave_channel(&mut self) { + self.active_channel = None; + self.muted = false; + self.deafened = false; } - pub fn toggle_deafen(&mut self, room_id: &str, user_id: &str) -> Option { - if let Some(channel) = self.channels.get_mut(room_id) { - if let Some(participant) = channel.participants.iter_mut().find(|p| p.user_id == user_id) { - participant.deafened = !participant.deafened; - if participant.deafened { - participant.muted = true; - } - return Some(participant.deafened); - } + pub fn toggle_mute(&mut self) -> bool { + self.muted = !self.muted; + self.muted + } + + pub fn toggle_deafen(&mut self) -> bool { + self.deafened = !self.deafened; + if self.deafened { + self.muted = true; } - None + self.deafened + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn voice_manager_new_is_idle() { + let vm = VoiceManager::new(); + assert!(vm.active_channel.is_none()); + assert!(!vm.muted); + assert!(!vm.deafened); + } + + #[test] + fn voice_manager_default_is_idle() { + let vm = VoiceManager::default(); + assert!(vm.active_channel.is_none()); + assert!(!vm.muted); + assert!(!vm.deafened); + } + + #[test] + fn join_channel_sets_active() { + let mut vm = VoiceManager::new(); + vm.join_channel("!room:server"); + assert_eq!(vm.active_channel, Some("!room:server".to_string())); + assert!(!vm.muted); + assert!(!vm.deafened); + } + + #[test] + fn join_channel_resets_mute_deafen() { + let mut vm = VoiceManager::new(); + vm.muted = true; + vm.deafened = true; + vm.join_channel("!room:server"); + assert!(!vm.muted); + assert!(!vm.deafened); + } + + #[test] + fn leave_channel_clears_state() { + let mut vm = VoiceManager::new(); + vm.join_channel("!room:server"); + vm.muted = true; + vm.leave_channel(); + assert!(vm.active_channel.is_none()); + assert!(!vm.muted); + assert!(!vm.deafened); + } + + #[test] + fn toggle_mute_flips_state() { + let mut vm = VoiceManager::new(); + assert!(!vm.muted); + let result = vm.toggle_mute(); + assert!(result); + assert!(vm.muted); + let result = vm.toggle_mute(); + assert!(!result); + assert!(!vm.muted); + } + + #[test] + fn toggle_deafen_sets_muted_when_deafened() { + let mut vm = VoiceManager::new(); + let result = vm.toggle_deafen(); + assert!(result); + assert!(vm.deafened); + assert!(vm.muted); + } + + #[test] + fn toggle_deafen_off_keeps_muted() { + let mut vm = VoiceManager::new(); + vm.muted = true; + vm.toggle_deafen(); + assert!(vm.muted); + vm.toggle_deafen(); + assert!(vm.deafened == false); + assert!(vm.muted); + } + + #[test] + fn ws_event_serialization() { + let event = WsEvent::Message { + room_id: "!room:server".to_string(), + event_id: "$event".to_string(), + sender: "@user:server".to_string(), + body: "hello".to_string(), + timestamp: 123456, + reply_to: None, + msgtype: Some("m.text".to_string()), + media_url: None, + filename: None, + mimetype: None, + }; + let json = serde_json::to_string(&event).unwrap(); + assert!(json.contains("\"type\":\"message\"")); + assert!(json.contains("\"body\":\"hello\"")); + } + + #[test] + fn ws_event_presence_serialization() { + let event = WsEvent::Presence { + user_id: "@alice:server".to_string(), + status: "online".to_string(), + status_msg: Some("working".to_string()), + }; + let json = serde_json::to_string(&event).unwrap(); + assert!(json.contains("\"type\":\"presence\"")); + assert!(json.contains("\"status\":\"online\"")); + } + + #[test] + fn is_expired_true_when_past() { + let created = Instant::now() - std::time::Duration::from_secs(120); + let expires = created + std::time::Duration::from_secs(60); + assert!(Instant::now() > expires); + } + + #[test] + fn is_expired_none_means_not_expired() { + let expires_at: Option = None; + let expired = expires_at + .map(|expires| Instant::now() > expires) + .unwrap_or(false); + assert!(!expired); + } + + #[test] + fn is_expired_future_means_not_expired() { + let expires_at: Option = + Some(Instant::now() + std::time::Duration::from_secs(3600)); + let expired = expires_at + .map(|expires| Instant::now() > expires) + .unwrap_or(false); + assert!(!expired); + } + + #[test] + fn is_expired_past_means_expired() { + let expires_at: Option = Some(Instant::now() - std::time::Duration::from_secs(1)); + let expired = expires_at + .map(|expires| Instant::now() > expires) + .unwrap_or(false); + assert!(expired); } } pub struct ServerStateInner { pub sessions: HashMap, + pub session_store: Arc, + pub livekit: LiveKitConfig, + pub voice_rooms: VoiceRooms, + pub session_ttl: Option, +} + +pub struct VoiceRooms { + pub rooms: HashMap, +} + +pub struct VoiceRoom { + pub participants: HashMap, +} + +#[derive(Clone, serde::Serialize)] +pub struct VoiceParticipant { + pub user_id: String, + pub muted: bool, + pub deafened: bool, } impl ServerStateInner { - pub fn new() -> Self { + pub fn new( + session_store: Arc, + livekit: LiveKitConfig, + session_ttl: Option, + ) -> Self { Self { sessions: HashMap::new(), + session_store, + livekit, + voice_rooms: VoiceRooms { + rooms: HashMap::new(), + }, + session_ttl, } } } -pub type ServerState = Arc>; +#[derive(Clone)] +pub struct ServerState { + inner: Arc>, +} impl ServerState { - pub fn new() -> Self { - Arc::new(RwLock::new(ServerStateInner::new())) + pub fn new( + session_store: Arc, + livekit: LiveKitConfig, + session_ttl: Option, + ) -> Self { + Self { + inner: Arc::new(RwLock::new(ServerStateInner::new( + session_store, + livekit, + session_ttl, + ))), + } } -} \ No newline at end of file + + pub async fn read(&self) -> tokio::sync::RwLockReadGuard<'_, ServerStateInner> { + self.inner.read().await + } + + pub async fn write(&self) -> tokio::sync::RwLockWriteGuard<'_, ServerStateInner> { + self.inner.write().await + } +}