diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..1a12f45 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,91 @@ +{ + "permissions": { + "allow": [ + "Bash(ssh:*)", + "Bash(bun run lint)", + "Bash(bun run prisma:generate:*)", + "Bash(bun run build:bun:*)", + "WebSearch", + "Bash(bun add:*)", + "Bash(bun run tauri:*)", + "Bash(curl:*)", + "Bash(dir \"D:\\Projetos IA\\sistema-de-chamados\")", + "Bash(findstr:*)", + "Bash(cat:*)", + "Bash(chmod:*)", + "Bash(find:*)", + "Bash(grep:*)", + "WebFetch(domain:medium.com)", + "WebFetch(domain:henrywithu.com)", + "WebFetch(domain:hub.docker.com)", + "Bash(python3:*)", + "WebFetch(domain:www.npmjs.com)", + "WebFetch(domain:docs.strapi.io)", + "Bash(tablename)", + "Bash(\"\"\" OWNER TO renan; FROM pg_tables WHERE schemaname = public;\"\" | docker exec -i c95ebc27eb82 psql -U sistema -d strapi_blog\")", + "Bash(sequence_name)", + "Bash(\"\"\" OWNER TO renan; FROM information_schema.sequences WHERE sequence_schema = public;\"\" | docker exec -i c95ebc27eb82 psql -U sistema -d strapi_blog\")", + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(git push:*)", + "Bash(cargo check:*)", + "Bash(bun run:*)", + "Bash(icacls \"D:\\Projetos IA\\sistema-de-chamados\\codex_ed25519\")", + "Bash(copy \"D:\\Projetos IA\\sistema-de-chamados\\codex_ed25519\" \"%TEMP%\\codex_key\")", + "Bash(icacls \"%TEMP%\\codex_key\" /inheritance:r /grant:r \"%USERNAME%:R\")", + "Bash(cmd /c \"echo %TEMP%\")", + "Bash(cmd /c \"dir \"\"%TEMP%\\codex_key\"\"\")", + "Bash(where:*)", + "Bash(ssh-keygen:*)", + "Bash(/c/Program\\ Files/Git/usr/bin/ssh:*)", + "Bash(npx convex deploy:*)", + "Bash(dir \"%LOCALAPPDATA%\\Raven\")", + "Bash(dir \"%APPDATA%\\Raven\")", + "Bash(dir \"%LOCALAPPDATA%\\com.raven.app\")", + "Bash(dir \"%APPDATA%\\com.raven.app\")", + "Bash(tasklist:*)", + "Bash(dir /s /b %LOCALAPPDATA%*raven*)", + "Bash(cmd /c \"tasklist | findstr /i raven\")", + "Bash(cmd /c \"dir /s /b %LOCALAPPDATA%\\*raven* 2>nul\")", + "Bash(powershell -Command \"Get-Process | Where-Object {$_ProcessName -like ''*raven*'' -or $_ProcessName -like ''*appsdesktop*''} | Select-Object ProcessName, Id\")", + "Bash(node:*)", + "Bash(bun scripts/test-all-emails.tsx:*)", + "Bash(bun scripts/send-test-react-email.tsx:*)", + "Bash(dir:*)", + "Bash(git reset:*)", + "Bash(npx convex:*)", + "Bash(bun tsc:*)", + "Bash(scp:*)", + "Bash(docker run:*)", + "Bash(cmd /c \"docker run -d --name postgres-dev -p 5432:5432 -e POSTGRES_PASSWORD=dev -e POSTGRES_DB=sistema_chamados postgres:18\")", + "Bash(cmd /c \"docker ps -a --filter name=postgres-dev\")", + "Bash(cmd /c \"docker --version && docker ps -a\")", + "Bash(powershell -Command \"docker --version\")", + "Bash(powershell -Command \"docker run -d --name postgres-dev -p 5432:5432 -e POSTGRES_PASSWORD=dev -e POSTGRES_DB=sistema_chamados postgres:18\")", + "Bash(dir \"D:\\Projetos IA\\sistema-de-chamados\" /b)", + "Bash(bunx prisma migrate:*)", + "Bash(bunx prisma db push:*)", + "Bash(bun run auth:seed:*)", + "Bash(set DATABASE_URL=postgresql://postgres:dev@localhost:5432/sistema_chamados:*)", + "Bash(bun tsx:*)", + "Bash(DATABASE_URL=\"postgresql://postgres:dev@localhost:5432/sistema_chamados\" bun tsx:*)", + "Bash(docker stop:*)", + "Bash(docker rm:*)", + "Bash(git commit -m \"$(cat <<''EOF''\nfeat(checklist): exibe descricao do template e do item no ticket\n\n- Adiciona campo templateDescription ao schema do checklist\n- Copia descricao do template ao aplicar checklist no ticket\n- Exibe ambas descricoes na visualizacao do ticket (template em italico)\n- Adiciona documentacao de desenvolvimento local (docs/LOCAL-DEV.md)\n- Corrige prisma-client.mjs para usar PostgreSQL em vez de SQLite\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n)\")", + "Bash(timeout 90 git push:*)", + "Bash(docker ps:*)", + "Bash(docker start:*)", + "Bash(docker inspect:*)", + "Bash(docker exec:*)", + "Bash(timeout 90 git push)", + "Bash(bun test:*)", + "Bash(git restore:*)", + "Bash(cd:*)", + "Bash(dir \"D:\\Projetos IA\\sistema-de-chamados\\src\\components\\ui\" /b)", + "Bash(timeout 120 bun:*)", + "Bash(bun run tauri:build:*)", + "Bash(git remote:*)", + "Bash(powershell.exe -NoProfile -ExecutionPolicy Bypass -File \"D:/Projetos IA/sistema-de-chamados/scripts/test-windows-collection.ps1\")" + ] + } +} diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..739bb8b --- /dev/null +++ b/.env.example @@ -0,0 +1,38 @@ +NODE_ENV=development + +# Public app URL +NEXT_PUBLIC_APP_URL=http://localhost:3000 + +# Better Auth +BETTER_AUTH_URL=http://localhost:3000 +BETTER_AUTH_SECRET=your-secret-key-at-least-32-chars-long + +# Convex (dev server URL) +NEXT_PUBLIC_CONVEX_URL=http://127.0.0.1:3210 +CONVEX_INTERNAL_URL=http://127.0.0.1:3210 +# Intervalo (ms) para aceitar token revogado ao sincronizar acessos remotos (opcional) +REMOTE_ACCESS_TOKEN_GRACE_MS=900000 +# Token interno opcional para o dashboard de saude (/admin/health) e queries internas +INTERNAL_HEALTH_TOKEN=dev-health-token +# Segredo para crons HTTP (reutilize em prod se preferir um unico token) +REPORTS_CRON_SECRET=reports-cron-secret +# Diretório para arquivamento local de tickets (JSONL/backup) +ARCHIVE_DIR=./archives + +# PostgreSQL database (versao 18) +# Para desenvolvimento local, use Docker: +# docker run -d --name postgres-chamados -p 5432:5432 -e POSTGRES_PASSWORD=dev -e POSTGRES_DB=sistema_chamados postgres:18 +DATABASE_URL=postgresql://postgres:dev@localhost:5432/sistema_chamados + +# SMTP Configuration (production values in docs/SMTP.md) +SMTP_HOST=smtp.c.inova.com.br +SMTP_PORT=587 +SMTP_SECURE=false +SMTP_USER=envio@rever.com.br +SMTP_PASS=CAAJQm6ZT6AUdhXRTDYu +SMTP_FROM_NAME=Sistema de Chamados +SMTP_FROM_EMAIL=envio@rever.com.br + +# Dev-only bypass to simplify local testing (do NOT enable in prod) +# DEV_BYPASS_AUTH=0 +# NEXT_PUBLIC_DEV_BYPASS_AUTH=0 diff --git a/.forgejo/workflows/ci-cd-web-desktop.yml b/.forgejo/workflows/ci-cd-web-desktop.yml new file mode 100644 index 0000000..db80c21 --- /dev/null +++ b/.forgejo/workflows/ci-cd-web-desktop.yml @@ -0,0 +1,492 @@ +name: CI/CD Web + Desktop + +on: + push: + branches: [ main ] + tags: + - 'v*.*.*' + workflow_dispatch: + inputs: + force_web_deploy: + description: 'Forcar deploy do Web (ignorar filtro)?' + type: boolean + required: false + default: false + force_convex_deploy: + description: 'Forcar deploy do Convex (ignorar filtro)?' + type: boolean + required: false + default: false + +env: + APP_DIR: /srv/apps/sistema + VPS_UPDATES_DIR: /var/www/updates + +jobs: + changes: + name: Detect changes + runs-on: [ self-hosted, linux, vps ] + timeout-minutes: 5 + outputs: + convex: ${{ steps.filter.outputs.convex }} + web: ${{ steps.filter.outputs.web }} + steps: + - name: Checkout + uses: https://github.com/actions/checkout@v4 + - name: Paths filter + id: filter + uses: https://github.com/dorny/paths-filter@v3 + with: + filters: | + convex: + - 'convex/**' + web: + - 'src/**' + - 'public/**' + - 'prisma/**' + - 'next.config.ts' + - 'package.json' + - 'bun.lock' + - 'tsconfig.json' + - 'middleware.ts' + - 'stack.yml' + + deploy: + name: Deploy (VPS Linux) + needs: changes + timeout-minutes: 30 + if: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' }} + runs-on: [ self-hosted, linux, vps ] + steps: + - name: Checkout + uses: https://github.com/actions/checkout@v4 + + - name: Determine APP_DIR (fallback safe path) + id: appdir + run: | + TS=$(date +%s) + FALLBACK_DIR="$HOME/apps/web.build.$TS" + mkdir -p "$FALLBACK_DIR" + echo "Using APP_DIR (fallback)=$FALLBACK_DIR" + echo "EFFECTIVE_APP_DIR=$FALLBACK_DIR" >> "$GITHUB_ENV" + + - name: Setup Bun + uses: https://github.com/oven-sh/setup-bun@v2 + with: + bun-version: 1.3.4 + + - name: Sync workspace to APP_DIR (preserving local env) + run: | + mkdir -p "$EFFECTIVE_APP_DIR" + RSYNC_FLAGS="-az --inplace --no-times --no-perms --no-owner --no-group --delete" + EXCLUDE_ENV="--exclude '.env*' --exclude 'apps/desktop/.env*' --exclude 'convex/.env*'" + if [ "$EFFECTIVE_APP_DIR" != "${APP_DIR:-/srv/apps/sistema}" ]; then + EXCLUDE_ENV="" + fi + rsync $RSYNC_FLAGS \ + --filter='protect .next.old*' \ + --exclude '.next.old*' \ + --filter='protect node_modules' \ + --filter='protect node_modules/**' \ + --filter='protect .pnpm-store' \ + --filter='protect .pnpm-store/**' \ + --filter='protect .env' \ + --filter='protect .env*' \ + --filter='protect apps/desktop/.env*' \ + --filter='protect convex/.env*' \ + --exclude '.git' \ + --exclude '.next' \ + --exclude 'node_modules' \ + --exclude 'node_modules/**' \ + --exclude '.pnpm-store' \ + --exclude '.pnpm-store/**' \ + $EXCLUDE_ENV \ + ./ "$EFFECTIVE_APP_DIR"/ + + - name: Acquire Convex admin key + id: key + run: | + echo "Waiting for Convex container..." + CID="" + for attempt in $(seq 1 12); do + CID=$(docker ps --format '{{.ID}} {{.Names}}' | awk '/sistema_convex_backend/{print $1; exit}') + if [ -n "$CID" ]; then + echo "Convex container ready (CID=$CID)" + break + fi + echo "Attempt $attempt/12: container not ready yet; waiting 5s..." + sleep 5 + done + CONVEX_IMAGE="ghcr.io/get-convex/convex-backend:latest" + if [ -n "$CID" ]; then + KEY=$(docker exec -i "$CID" /bin/sh -lc './generate_admin_key.sh' | tr -d '\r' | grep -o 'convex-self-hosted|[^ ]*' | tail -n1) + else + echo "No running convex container detected; attempting offline admin key extraction..." + VOLUME="sistema_convex_data" + if docker volume inspect "$VOLUME" >/dev/null 2>&1; then + KEY=$(docker run --rm --entrypoint /bin/sh -v "$VOLUME":/convex/data "$CONVEX_IMAGE" -lc './generate_admin_key.sh' | tr -d '\r' | grep -o 'convex-self-hosted|[^ ]*' | tail -n1) + else + echo "Volume $VOLUME nao encontrado; nao foi possivel extrair a chave admin" + fi + fi + echo "ADMIN_KEY=$KEY" >> $GITHUB_OUTPUT + echo "Admin key acquired? $([ -n "$KEY" ] && echo yes || echo no)" + if [ -z "$KEY" ]; then + echo "ERRO: Nao foi possivel obter a chave admin do Convex" + docker service ps sistema_convex_backend || true + exit 1 + fi + + - name: Copy production .env if present + run: | + DEFAULT_DIR="${APP_DIR:-/srv/apps/sistema}" + if [ "$EFFECTIVE_APP_DIR" != "$DEFAULT_DIR" ] && [ -f "$DEFAULT_DIR/.env" ]; then + echo "Copying production .env from $DEFAULT_DIR to $EFFECTIVE_APP_DIR" + cp -f "$DEFAULT_DIR/.env" "$EFFECTIVE_APP_DIR/.env" + fi + + - name: Ensure Next.js cache directory exists and is writable + run: | + cd "$EFFECTIVE_APP_DIR" + mkdir -p .next/cache + chmod -R u+rwX .next || true + + - name: Cache Next.js build cache (.next/cache) + uses: https://github.com/actions/cache@v4 + with: + path: ${{ env.EFFECTIVE_APP_DIR }}/.next/cache + key: ${{ runner.os }}-nextjs-${{ hashFiles('bun.lock') }}-${{ hashFiles('next.config.ts') }} + restore-keys: | + ${{ runner.os }}-nextjs-${{ hashFiles('bun.lock') }}- + ${{ runner.os }}-nextjs- + + - name: Lint check (fail fast before build) + run: | + cd "$EFFECTIVE_APP_DIR" + docker run --rm \ + -v "$EFFECTIVE_APP_DIR":/app \ + -w /app \ + sistema_web:node22-bun \ + bash -lc "set -euo pipefail; bun install --frozen-lockfile --filter '!appsdesktop'; bun run lint" + + - name: Install and build (Next.js) + env: + PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING: "1" + run: | + cd "$EFFECTIVE_APP_DIR" + docker run --rm \ + -e PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING="$PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING" \ + -e NODE_OPTIONS="--max-old-space-size=4096" \ + -v "$EFFECTIVE_APP_DIR":/app \ + -w /app \ + sistema_web:node22-bun \ + bash -lc "set -euo pipefail; bun install --frozen-lockfile --filter '!appsdesktop'; bun run prisma:generate; bun run build:bun" + + - name: Fix Docker-created file permissions + run: | + # Docker cria arquivos como root - corrigir para o usuario runner (UID 1000) + docker run --rm -v "$EFFECTIVE_APP_DIR":/target alpine:3 \ + chown -R 1000:1000 /target + echo "Permissoes do build corrigidas" + + - name: Atualizar symlink do APP_DIR estavel (deploy atomico) + run: | + set -euo pipefail + ROOT="$HOME/apps" + STABLE_LINK="$ROOT/sistema.current" + + mkdir -p "$ROOT" + + # Sanidade: se esses arquivos nao existirem, o container vai falhar no boot. + test -f "$EFFECTIVE_APP_DIR/scripts/start-web.sh" || { echo "ERROR: scripts/start-web.sh nao encontrado em $EFFECTIVE_APP_DIR" >&2; exit 1; } + test -f "$EFFECTIVE_APP_DIR/stack.yml" || { echo "ERROR: stack.yml nao encontrado em $EFFECTIVE_APP_DIR" >&2; exit 1; } + test -d "$EFFECTIVE_APP_DIR/node_modules" || { echo "ERROR: node_modules nao encontrado em $EFFECTIVE_APP_DIR (necessario para next start)" >&2; exit 1; } + test -d "$EFFECTIVE_APP_DIR/.next" || { echo "ERROR: .next nao encontrado em $EFFECTIVE_APP_DIR (build nao gerado)" >&2; exit 1; } + + PREV="" + if [ -L "$STABLE_LINK" ]; then + PREV="$(readlink -f "$STABLE_LINK" || true)" + fi + echo "PREV_APP_DIR=$PREV" >> "$GITHUB_ENV" + + ln -sfn "$EFFECTIVE_APP_DIR" "$STABLE_LINK" + + # Compat: mantem $HOME/apps/sistema como symlink quando possivel (nao mexe se for pasta). + if [ -L "$ROOT/sistema" ] || [ ! -e "$ROOT/sistema" ]; then + ln -sfn "$STABLE_LINK" "$ROOT/sistema" + fi + + echo "APP_DIR estavel -> $(readlink -f "$STABLE_LINK")" + + - name: Swarm deploy (stack.yml) + run: | + APP_DIR_STABLE="$HOME/apps/sistema.current" + if [ ! -d "$APP_DIR_STABLE" ]; then + echo "ERROR: Stable APP_DIR does not exist: $APP_DIR_STABLE" >&2; exit 1 + fi + cd "$APP_DIR_STABLE" + set -o allexport + if [ -f .env ]; then + echo "Loading .env from $APP_DIR_STABLE" + . ./.env + else + echo "WARNING: No .env found at $APP_DIR_STABLE - stack vars may be empty!" + fi + set +o allexport + echo "Using APP_DIR (stable)=$APP_DIR_STABLE" + echo "NEXT_PUBLIC_CONVEX_URL=${NEXT_PUBLIC_CONVEX_URL:-}" + echo "NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL:-}" + APP_DIR="$APP_DIR_STABLE" RELEASE_SHA=${{ github.sha }} docker stack deploy --with-registry-auth -c stack.yml sistema + + - name: Wait for services to be healthy + run: | + echo "Aguardando servicos ficarem saudaveis..." + for i in $(seq 1 18); do + WEB_STATUS=$(docker service ls --filter "name=sistema_web" --format "{{.Replicas}}" 2>/dev/null || echo "0/0") + CONVEX_STATUS=$(docker service ls --filter "name=sistema_convex_backend" --format "{{.Replicas}}" 2>/dev/null || echo "0/0") + echo "Tentativa $i/18: web=$WEB_STATUS convex=$CONVEX_STATUS" + if echo "$WEB_STATUS" | grep -q "2/2" && echo "$CONVEX_STATUS" | grep -q "1/1"; then + echo "Todos os servicos estao saudaveis!" + exit 0 + fi + sleep 10 + done + echo "ERRO: Timeout aguardando servicos. Status atual:" + docker service ls --filter "label=com.docker.stack.namespace=sistema" || true + docker service ps sistema_web --no-trunc || true + docker service logs sistema_web --since 5m --raw 2>/dev/null | tail -n 200 || true + + if [ -n "${PREV_APP_DIR:-}" ]; then + echo "Rollback: revertendo APP_DIR estavel para: $PREV_APP_DIR" + ln -sfn "$PREV_APP_DIR" "$HOME/apps/sistema.current" + cd "$HOME/apps/sistema.current" + set -o allexport + if [ -f .env ]; then + . ./.env + fi + set +o allexport + APP_DIR="$HOME/apps/sistema.current" RELEASE_SHA=${{ github.sha }} docker stack deploy --with-registry-auth -c stack.yml sistema || true + fi + + exit 1 + + - name: Cleanup old build workdirs (keep last 2) + run: | + set -e + ROOT="$HOME/apps" + KEEP=2 + PATTERN='web.build.*' + ACTIVE="$(readlink -f "$HOME/apps/sistema.current" 2>/dev/null || true)" + echo "Scanning $ROOT for old $PATTERN dirs" + LIST=$(find "$ROOT" -maxdepth 1 -type d -name "$PATTERN" | sort -r || true) + echo "$LIST" | sed -n "1,${KEEP}p" | sed 's/^/Keeping: /' || true + echo "$LIST" | sed "1,${KEEP}d" | while read dir; do + [ -z "$dir" ] && continue + if [ -n "$ACTIVE" ] && [ "$(readlink -f "$dir")" = "$ACTIVE" ]; then + echo "Skipping active dir (in use by APP_DIR): $dir"; continue + fi + echo "Removing $dir" + chmod -R u+rwX "$dir" 2>/dev/null || true + rm -rf "$dir" || { + echo "Local rm failed, falling back to docker (root) cleanup for $dir..." + docker run --rm -v "$dir":/target alpine:3 sh -lc 'chown -R 1000:1000 /target 2>/dev/null || true; chmod -R u+rwX /target 2>/dev/null || true; rm -rf /target/* /target/.[!.]* /target/..?* 2>/dev/null || true' || true + rm -rf "$dir" 2>/dev/null || rmdir "$dir" 2>/dev/null || true + } + done + echo "Disk usage (top 10 under $ROOT):" + du -sh "$ROOT"/* 2>/dev/null | sort -rh | head -n 10 || true + + convex_deploy: + name: Deploy Convex functions + needs: changes + timeout-minutes: 20 + if: ${{ github.event_name == 'workflow_dispatch' || needs.changes.outputs.convex == 'true' }} + runs-on: [ self-hosted, linux, vps ] + env: + APP_DIR: /srv/apps/sistema + steps: + - name: Checkout + uses: https://github.com/actions/checkout@v4 + + - name: Determine APP_DIR (fallback safe path) + id: appdir + run: | + TS=$(date +%s) + FALLBACK_DIR="$HOME/apps/convex.build.$TS" + mkdir -p "$FALLBACK_DIR" + echo "Using APP_DIR (fallback)=$FALLBACK_DIR" + echo "EFFECTIVE_APP_DIR=$FALLBACK_DIR" >> "$GITHUB_ENV" + + - name: Sync workspace to APP_DIR (preserving local env) + run: | + mkdir -p "$EFFECTIVE_APP_DIR" + RSYNC_FLAGS="-az --inplace --no-times --no-perms --no-owner --no-group --delete" + rsync $RSYNC_FLAGS \ + --filter='protect .next.old*' \ + --exclude '.next.old*' \ + --exclude '.env*' \ + --exclude 'apps/desktop/.env*' \ + --exclude 'convex/.env*' \ + --filter='protect node_modules' \ + --filter='protect node_modules/**' \ + --filter='protect .pnpm-store' \ + --filter='protect .pnpm-store/**' \ + --exclude '.git' \ + --exclude '.next' \ + --exclude 'node_modules' \ + --exclude 'node_modules/**' \ + --exclude '.pnpm-store' \ + --exclude '.pnpm-store/**' \ + ./ "$EFFECTIVE_APP_DIR"/ + + - name: Acquire Convex admin key + id: key + run: | + echo "Waiting for Convex container..." + CID="" + for attempt in $(seq 1 12); do + CID=$(docker ps --format '{{.ID}} {{.Names}}' | awk '/sistema_convex_backend/{print $1; exit}') + if [ -n "$CID" ]; then + echo "Convex container ready (CID=$CID)" + break + fi + echo "Attempt $attempt/12: container not ready yet; waiting 5s..." + sleep 5 + done + CONVEX_IMAGE="ghcr.io/get-convex/convex-backend:latest" + if [ -n "$CID" ]; then + KEY=$(docker exec -i "$CID" /bin/sh -lc './generate_admin_key.sh' | tr -d '\r' | grep -o 'convex-self-hosted|[^ ]*' | tail -n1) + else + echo "No running convex container detected; attempting offline admin key extraction..." + VOLUME="sistema_convex_data" + if docker volume inspect "$VOLUME" >/dev/null 2>&1; then + KEY=$(docker run --rm --entrypoint /bin/sh -v "$VOLUME":/convex/data "$CONVEX_IMAGE" -lc './generate_admin_key.sh' | tr -d '\r' | grep -o 'convex-self-hosted|[^ ]*' | tail -n1) + else + echo "Volume $VOLUME nao encontrado; nao foi possivel extrair a chave admin" + fi + fi + echo "ADMIN_KEY=$KEY" >> $GITHUB_OUTPUT + echo "Admin key acquired? $([ -n "$KEY" ] && echo yes || echo no)" + if [ -z "$KEY" ]; then + echo "ERRO: Nao foi possivel obter a chave admin do Convex" + docker service ps sistema_convex_backend || true + exit 1 + fi + + - name: Bring convex.json from live app if present + run: | + if [ -f "$APP_DIR/convex.json" ]; then + echo "Copying $APP_DIR/convex.json -> $EFFECTIVE_APP_DIR/convex.json" + cp -f "$APP_DIR/convex.json" "$EFFECTIVE_APP_DIR/convex.json" + else + echo "No existing convex.json found at $APP_DIR; convex CLI will need self-hosted vars" + fi + + - name: Set Convex env vars (self-hosted) + env: + CONVEX_SELF_HOSTED_URL: https://convex.esdrasrenan.com.br + CONVEX_SELF_HOSTED_ADMIN_KEY: ${{ steps.key.outputs.ADMIN_KEY }} + MACHINE_PROVISIONING_SECRET: ${{ secrets.MACHINE_PROVISIONING_SECRET }} + MACHINE_TOKEN_TTL_MS: ${{ secrets.MACHINE_TOKEN_TTL_MS }} + FLEET_SYNC_SECRET: ${{ secrets.FLEET_SYNC_SECRET }} + run: | + set -e + docker run --rm -i \ + -v "$EFFECTIVE_APP_DIR":/app \ + -w /app \ + -e CONVEX_SELF_HOSTED_URL \ + -e CONVEX_SELF_HOSTED_ADMIN_KEY \ + -e MACHINE_PROVISIONING_SECRET \ + -e MACHINE_TOKEN_TTL_MS \ + -e FLEET_SYNC_SECRET \ + -e CONVEX_TMPDIR=/app/.convex-tmp \ + node:20-bullseye bash -lc "set -euo pipefail; curl -fsSL https://bun.sh/install | bash >/tmp/bun-install.log; export BUN_INSTALL=\"\${BUN_INSTALL:-/root/.bun}\"; export PATH=\"\$BUN_INSTALL/bin:\$PATH\"; export CONVEX_TMPDIR=/app/.convex-tmp; bun install --frozen-lockfile; \ + if [ -n \"$MACHINE_PROVISIONING_SECRET\" ]; then bunx convex env set MACHINE_PROVISIONING_SECRET \"$MACHINE_PROVISIONING_SECRET\"; fi; \ + if [ -n \"$MACHINE_TOKEN_TTL_MS\" ]; then bunx convex env set MACHINE_TOKEN_TTL_MS \"$MACHINE_TOKEN_TTL_MS\"; fi; \ + if [ -n \"$FLEET_SYNC_SECRET\" ]; then bunx convex env set FLEET_SYNC_SECRET \"$FLEET_SYNC_SECRET\"; fi; \ + bunx convex env list" + + - name: Prepare Convex deploy workspace + run: | + cd "$EFFECTIVE_APP_DIR" + if [ -f .env ]; then + echo "Renaming .env -> .env.bak (Convex self-hosted deploy)" + mv -f .env .env.bak + fi + mkdir -p .convex-tmp + + - name: Deploy functions to Convex self-hosted + env: + CONVEX_SELF_HOSTED_URL: https://convex.esdrasrenan.com.br + CONVEX_SELF_HOSTED_ADMIN_KEY: ${{ steps.key.outputs.ADMIN_KEY }} + run: | + docker run --rm -i \ + -v "$EFFECTIVE_APP_DIR":/app \ + -w /app \ + -e CI=true \ + -e CONVEX_SELF_HOSTED_URL \ + -e CONVEX_SELF_HOSTED_ADMIN_KEY \ + -e CONVEX_TMPDIR=/app/.convex-tmp \ + node:20-bullseye bash -lc "set -euo pipefail; curl -fsSL https://bun.sh/install | bash >/tmp/bun-install.log; export BUN_INSTALL=\"\${BUN_INSTALL:-/root/.bun}\"; export PATH=\"\$BUN_INSTALL/bin:\$PATH\"; export CONVEX_TMPDIR=/app/.convex-tmp; bun install --frozen-lockfile; bunx convex deploy" + + - name: Cleanup old convex build workdirs (keep last 2) + run: | + set -e + ROOT="$HOME/apps" + KEEP=2 + PATTERN='convex.build.*' + LIST=$(find "$ROOT" -maxdepth 1 -type d -name "$PATTERN" | sort -r || true) + echo "$LIST" | sed -n "1,${KEEP}p" | sed 's/^/Keeping: /' || true + echo "$LIST" | sed "1,${KEEP}d" | while read dir; do + [ -z "$dir" ] && continue + echo "Removing $dir" + chmod -R u+rwX "$dir" 2>/dev/null || true + rm -rf "$dir" || { + echo "Local rm failed, falling back to docker (root) cleanup for $dir..." + docker run --rm -v "$dir":/target alpine:3 sh -lc 'chown -R 1000:1000 /target 2>/dev/null || true; chmod -R u+rwX /target 2>/dev/null || true; rm -rf /target/* /target/.[!.]* /target/..?* 2>/dev/null || true' || true + rm -rf "$dir" 2>/dev/null || rmdir "$dir" 2>/dev/null || true + } + done + + # NOTA: Job comentado porque nao ha runner Windows configurado. + # Descomentar quando configurar um runner com labels: [self-hosted, windows, desktop] + # + # desktop_release: + # name: Desktop Release (Windows) + # timeout-minutes: 30 + # if: ${{ startsWith(github.ref, 'refs/tags/v') }} + # runs-on: [ self-hosted, windows, desktop ] + # defaults: + # run: + # working-directory: apps/desktop + # steps: + # - name: Checkout + # uses: https://github.com/actions/checkout@v4 + # + # - name: Setup pnpm + # uses: https://github.com/pnpm/action-setup@v4 + # with: + # version: 10.20.0 + # + # - name: Setup Node.js + # uses: https://github.com/actions/setup-node@v4 + # with: + # node-version: 20 + # + # - name: Install deps (desktop) + # run: pnpm install --frozen-lockfile + # + # - name: Build with Tauri + # uses: https://github.com/tauri-apps/tauri-action@v0 + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} + # TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + # with: + # projectPath: apps/desktop + # + # - name: Upload bundles to VPS + # run: | + # # Upload via SCP (configurar chave SSH no runner Windows) + # # scp -r src-tauri/target/release/bundle/* user@vps:/var/www/updates/ + # echo "TODO: Configurar upload para VPS" diff --git a/.forgejo/workflows/quality-checks.yml b/.forgejo/workflows/quality-checks.yml new file mode 100644 index 0000000..daed18b --- /dev/null +++ b/.forgejo/workflows/quality-checks.yml @@ -0,0 +1,54 @@ +name: Quality Checks + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + lint-test-build: + name: Lint, Test and Build + runs-on: [ self-hosted, linux, vps ] + env: + BETTER_AUTH_SECRET: test-secret + NEXT_PUBLIC_APP_URL: http://localhost:3000 + BETTER_AUTH_URL: http://localhost:3000 + NEXT_PUBLIC_CONVEX_URL: http://localhost:3210 + DATABASE_URL: file:./prisma/db.dev.sqlite + steps: + - name: Checkout + uses: https://github.com/actions/checkout@v4 + + - name: Setup Bun + uses: https://github.com/oven-sh/setup-bun@v2 + with: + bun-version: 1.3.4 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Cache Next.js build cache + uses: https://github.com/actions/cache@v4 + with: + path: | + ${{ github.workspace }}/.next/cache + key: ${{ runner.os }}-nextjs-${{ hashFiles('bun.lock') }}-${{ hashFiles('**/*.{js,jsx,ts,tsx}') }} + restore-keys: | + ${{ runner.os }}-nextjs-${{ hashFiles('bun.lock') }}- + + - name: Generate Prisma client + env: + PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING: "1" + run: bun run prisma:generate + + - name: Lint + run: bun run lint + + - name: Test + run: bun test + + - name: Build + run: bun run build:bun diff --git a/.github/workflows.disabled/ci-cd-web-desktop.yml b/.github/workflows.disabled/ci-cd-web-desktop.yml new file mode 100644 index 0000000..e95322c --- /dev/null +++ b/.github/workflows.disabled/ci-cd-web-desktop.yml @@ -0,0 +1,639 @@ +name: CI/CD Web + Desktop + +on: + push: + branches: [ main ] + tags: + - 'v*.*.*' + workflow_dispatch: + inputs: + force_web_deploy: + description: 'Forçar deploy do Web (ignorar filtro)?' + required: false + default: 'false' + force_convex_deploy: + description: 'Forçar deploy do Convex (ignorar filtro)?' + required: false + default: 'false' + +env: + APP_DIR: /srv/apps/sistema + VPS_UPDATES_DIR: /var/www/updates + RUN_MACHINE_SMOKE: ${{ vars.RUN_MACHINE_SMOKE || secrets.RUN_MACHINE_SMOKE || 'false' }} + +jobs: + changes: + name: Detect changes + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + convex: ${{ steps.filter.outputs.convex }} + web: ${{ steps.filter.outputs.web }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Paths filter + id: filter + uses: dorny/paths-filter@v3 + with: + filters: | + convex: + - 'convex/**' + web: + - 'src/**' + - 'public/**' + - 'prisma/**' + - 'next.config.ts' + - 'package.json' + - 'pnpm-lock.yaml' + - 'tsconfig.json' + - 'middleware.ts' + - 'stack.yml' + + deploy: + name: Deploy (VPS Linux) + needs: changes + timeout-minutes: 30 + # Executa em qualquer push na main (independente do filtro) ou quando disparado manualmente + if: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' }} + runs-on: [ self-hosted, linux, vps ] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Determine APP_DIR (fallback safe path) + id: appdir + run: | + TS=$(date +%s) + # Use a web-specific build dir to avoid clashes with convex job + FALLBACK_DIR="$HOME/apps/web.build.$TS" + mkdir -p "$FALLBACK_DIR" + echo "Using APP_DIR (fallback)=$FALLBACK_DIR" + echo "EFFECTIVE_APP_DIR=$FALLBACK_DIR" >> "$GITHUB_ENV" + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.20.0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.1 + + - name: Verify Bun runtime + run: bun --version + + - name: Permissions diagnostic (server paths) + run: | + set +e + echo "== Basic context ==" + whoami || true + id || true + groups || true + umask || true + echo "HOME=$HOME" + echo "APP_DIR(default)=${APP_DIR:-/srv/apps/sistema}" + echo "EFFECTIVE_APP_DIR=$EFFECTIVE_APP_DIR" + + echo "\n== Permissions check ==" + check_path() { + P="$1" + echo "-- $P" + if [ -e "$P" ]; then + stat -c '%A %U:%G %n' "$P" 2>/dev/null || ls -ld "$P" || true + echo -n "WRITABLE? "; [ -w "$P" ] && echo yes || echo no + if command -v namei >/dev/null 2>&1; then + namei -l "$P" || true + fi + TMP="$P/.permtest.$$" + (echo test > "$TMP" 2>/dev/null && echo "CREATE_FILE: ok" && rm -f "$TMP") || echo "CREATE_FILE: failed" + else + echo "(missing)" + fi + } + check_path "/srv/apps/sistema" + check_path "/srv/apps/sistema/src/app/machines/handshake" + check_path "/srv/apps/sistema/apps/desktop/node_modules" + check_path "/srv/apps/sistema/node_modules" + check_path "$EFFECTIVE_APP_DIR" + check_path "$EFFECTIVE_APP_DIR/node_modules" + + - name: Sync workspace to APP_DIR (preserving local env) + run: | + mkdir -p "$EFFECTIVE_APP_DIR" + RSYNC_FLAGS="-az --inplace --no-times --no-perms --no-owner --no-group --delete" + # Excluir .env apenas quando copiando para o diretório padrão (/srv) para preservar segredos locais + EXCLUDE_ENV="--exclude '.env*' --exclude 'apps/desktop/.env*' --exclude 'convex/.env*'" + if [ "$EFFECTIVE_APP_DIR" != "${APP_DIR:-/srv/apps/sistema}" ]; then + EXCLUDE_ENV="" + fi + rsync $RSYNC_FLAGS \ + --filter='protect .next.old*' \ + --exclude '.next.old*' \ + --filter='protect node_modules' \ + --filter='protect node_modules/**' \ + --filter='protect .pnpm-store' \ + --filter='protect .pnpm-store/**' \ + --filter='protect .env' \ + --filter='protect .env*' \ + --filter='protect apps/desktop/.env*' \ + --filter='protect convex/.env*' \ + --exclude '.git' \ + --exclude '.next' \ + --exclude 'node_modules' \ + --exclude 'node_modules/**' \ + --exclude '.pnpm-store' \ + --exclude '.pnpm-store/**' \ + $EXCLUDE_ENV \ + ./ "$EFFECTIVE_APP_DIR"/ + + - name: Acquire Convex admin key + id: key + run: | + echo "Waiting for Convex container..." + CID="" + # Aguarda ate 60s (12 tentativas x 5s) pelo container ficar pronto + # Nao forca restart - deixa o Swarm gerenciar via health checks + for attempt in $(seq 1 12); do + CID=$(docker ps --format '{{.ID}} {{.Names}}' | awk '/sistema_convex_backend/{print $1; exit}') + if [ -n "$CID" ]; then + echo "Convex container ready (CID=$CID)" + break + fi + echo "Attempt $attempt/12: container not ready yet; waiting 5s..." + sleep 5 + done + CONVEX_IMAGE="ghcr.io/get-convex/convex-backend:latest" + if [ -n "$CID" ]; then + KEY=$(docker exec -i "$CID" /bin/sh -lc './generate_admin_key.sh' | tr -d '\r' | grep -o 'convex-self-hosted|[^ ]*' | tail -n1) + else + echo "No running convex container detected; attempting offline admin key extraction..." + VOLUME="sistema_convex_data" + if docker volume inspect "$VOLUME" >/dev/null 2>&1; then + KEY=$(docker run --rm --entrypoint /bin/sh -v "$VOLUME":/convex/data "$CONVEX_IMAGE" -lc './generate_admin_key.sh' | tr -d '\r' | grep -o 'convex-self-hosted|[^ ]*' | tail -n1) + else + echo "Volume $VOLUME nao encontrado; nao foi possivel extrair a chave admin" + fi + fi + echo "ADMIN_KEY=$KEY" >> $GITHUB_OUTPUT + echo "Admin key acquired? $([ -n "$KEY" ] && echo yes || echo no)" + if [ -z "$KEY" ]; then + echo "ERRO: Nao foi possivel obter a chave admin do Convex" + docker service ps sistema_convex_backend || true + exit 1 + fi + + - name: Copy production .env if present + run: | + DEFAULT_DIR="${APP_DIR:-/srv/apps/sistema}" + if [ "$EFFECTIVE_APP_DIR" != "$DEFAULT_DIR" ] && [ -f "$DEFAULT_DIR/.env" ]; then + echo "Copying production .env from $DEFAULT_DIR to $EFFECTIVE_APP_DIR" + cp -f "$DEFAULT_DIR/.env" "$EFFECTIVE_APP_DIR/.env" + fi + + - name: Prune workspace for server-only build + run: | + cd "$EFFECTIVE_APP_DIR" + # Keep only root (web) as a package in this effective workspace + printf "packages:\n - .\n\nignoredBuiltDependencies:\n - '@prisma/client'\n - '@prisma/engines'\n - '@tailwindcss/oxide'\n - esbuild\n - prisma\n - sharp\n - unrs-resolver\n" > pnpm-workspace.yaml + + - name: Ensure Next.js cache directory exists and is writable + run: | + cd "$EFFECTIVE_APP_DIR" + mkdir -p .next/cache + chmod -R u+rwX .next || true + + - name: Cache Next.js build cache (.next/cache) + uses: actions/cache@v4 + with: + path: ${{ env.EFFECTIVE_APP_DIR }}/.next/cache + key: ${{ runner.os }}-nextjs-${{ hashFiles('pnpm-lock.yaml', 'bun.lock') }}-${{ hashFiles('src/**/*.ts', 'src/**/*.tsx', 'src/**/*.js', 'src/**/*.jsx', 'next.config.ts') }} + restore-keys: | + ${{ runner.os }}-nextjs-${{ hashFiles('pnpm-lock.yaml', 'bun.lock') }}- + + - name: Lint check (fail fast before build) + run: | + cd "$EFFECTIVE_APP_DIR" + docker run --rm \ + -v "$EFFECTIVE_APP_DIR":/app \ + -w /app \ + sistema_web:node22-bun \ + bash -lc "set -euo pipefail; bun install --frozen-lockfile --filter '!appsdesktop'; bun run lint" + + - name: Install and build (Next.js) + env: + PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING: "1" + run: | + cd "$EFFECTIVE_APP_DIR" + docker run --rm \ + -e PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING="$PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING" \ + -e NODE_OPTIONS="--max-old-space-size=4096" \ + -v "$EFFECTIVE_APP_DIR":/app \ + -w /app \ + sistema_web:node22-bun \ + bash -lc "set -euo pipefail; bun install --frozen-lockfile --filter '!appsdesktop'; bun run prisma:generate; bun run build:bun" + + - name: Publish build to stable APP_DIR directory + run: | + set -e + DEST="$HOME/apps/sistema" + mkdir -p "$DEST" + mkdir -p "$DEST/.next/static" + # One-time fix for old root-owned files (esp. .pnpm-store) left by previous containers + docker run --rm -v "$DEST":/target alpine:3 sh -lc 'chown -R 1000:1000 /target 2>/dev/null || true; chmod -R u+rwX /target 2>/dev/null || true' || true + # Preserve previously published static assets to keep stale chunks available for clients mid-navigation + if [ -d "$EFFECTIVE_APP_DIR/.next/static" ]; then + rsync -a \ + "$EFFECTIVE_APP_DIR/.next/static/" "$DEST/.next/static/" + fi + # Publish new build; exclude .pnpm-store to avoid Permission denied on old entries + rsync -a --delete \ + --chown=1000:1000 \ + --exclude '.pnpm-store' --exclude '.pnpm-store/**' \ + --exclude '.next/static' \ + "$EFFECTIVE_APP_DIR"/ "$DEST"/ + echo "Published build to: $DEST" + + - name: Swarm deploy (stack.yml) + run: | + APP_DIR_STABLE="$HOME/apps/sistema" + if [ ! -d "$APP_DIR_STABLE" ]; then + echo "ERROR: Stable APP_DIR does not exist: $APP_DIR_STABLE" >&2; exit 1 + fi + cd "$APP_DIR_STABLE" + # Exporta variáveis do .env (do diretório de produção) para substituição no stack + # IMPORTANTE: Usar o .env do APP_DIR_STABLE, não do EFFECTIVE_APP_DIR (build temporário) + set -o allexport + if [ -f .env ]; then + echo "Loading .env from $APP_DIR_STABLE" + . ./.env + else + echo "WARNING: No .env found at $APP_DIR_STABLE - stack vars may be empty!" + fi + set +o allexport + echo "Using APP_DIR (stable)=$APP_DIR_STABLE" + echo "NEXT_PUBLIC_CONVEX_URL=${NEXT_PUBLIC_CONVEX_URL:-}" + echo "NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL:-}" + APP_DIR="$APP_DIR_STABLE" RELEASE_SHA=${{ github.sha }} docker stack deploy --with-registry-auth -c stack.yml sistema + + - name: Wait for services to be healthy + run: | + echo "Aguardando servicos ficarem saudaveis..." + # Aguarda ate 3 minutos (18 tentativas x 10s) pelos servicos + for i in $(seq 1 18); do + WEB_STATUS=$(docker service ls --filter "name=sistema_web" --format "{{.Replicas}}" 2>/dev/null || echo "0/0") + CONVEX_STATUS=$(docker service ls --filter "name=sistema_convex_backend" --format "{{.Replicas}}" 2>/dev/null || echo "0/0") + echo "Tentativa $i/18: web=$WEB_STATUS convex=$CONVEX_STATUS" + # Verifica se web tem 2/2 replicas e convex tem 1/1 + if echo "$WEB_STATUS" | grep -q "2/2" && echo "$CONVEX_STATUS" | grep -q "1/1"; then + echo "Todos os servicos estao saudaveis!" + exit 0 + fi + sleep 10 + done + echo "AVISO: Timeout aguardando servicos. Status atual:" + docker service ls --filter "label=com.docker.stack.namespace=sistema" + # Nao falha o deploy, apenas avisa (o Swarm continua o rolling update em background) + + - name: Smoke test — register + heartbeat + run: | + set -e + if [ "${RUN_MACHINE_SMOKE:-false}" != "true" ]; then + echo "RUN_MACHINE_SMOKE != true — pulando smoke test"; exit 0 + fi + # Load MACHINE_PROVISIONING_SECRET from production .env on the host + if [ -f /srv/apps/sistema/.env ]; then + set -o allexport + . /srv/apps/sistema/.env + set +o allexport + fi + if [ -z "${MACHINE_PROVISIONING_SECRET:-}" ]; then + echo "MACHINE_PROVISIONING_SECRET ausente — pulando smoke test"; exit 0 + fi + HOSTNAME_TEST="ci-smoke-$(date +%s)" + BODY='{"provisioningSecret":"'"$MACHINE_PROVISIONING_SECRET"'","tenantId":"tenant-atlas","hostname":"'"$HOSTNAME_TEST"'","os":{"name":"Linux","version":"6.1.0","architecture":"x86_64"},"macAddresses":["AA:BB:CC:DD:EE:FF"],"serialNumbers":[],"metadata":{"inventory":{"cpu":"i7","ramGb":16}},"registeredBy":"ci-smoke"}' + HTTP=$(curl -sS -o resp.json -w "%{http_code}" -H 'Content-Type: application/json' -d "$BODY" https://tickets.esdrasrenan.com.br/api/machines/register || true) + echo "Register HTTP=$HTTP" + if [ "$HTTP" != "201" ]; then + echo "Register failed:"; tail -c 600 resp.json || true; exit 1; fi + TOKEN=$(node -e 'try{const j=require("fs").readFileSync("resp.json","utf8");process.stdout.write(JSON.parse(j).machineToken||"");}catch(e){process.stdout.write("")}' ) + if [ -z "$TOKEN" ]; then echo "Missing token in register response"; exit 1; fi + HB=$(curl -sS -o /dev/null -w "%{http_code}" -H 'Content-Type: application/json' -d '{"machineToken":"'"$TOKEN"'","status":"online","metrics":{"cpuPct":5,"memFreePct":70}}' https://tickets.esdrasrenan.com.br/api/machines/heartbeat || true) + echo "Heartbeat HTTP=$HB" + if [ "$HB" != "200" ]; then echo "Heartbeat failed"; exit 1; fi + + - name: Cleanup old build workdirs (keep last 2) + run: | + set -e + ROOT="$HOME/apps" + KEEP=2 + PATTERN='web.build.*' + ACTIVE="$HOME/apps/sistema" + echo "Scanning $ROOT for old $PATTERN dirs" + LIST=$(find "$ROOT" -maxdepth 1 -type d -name "$PATTERN" | sort -r || true) + echo "$LIST" | sed -n "1,${KEEP}p" | sed 's/^/Keeping: /' || true + echo "$LIST" | sed "1,${KEEP}d" | while read dir; do + [ -z "$dir" ] && continue + if [ -n "$ACTIVE" ] && [ "$(readlink -f "$dir")" = "$ACTIVE" ]; then + echo "Skipping active dir (in use by APP_DIR): $dir"; continue + fi + echo "Removing $dir" + chmod -R u+rwX "$dir" 2>/dev/null || true + rm -rf "$dir" || { + echo "Local rm failed, falling back to docker (root) cleanup for $dir..." + docker run --rm -v "$dir":/target alpine:3 sh -lc 'chown -R 1000:1000 /target 2>/dev/null || true; chmod -R u+rwX /target 2>/dev/null || true; rm -rf /target/* /target/.[!.]* /target/..?* 2>/dev/null || true' || true + rm -rf "$dir" 2>/dev/null || rmdir "$dir" 2>/dev/null || true + } + done + echo "Disk usage (top 10 under $ROOT):" + du -sh "$ROOT"/* 2>/dev/null | sort -rh | head -n 10 || true + + - name: Restart web service with new code (skip — stack deploy already updated) + if: ${{ always() && false }} + run: | + docker service update --force sistema_web + + # Comentado: o stack deploy já atualiza os serviços com update_config.order: start-first + # Forçar update aqui causa downtime porque ignora a estratégia de rolling update + # - name: Restart Convex backend service (optional) + # run: | + # docker service update --force sistema_convex_backend + + convex_deploy: + name: Deploy Convex functions + needs: changes + timeout-minutes: 20 + # Executa quando convex/** mudar ou via workflow_dispatch + if: ${{ github.event_name == 'workflow_dispatch' || needs.changes.outputs.convex == 'true' }} + runs-on: [ self-hosted, linux, vps ] + env: + APP_DIR: /srv/apps/sistema + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Determine APP_DIR (fallback safe path) + id: appdir + run: | + TS=$(date +%s) + # Use a convex-specific build dir to avoid clashes with web job + FALLBACK_DIR="$HOME/apps/convex.build.$TS" + mkdir -p "$FALLBACK_DIR" + echo "Using APP_DIR (fallback)=$FALLBACK_DIR" + echo "EFFECTIVE_APP_DIR=$FALLBACK_DIR" >> "$GITHUB_ENV" + + - name: Sync workspace to APP_DIR (preserving local env) + run: | + mkdir -p "$EFFECTIVE_APP_DIR" + RSYNC_FLAGS="-az --inplace --no-times --no-perms --no-owner --no-group --delete" + rsync $RSYNC_FLAGS \ + --filter='protect .next.old*' \ + --exclude '.next.old*' \ + --exclude '.env*' \ + --exclude 'apps/desktop/.env*' \ + --exclude 'convex/.env*' \ + --filter='protect node_modules' \ + --filter='protect node_modules/**' \ + --filter='protect .pnpm-store' \ + --filter='protect .pnpm-store/**' \ + --exclude '.git' \ + --exclude '.next' \ + --exclude 'node_modules' \ + --exclude 'node_modules/**' \ + --exclude '.pnpm-store' \ + --exclude '.pnpm-store/**' \ + ./ "$EFFECTIVE_APP_DIR"/ + + - name: Acquire Convex admin key + id: key + run: | + echo "Waiting for Convex container..." + CID="" + # Aguarda ate 60s (12 tentativas x 5s) pelo container ficar pronto + # Nao forca restart - deixa o Swarm gerenciar via health checks + for attempt in $(seq 1 12); do + CID=$(docker ps --format '{{.ID}} {{.Names}}' | awk '/sistema_convex_backend/{print $1; exit}') + if [ -n "$CID" ]; then + echo "Convex container ready (CID=$CID)" + break + fi + echo "Attempt $attempt/12: container not ready yet; waiting 5s..." + sleep 5 + done + CONVEX_IMAGE="ghcr.io/get-convex/convex-backend:latest" + if [ -n "$CID" ]; then + KEY=$(docker exec -i "$CID" /bin/sh -lc './generate_admin_key.sh' | tr -d '\r' | grep -o 'convex-self-hosted|[^ ]*' | tail -n1) + else + echo "No running convex container detected; attempting offline admin key extraction..." + VOLUME="sistema_convex_data" + if docker volume inspect "$VOLUME" >/dev/null 2>&1; then + KEY=$(docker run --rm --entrypoint /bin/sh -v "$VOLUME":/convex/data "$CONVEX_IMAGE" -lc './generate_admin_key.sh' | tr -d '\r' | grep -o 'convex-self-hosted|[^ ]*' | tail -n1) + else + echo "Volume $VOLUME nao encontrado; nao foi possivel extrair a chave admin" + fi + fi + echo "ADMIN_KEY=$KEY" >> $GITHUB_OUTPUT + echo "Admin key acquired? $([ -n "$KEY" ] && echo yes || echo no)" + if [ -z "$KEY" ]; then + echo "ERRO: Nao foi possivel obter a chave admin do Convex" + docker service ps sistema_convex_backend || true + exit 1 + fi + + - name: Bring convex.json from live app if present + run: | + if [ -f "$APP_DIR/convex.json" ]; then + echo "Copying $APP_DIR/convex.json -> $EFFECTIVE_APP_DIR/convex.json" + cp -f "$APP_DIR/convex.json" "$EFFECTIVE_APP_DIR/convex.json" + else + echo "No existing convex.json found at $APP_DIR; convex CLI will need self-hosted vars" + fi + + - name: Set Convex env vars (self-hosted) + env: + CONVEX_SELF_HOSTED_URL: https://convex.esdrasrenan.com.br + CONVEX_SELF_HOSTED_ADMIN_KEY: ${{ steps.key.outputs.ADMIN_KEY }} + MACHINE_PROVISIONING_SECRET: ${{ secrets.MACHINE_PROVISIONING_SECRET }} + MACHINE_TOKEN_TTL_MS: ${{ secrets.MACHINE_TOKEN_TTL_MS }} + FLEET_SYNC_SECRET: ${{ secrets.FLEET_SYNC_SECRET }} + run: | + set -e + docker run --rm -i \ + -v "$EFFECTIVE_APP_DIR":/app \ + -w /app \ + -e CONVEX_SELF_HOSTED_URL \ + -e CONVEX_SELF_HOSTED_ADMIN_KEY \ + -e MACHINE_PROVISIONING_SECRET \ + -e MACHINE_TOKEN_TTL_MS \ + -e FLEET_SYNC_SECRET \ + -e CONVEX_TMPDIR=/app/.convex-tmp \ + node:20-bullseye bash -lc "set -euo pipefail; curl -fsSL https://bun.sh/install | bash >/tmp/bun-install.log; export BUN_INSTALL=\"\${BUN_INSTALL:-/root/.bun}\"; export PATH=\"\$BUN_INSTALL/bin:\$PATH\"; export CONVEX_TMPDIR=/app/.convex-tmp; bun install --frozen-lockfile; \ + if [ -n \"$MACHINE_PROVISIONING_SECRET\" ]; then bunx convex env set MACHINE_PROVISIONING_SECRET \"$MACHINE_PROVISIONING_SECRET\"; fi; \ + if [ -n \"$MACHINE_TOKEN_TTL_MS\" ]; then bunx convex env set MACHINE_TOKEN_TTL_MS \"$MACHINE_TOKEN_TTL_MS\"; fi; \ + if [ -n \"$FLEET_SYNC_SECRET\" ]; then bunx convex env set FLEET_SYNC_SECRET \"$FLEET_SYNC_SECRET\"; fi; \ + bunx convex env list" + + - name: Prepare Convex deploy workspace + run: | + cd "$EFFECTIVE_APP_DIR" + if [ -f .env ]; then + echo "Renaming .env -> .env.bak (Convex self-hosted deploy)" + mv -f .env .env.bak + fi + # Dedicated tmp dir outside convex/_generated so CLI cleanups don't remove it + mkdir -p .convex-tmp + - name: Deploy functions to Convex self-hosted + env: + CONVEX_SELF_HOSTED_URL: https://convex.esdrasrenan.com.br + CONVEX_SELF_HOSTED_ADMIN_KEY: ${{ steps.key.outputs.ADMIN_KEY }} + run: | + docker run --rm -i \ + -v "$EFFECTIVE_APP_DIR":/app \ + -w /app \ + -e CI=true \ + -e CONVEX_SELF_HOSTED_URL \ + -e CONVEX_SELF_HOSTED_ADMIN_KEY \ + -e CONVEX_TMPDIR=/app/.convex-tmp \ + node:20-bullseye bash -lc "set -euo pipefail; curl -fsSL https://bun.sh/install | bash >/tmp/bun-install.log; export BUN_INSTALL=\"\${BUN_INSTALL:-/root/.bun}\"; export PATH=\"\$BUN_INSTALL/bin:\$PATH\"; export CONVEX_TMPDIR=/app/.convex-tmp; bun install --frozen-lockfile; bunx convex deploy" + + - name: Cleanup old convex build workdirs (keep last 2) + run: | + set -e + ROOT="$HOME/apps" + KEEP=2 + PATTERN='convex.build.*' + LIST=$(find "$ROOT" -maxdepth 1 -type d -name "$PATTERN" | sort -r || true) + echo "$LIST" | sed -n "1,${KEEP}p" | sed 's/^/Keeping: /' || true + echo "$LIST" | sed "1,${KEEP}d" | while read dir; do + [ -z "$dir" ] && continue + echo "Removing $dir" + chmod -R u+rwX "$dir" 2>/dev/null || true + rm -rf "$dir" || { + echo "Local rm failed, falling back to docker (root) cleanup for $dir..." + docker run --rm -v "$dir":/target alpine:3 sh -lc 'chown -R 1000:1000 /target 2>/dev/null || true; chmod -R u+rwX /target 2>/dev/null || true; rm -rf /target/* /target/.[!.]* /target/..?* 2>/dev/null || true' || true + rm -rf "$dir" 2>/dev/null || rmdir "$dir" 2>/dev/null || true + } + done + + desktop_release: + name: Desktop Release (Windows) + timeout-minutes: 30 + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + runs-on: [ self-hosted, windows, desktop ] + defaults: + run: + working-directory: apps/desktop + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.20.0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install deps (desktop) + run: pnpm install --frozen-lockfile + + - name: Build with Tauri + uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} + TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + with: + projectPath: apps/desktop + + + - name: Upload latest.json + bundles to VPS + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.VPS_HOST }} + username: ${{ secrets.VPS_USER }} + key: ${{ secrets.VPS_SSH_KEY }} + source: | + **/bundle/**/latest.json + **/bundle/**/* + target: ${{ env.VPS_UPDATES_DIR }} + overwrite: true + + diagnose_convex: + name: Diagnose Convex (env + register test) + timeout-minutes: 10 + if: ${{ github.event_name == 'workflow_dispatch' }} + runs-on: [ self-hosted, linux, vps ] + steps: + - name: Print service env and .env subset + run: | + echo "=== Convex service env ===" + docker service inspect sistema_convex_backend --format '{{range .Spec.TaskTemplate.ContainerSpec.Env}}{{println .}}{{end}}' || true + echo + echo "=== /srv/apps/sistema/.env subset ===" + [ -f /srv/apps/sistema/.env ] && grep -E '^(MACHINE_PROVISIONING_SECRET|MACHINE_TOKEN_TTL_MS|FLEET_SYNC_SECRET|NEXT_PUBLIC_CONVEX_URL)=' -n /srv/apps/sistema/.env || echo '(no .env)' + - name: Acquire Convex admin key + id: key + run: | + echo "Waiting for Convex container..." + CID="" + # Aguarda ate 60s (12 tentativas x 5s) pelo container ficar pronto + for attempt in $(seq 1 12); do + CID=$(docker ps --format '{{.ID}} {{.Names}}' | awk '/sistema_convex_backend/{print $1; exit}') + if [ -n "$CID" ]; then + echo "Convex container ready (CID=$CID)" + break + fi + echo "Attempt $attempt/12: container not ready yet; waiting 5s..." + sleep 5 + done + CONVEX_IMAGE="ghcr.io/get-convex/convex-backend:latest" + if [ -n "$CID" ]; then + KEY=$(docker exec -i "$CID" /bin/sh -lc './generate_admin_key.sh' | tr -d '\r' | grep -o 'convex-self-hosted|[^ ]*' | tail -n1) + else + echo "No running convex container detected; attempting offline admin key extraction..." + VOLUME="sistema_convex_data" + if docker volume inspect "$VOLUME" >/dev/null 2>&1; then + KEY=$(docker run --rm --entrypoint /bin/sh -v "$VOLUME":/convex/data "$CONVEX_IMAGE" -lc './generate_admin_key.sh' | tr -d '\r' | grep -o 'convex-self-hosted|[^ ]*' | tail -n1) + else + echo "Volume $VOLUME nao encontrado; nao foi possivel extrair a chave admin" + fi + fi + echo "ADMIN_KEY=$KEY" >> $GITHUB_OUTPUT + echo "Admin key acquired? $([ -n "$KEY" ] && echo yes || echo no)" + - name: List Convex env and set missing + env: + CONVEX_SELF_HOSTED_URL: https://convex.esdrasrenan.com.br + ADMIN_KEY: ${{ steps.key.outputs.ADMIN_KEY }} + run: | + set -e + if [ -f /srv/apps/sistema/.env ]; then + set -o allexport + . /srv/apps/sistema/.env + set +o allexport + fi + docker run --rm -i \ + -v /srv/apps/sistema:/app -w /app \ + -e CONVEX_SELF_HOSTED_URL -e CONVEX_SELF_HOSTED_ADMIN_KEY="$ADMIN_KEY" \ + -e MACHINE_PROVISIONING_SECRET -e MACHINE_TOKEN_TTL_MS -e FLEET_SYNC_SECRET \ + node:20-bullseye bash -lc "set -euo pipefail; curl -fsSL https://bun.sh/install | bash >/tmp/bun-install.log; export BUN_INSTALL=\"\${BUN_INSTALL:-/root/.bun}\"; export PATH=\"\$BUN_INSTALL/bin:\$PATH\"; bun install --frozen-lockfile; \ + unset CONVEX_DEPLOYMENT; bunx convex env list; \ + if [ -n \"$MACHINE_PROVISIONING_SECRET\" ]; then bunx convex env set MACHINE_PROVISIONING_SECRET \"$MACHINE_PROVISIONING_SECRET\"; fi; \ + if [ -n \"$MACHINE_TOKEN_TTL_MS\" ]; then bunx convex env set MACHINE_TOKEN_TTL_MS \"$MACHINE_TOKEN_TTL_MS\"; fi; \ + if [ -n \"$FLEET_SYNC_SECRET\" ]; then bunx convex env set FLEET_SYNC_SECRET \"$FLEET_SYNC_SECRET\"; fi; \ + bunx convex env list" + - name: Test register from runner + run: | + HOST="vm-teste-$(date +%s)" + DATA='{"provisioningSecret":"'"${MACHINE_PROVISIONING_SECRET:-"71daa9ef54cb224547e378f8121ca898b614446c142a132f73c2221b4d53d7d6"}"'","tenantId":"tenant-atlas","hostname":"'"$HOST"'","os":{"name":"Linux","version":"6.1.0","architecture":"x86_64"},"macAddresses":["AA:BB:CC:DD:EE:FF"],"serialNumbers":[],"metadata":{"inventario":{"cpu":"i7","ramGb":16}},"registeredBy":"diag-test"}' + HTTP=$(curl -sS -o resp.json -w "%{http_code}" -H 'Content-Type: application/json' -d "$DATA" https://tickets.esdrasrenan.com.br/api/machines/register || true) + echo "Register HTTP=$HTTP" && tail -c 400 resp.json || true diff --git a/.github/workflows.disabled/desktop-release.yml b/.github/workflows.disabled/desktop-release.yml new file mode 100644 index 0000000..d0b3c21 --- /dev/null +++ b/.github/workflows.disabled/desktop-release.yml @@ -0,0 +1,67 @@ +name: Desktop Release (Tauri) + +on: + workflow_dispatch: + push: + tags: + - 'desktop-v*' + +permissions: + contents: write + +jobs: + build: + name: Build ${{ matrix.platform }} + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - platform: linux + runner: ubuntu-latest + - platform: windows + runner: windows-latest + - platform: macos + runner: macos-latest + defaults: + run: + shell: bash + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Enable Corepack + run: corepack enable && corepack prepare pnpm@10.20.0 --activate + + - name: Install Rust (stable) + uses: dtolnay/rust-toolchain@stable + + - name: Install Linux deps + if: matrix.platform == 'linux' + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev libssl-dev build-essential curl wget file + + - name: Install pnpm deps + run: pnpm -C apps/desktop install --frozen-lockfile + + + - name: Build desktop + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} + TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + VITE_APP_URL: https://tickets.esdrasrenan.com.br + VITE_API_BASE_URL: https://tickets.esdrasrenan.com.br + run: pnpm -C apps/desktop tauri build + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: desktop-${{ matrix.platform }} + path: apps/desktop/src-tauri/target/release/bundle diff --git a/.github/workflows.disabled/quality-checks.yml b/.github/workflows.disabled/quality-checks.yml new file mode 100644 index 0000000..a14a2d7 --- /dev/null +++ b/.github/workflows.disabled/quality-checks.yml @@ -0,0 +1,62 @@ +name: Quality Checks + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + lint-test-build: + name: Lint, Test and Build + runs-on: ubuntu-latest + env: + BETTER_AUTH_SECRET: test-secret + NEXT_PUBLIC_APP_URL: http://localhost:3000 + BETTER_AUTH_URL: http://localhost:3000 + NEXT_PUBLIC_CONVEX_URL: http://localhost:3210 + DATABASE_URL: file:./prisma/db.dev.sqlite + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.1 + + - name: Verify Bun + run: bun --version + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Cache Next.js build cache + uses: actions/cache@v4 + with: + path: | + ${{ github.workspace }}/.next/cache + key: ${{ runner.os }}-nextjs-${{ hashFiles('pnpm-lock.yaml', 'bun.lock') }}-${{ hashFiles('**/*.{js,jsx,ts,tsx}') }} + restore-keys: | + ${{ runner.os }}-nextjs-${{ hashFiles('pnpm-lock.yaml', 'bun.lock') }}- + + - name: Generate Prisma client + env: + PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING: "1" + run: bun run prisma:generate + + - name: Lint + run: bun run lint + + - name: Test + run: bun test + + - name: Build + run: bun run build:bun diff --git a/.gitignore b/.gitignore index f3b0871..30d6e0c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,73 @@ -# Root ignore for monorepo -web/node_modules/ -web/.next/ -web/.turbo/ -web/out/ -web/.env.local -web/.env* -.DS_Store -Thumbs.db +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# React Email +/.react-email/ +/emails/out/ + +# production +/build + +# misc +.DS_Store +*.pem +*.sqlite +# external experiments +nova-calendar-main/ + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* +!.env.example +!apps/desktop/.env.example + +# Accidental Windows duplicate downloads (e.g., "env (1)") +env (*) +env (1) + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# backups locais +.archive/ + +# arquivos locais temporários +Captura de tela *.png +Screenshot*.png +# Ignore NTFS ADS streams accidentally committed from Windows downloads +*:*Zone.Identifier +*:\:Zone.Identifier +# Infrastructure secrets +.ci.env + +# ferramentas externas +rustdesk/ + +# Prisma generated files +src/generated/ +apps/desktop/service/target/ diff --git a/Dockerfile.prod b/Dockerfile.prod new file mode 100644 index 0000000..bb79ec4 --- /dev/null +++ b/Dockerfile.prod @@ -0,0 +1,29 @@ +# Runtime image with Node 22 + Bun 1.3.4 and build toolchain preinstalled +FROM node:22-bullseye-slim + +ENV BUN_INSTALL=/root/.bun +ENV PATH="$BUN_INSTALL/bin:$PATH" + +RUN apt-get update -y \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + gnupg \ + unzip \ + build-essential \ + python3 \ + make \ + pkg-config \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Install Bun 1.3.4 +RUN curl -fsSL https://bun.sh/install \ + | bash -s -- bun-v1.3.4 \ + && ln -sf /root/.bun/bin/bun /usr/local/bin/bun \ + && ln -sf /root/.bun/bin/bun /usr/local/bin/bunx + +WORKDIR /app + +# We'll mount the app code at runtime; image just provides runtimes/toolchains. +CMD ["bash"] diff --git a/Inter,Manrope/Inter/Inter-Italic-VariableFont_opsz,wght.ttf b/Inter,Manrope/Inter/Inter-Italic-VariableFont_opsz,wght.ttf new file mode 100644 index 0000000..43ed4f5 Binary files /dev/null and b/Inter,Manrope/Inter/Inter-Italic-VariableFont_opsz,wght.ttf differ diff --git a/Inter,Manrope/Inter/Inter-VariableFont_opsz,wght.ttf b/Inter,Manrope/Inter/Inter-VariableFont_opsz,wght.ttf new file mode 100644 index 0000000..e31b51e Binary files /dev/null and b/Inter,Manrope/Inter/Inter-VariableFont_opsz,wght.ttf differ diff --git a/Inter,Manrope/Inter/OFL.txt b/Inter,Manrope/Inter/OFL.txt new file mode 100644 index 0000000..d05ec4b --- /dev/null +++ b/Inter,Manrope/Inter/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Inter,Manrope/Inter/README.txt b/Inter,Manrope/Inter/README.txt new file mode 100644 index 0000000..b92a417 --- /dev/null +++ b/Inter,Manrope/Inter/README.txt @@ -0,0 +1,118 @@ +Inter Variable Font +=================== + +This download contains Inter as both variable fonts and static fonts. + +Inter is a variable font with these axes: + opsz + wght + +This means all the styles are contained in these files: + Inter/Inter-VariableFont_opsz,wght.ttf + Inter/Inter-Italic-VariableFont_opsz,wght.ttf + +If your app fully supports variable fonts, you can now pick intermediate styles +that aren’t available as static fonts. Not all apps support variable fonts, and +in those cases you can use the static font files for Inter: + Inter/static/Inter_18pt-Thin.ttf + Inter/static/Inter_18pt-ExtraLight.ttf + Inter/static/Inter_18pt-Light.ttf + Inter/static/Inter_18pt-Regular.ttf + Inter/static/Inter_18pt-Medium.ttf + Inter/static/Inter_18pt-SemiBold.ttf + Inter/static/Inter_18pt-Bold.ttf + Inter/static/Inter_18pt-ExtraBold.ttf + Inter/static/Inter_18pt-Black.ttf + Inter/static/Inter_24pt-Thin.ttf + Inter/static/Inter_24pt-ExtraLight.ttf + Inter/static/Inter_24pt-Light.ttf + Inter/static/Inter_24pt-Regular.ttf + Inter/static/Inter_24pt-Medium.ttf + Inter/static/Inter_24pt-SemiBold.ttf + Inter/static/Inter_24pt-Bold.ttf + Inter/static/Inter_24pt-ExtraBold.ttf + Inter/static/Inter_24pt-Black.ttf + Inter/static/Inter_28pt-Thin.ttf + Inter/static/Inter_28pt-ExtraLight.ttf + Inter/static/Inter_28pt-Light.ttf + Inter/static/Inter_28pt-Regular.ttf + Inter/static/Inter_28pt-Medium.ttf + Inter/static/Inter_28pt-SemiBold.ttf + Inter/static/Inter_28pt-Bold.ttf + Inter/static/Inter_28pt-ExtraBold.ttf + Inter/static/Inter_28pt-Black.ttf + Inter/static/Inter_18pt-ThinItalic.ttf + Inter/static/Inter_18pt-ExtraLightItalic.ttf + Inter/static/Inter_18pt-LightItalic.ttf + Inter/static/Inter_18pt-Italic.ttf + Inter/static/Inter_18pt-MediumItalic.ttf + Inter/static/Inter_18pt-SemiBoldItalic.ttf + Inter/static/Inter_18pt-BoldItalic.ttf + Inter/static/Inter_18pt-ExtraBoldItalic.ttf + Inter/static/Inter_18pt-BlackItalic.ttf + Inter/static/Inter_24pt-ThinItalic.ttf + Inter/static/Inter_24pt-ExtraLightItalic.ttf + Inter/static/Inter_24pt-LightItalic.ttf + Inter/static/Inter_24pt-Italic.ttf + Inter/static/Inter_24pt-MediumItalic.ttf + Inter/static/Inter_24pt-SemiBoldItalic.ttf + Inter/static/Inter_24pt-BoldItalic.ttf + Inter/static/Inter_24pt-ExtraBoldItalic.ttf + Inter/static/Inter_24pt-BlackItalic.ttf + Inter/static/Inter_28pt-ThinItalic.ttf + Inter/static/Inter_28pt-ExtraLightItalic.ttf + Inter/static/Inter_28pt-LightItalic.ttf + Inter/static/Inter_28pt-Italic.ttf + Inter/static/Inter_28pt-MediumItalic.ttf + Inter/static/Inter_28pt-SemiBoldItalic.ttf + Inter/static/Inter_28pt-BoldItalic.ttf + Inter/static/Inter_28pt-ExtraBoldItalic.ttf + Inter/static/Inter_28pt-BlackItalic.ttf + +Get started +----------- + +1. Install the font files you want to use + +2. Use your app's font picker to view the font family and all the +available styles + +Learn more about variable fonts +------------------------------- + + https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts + https://variablefonts.typenetwork.com + https://medium.com/variable-fonts + +In desktop apps + + https://theblog.adobe.com/can-variable-fonts-illustrator-cc + https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts + +Online + + https://developers.google.com/fonts/docs/getting_started + https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide + https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts + +Installing fonts + + MacOS: https://support.apple.com/en-us/HT201749 + Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux + Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows + +Android Apps + + https://developers.google.com/fonts/docs/android + https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts + +License +------- +Please read the full license text (OFL.txt) to understand the permissions, +restrictions and requirements for usage, redistribution, and modification. + +You can use them in your products & projects – print or digital, +commercial or otherwise. + +This isn't legal advice, please consider consulting a lawyer and see the full +license for all details. diff --git a/Inter,Manrope/Inter/static/Inter_18pt-Black.ttf b/Inter,Manrope/Inter/static/Inter_18pt-Black.ttf new file mode 100644 index 0000000..89673de Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_18pt-Black.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_18pt-BlackItalic.ttf b/Inter,Manrope/Inter/static/Inter_18pt-BlackItalic.ttf new file mode 100644 index 0000000..b33602f Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_18pt-BlackItalic.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_18pt-Bold.ttf b/Inter,Manrope/Inter/static/Inter_18pt-Bold.ttf new file mode 100644 index 0000000..57704d1 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_18pt-Bold.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_18pt-BoldItalic.ttf b/Inter,Manrope/Inter/static/Inter_18pt-BoldItalic.ttf new file mode 100644 index 0000000..d53a199 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_18pt-BoldItalic.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_18pt-ExtraBold.ttf b/Inter,Manrope/Inter/static/Inter_18pt-ExtraBold.ttf new file mode 100644 index 0000000..e71c601 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_18pt-ExtraBold.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_18pt-ExtraBoldItalic.ttf b/Inter,Manrope/Inter/static/Inter_18pt-ExtraBoldItalic.ttf new file mode 100644 index 0000000..df45062 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_18pt-ExtraBoldItalic.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_18pt-ExtraLight.ttf b/Inter,Manrope/Inter/static/Inter_18pt-ExtraLight.ttf new file mode 100644 index 0000000..f9c6cfc Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_18pt-ExtraLight.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_18pt-ExtraLightItalic.ttf b/Inter,Manrope/Inter/static/Inter_18pt-ExtraLightItalic.ttf new file mode 100644 index 0000000..275f305 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_18pt-ExtraLightItalic.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_18pt-Italic.ttf b/Inter,Manrope/Inter/static/Inter_18pt-Italic.ttf new file mode 100644 index 0000000..14d3595 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_18pt-Italic.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_18pt-Light.ttf b/Inter,Manrope/Inter/static/Inter_18pt-Light.ttf new file mode 100644 index 0000000..acae361 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_18pt-Light.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_18pt-LightItalic.ttf b/Inter,Manrope/Inter/static/Inter_18pt-LightItalic.ttf new file mode 100644 index 0000000..f69e18b Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_18pt-LightItalic.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_18pt-Medium.ttf b/Inter,Manrope/Inter/static/Inter_18pt-Medium.ttf new file mode 100644 index 0000000..71d9017 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_18pt-Medium.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_18pt-MediumItalic.ttf b/Inter,Manrope/Inter/static/Inter_18pt-MediumItalic.ttf new file mode 100644 index 0000000..5c8c8b1 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_18pt-MediumItalic.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_18pt-Regular.ttf b/Inter,Manrope/Inter/static/Inter_18pt-Regular.ttf new file mode 100644 index 0000000..ce097c8 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_18pt-Regular.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_18pt-SemiBold.ttf b/Inter,Manrope/Inter/static/Inter_18pt-SemiBold.ttf new file mode 100644 index 0000000..053185e Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_18pt-SemiBold.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_18pt-SemiBoldItalic.ttf b/Inter,Manrope/Inter/static/Inter_18pt-SemiBoldItalic.ttf new file mode 100644 index 0000000..d9c9896 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_18pt-SemiBoldItalic.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_18pt-Thin.ttf b/Inter,Manrope/Inter/static/Inter_18pt-Thin.ttf new file mode 100644 index 0000000..e68ec47 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_18pt-Thin.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_18pt-ThinItalic.ttf b/Inter,Manrope/Inter/static/Inter_18pt-ThinItalic.ttf new file mode 100644 index 0000000..134e837 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_18pt-ThinItalic.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_24pt-Black.ttf b/Inter,Manrope/Inter/static/Inter_24pt-Black.ttf new file mode 100644 index 0000000..dbb1b3b Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_24pt-Black.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_24pt-BlackItalic.ttf b/Inter,Manrope/Inter/static/Inter_24pt-BlackItalic.ttf new file mode 100644 index 0000000..b89d61c Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_24pt-BlackItalic.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_24pt-Bold.ttf b/Inter,Manrope/Inter/static/Inter_24pt-Bold.ttf new file mode 100644 index 0000000..e974d96 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_24pt-Bold.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_24pt-BoldItalic.ttf b/Inter,Manrope/Inter/static/Inter_24pt-BoldItalic.ttf new file mode 100644 index 0000000..1c3d251 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_24pt-BoldItalic.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_24pt-ExtraBold.ttf b/Inter,Manrope/Inter/static/Inter_24pt-ExtraBold.ttf new file mode 100644 index 0000000..b775c08 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_24pt-ExtraBold.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_24pt-ExtraBoldItalic.ttf b/Inter,Manrope/Inter/static/Inter_24pt-ExtraBoldItalic.ttf new file mode 100644 index 0000000..3461a92 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_24pt-ExtraBoldItalic.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_24pt-ExtraLight.ttf b/Inter,Manrope/Inter/static/Inter_24pt-ExtraLight.ttf new file mode 100644 index 0000000..2ec6ca3 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_24pt-ExtraLight.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_24pt-ExtraLightItalic.ttf b/Inter,Manrope/Inter/static/Inter_24pt-ExtraLightItalic.ttf new file mode 100644 index 0000000..c634a5d Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_24pt-ExtraLightItalic.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_24pt-Italic.ttf b/Inter,Manrope/Inter/static/Inter_24pt-Italic.ttf new file mode 100644 index 0000000..1048b07 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_24pt-Italic.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_24pt-Light.ttf b/Inter,Manrope/Inter/static/Inter_24pt-Light.ttf new file mode 100644 index 0000000..1a2a6f2 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_24pt-Light.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_24pt-LightItalic.ttf b/Inter,Manrope/Inter/static/Inter_24pt-LightItalic.ttf new file mode 100644 index 0000000..ded5a75 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_24pt-LightItalic.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_24pt-Medium.ttf b/Inter,Manrope/Inter/static/Inter_24pt-Medium.ttf new file mode 100644 index 0000000..5c88739 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_24pt-Medium.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_24pt-MediumItalic.ttf b/Inter,Manrope/Inter/static/Inter_24pt-MediumItalic.ttf new file mode 100644 index 0000000..be091b1 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_24pt-MediumItalic.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_24pt-Regular.ttf b/Inter,Manrope/Inter/static/Inter_24pt-Regular.ttf new file mode 100644 index 0000000..6b088a7 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_24pt-Regular.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_24pt-SemiBold.ttf b/Inter,Manrope/Inter/static/Inter_24pt-SemiBold.ttf new file mode 100644 index 0000000..ceb8576 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_24pt-SemiBold.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_24pt-SemiBoldItalic.ttf b/Inter,Manrope/Inter/static/Inter_24pt-SemiBoldItalic.ttf new file mode 100644 index 0000000..6921df2 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_24pt-SemiBoldItalic.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_24pt-Thin.ttf b/Inter,Manrope/Inter/static/Inter_24pt-Thin.ttf new file mode 100644 index 0000000..3505b35 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_24pt-Thin.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_24pt-ThinItalic.ttf b/Inter,Manrope/Inter/static/Inter_24pt-ThinItalic.ttf new file mode 100644 index 0000000..a3e6feb Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_24pt-ThinItalic.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_28pt-Black.ttf b/Inter,Manrope/Inter/static/Inter_28pt-Black.ttf new file mode 100644 index 0000000..66a252f Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_28pt-Black.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_28pt-BlackItalic.ttf b/Inter,Manrope/Inter/static/Inter_28pt-BlackItalic.ttf new file mode 100644 index 0000000..3c8fdf9 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_28pt-BlackItalic.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_28pt-Bold.ttf b/Inter,Manrope/Inter/static/Inter_28pt-Bold.ttf new file mode 100644 index 0000000..14db994 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_28pt-Bold.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_28pt-BoldItalic.ttf b/Inter,Manrope/Inter/static/Inter_28pt-BoldItalic.ttf new file mode 100644 index 0000000..704b12b Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_28pt-BoldItalic.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_28pt-ExtraBold.ttf b/Inter,Manrope/Inter/static/Inter_28pt-ExtraBold.ttf new file mode 100644 index 0000000..6d87cae Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_28pt-ExtraBold.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_28pt-ExtraBoldItalic.ttf b/Inter,Manrope/Inter/static/Inter_28pt-ExtraBoldItalic.ttf new file mode 100644 index 0000000..1a56735 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_28pt-ExtraBoldItalic.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_28pt-ExtraLight.ttf b/Inter,Manrope/Inter/static/Inter_28pt-ExtraLight.ttf new file mode 100644 index 0000000..d42b3f5 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_28pt-ExtraLight.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_28pt-ExtraLightItalic.ttf b/Inter,Manrope/Inter/static/Inter_28pt-ExtraLightItalic.ttf new file mode 100644 index 0000000..90e2f20 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_28pt-ExtraLightItalic.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_28pt-Italic.ttf b/Inter,Manrope/Inter/static/Inter_28pt-Italic.ttf new file mode 100644 index 0000000..c2a143a Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_28pt-Italic.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_28pt-Light.ttf b/Inter,Manrope/Inter/static/Inter_28pt-Light.ttf new file mode 100644 index 0000000..5eeff3a Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_28pt-Light.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_28pt-LightItalic.ttf b/Inter,Manrope/Inter/static/Inter_28pt-LightItalic.ttf new file mode 100644 index 0000000..6b90b76 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_28pt-LightItalic.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_28pt-Medium.ttf b/Inter,Manrope/Inter/static/Inter_28pt-Medium.ttf new file mode 100644 index 0000000..00120fe Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_28pt-Medium.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_28pt-MediumItalic.ttf b/Inter,Manrope/Inter/static/Inter_28pt-MediumItalic.ttf new file mode 100644 index 0000000..7481e7b Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_28pt-MediumItalic.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_28pt-Regular.ttf b/Inter,Manrope/Inter/static/Inter_28pt-Regular.ttf new file mode 100644 index 0000000..855b6f4 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_28pt-Regular.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_28pt-SemiBold.ttf b/Inter,Manrope/Inter/static/Inter_28pt-SemiBold.ttf new file mode 100644 index 0000000..8b84efc Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_28pt-SemiBold.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_28pt-SemiBoldItalic.ttf b/Inter,Manrope/Inter/static/Inter_28pt-SemiBoldItalic.ttf new file mode 100644 index 0000000..2e22c5a Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_28pt-SemiBoldItalic.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_28pt-Thin.ttf b/Inter,Manrope/Inter/static/Inter_28pt-Thin.ttf new file mode 100644 index 0000000..94e6108 Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_28pt-Thin.ttf differ diff --git a/Inter,Manrope/Inter/static/Inter_28pt-ThinItalic.ttf b/Inter,Manrope/Inter/static/Inter_28pt-ThinItalic.ttf new file mode 100644 index 0000000..d3d44cd Binary files /dev/null and b/Inter,Manrope/Inter/static/Inter_28pt-ThinItalic.ttf differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..47cd254 --- /dev/null +++ b/README.md @@ -0,0 +1,131 @@ +## Sistema de Chamados + +Aplicacao **Next.js 16 (App Router)** com **React 19**, **Convex** e **Better Auth** para gestao de tickets da Rever. A stack ainda inclui **Prisma 7** (PostgreSQL), **Tailwind** e **Turbopack** como bundler padrao (webpack permanece disponivel como fallback). Todo o codigo-fonte fica na raiz do monorepo seguindo as convencoes do App Router. + +## Requisitos + +- Bun >= 1.3 (recomendado 1.3.1). Após instalar via script oficial, adicione `export PATH="$HOME/.bun/bin:$PATH"` ao seu shell (ex.: `.bashrc`) para ter `bun` disponível globalmente. +- Node.js >= 20 (necessário para ferramentas auxiliares como Prisma CLI e Next.js em modo fallback). +- CLI do Convex (`bunx convex dev` instalará automaticamente no primeiro uso, se ainda não estiver presente). +- GitHub Actions/autodeploy dependem dessas versões e do CLI do Convex disponível; use `npx convex --help` para confirmar. + +## Configuração rápida + +1. Instale as dependências: + ```bash + bun install + ``` +2. Ajuste o arquivo `.env` (ou crie a partir de `.env.example`) e confirme os valores de: + - `NEXT_PUBLIC_CONVEX_URL` (gerado pelo Convex Dev) + - `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`, `DATABASE_URL` (PostgreSQL, ex: `postgresql://postgres:dev@localhost:5432/sistema_chamados`) +3. Aplique as migrações e gere o client Prisma: + ```bash + bunx prisma migrate deploy + bun run prisma:generate + ``` +4. Popule usuários padrão do Better Auth: + ```bash + bun run auth:seed + ``` + > Sempre que trocar de máquina ou quiser “zerar” o ambiente local, basta repetir os passos 3 e 4 com a mesma `DATABASE_URL`. + +### Resetar rapidamente o ambiente local + +1. Suba um PostgreSQL local (Docker recomendado): + ```bash + docker run -d --name postgres-dev -p 5432:5432 -e POSTGRES_PASSWORD=dev -e POSTGRES_DB=sistema_chamados postgres:18 + ``` +2. Aplique as migracoes: + ```bash + bunx prisma migrate deploy + ``` +3. Recrie/garanta as contas padrao de login: + ```bash + bun run auth:seed + ``` +4. Suba o servidor normalmente com `bun run dev`. + +### Subir serviços locais + +- (Opcional) Para re-sincronizar manualmente as filas padrão, execute `bun run queues:ensure`. +- Em um terminal, rode o backend em tempo real do Convex com `bun run convex:dev:bun` (ou `bun run convex:dev` para o runtime Node). +- Em outro terminal, suba o frontend Next.js (Turbopack) com `bun run dev:bun` (`bun run dev:webpack` serve como fallback). +- Com o Convex rodando, acesse `http://localhost:3000/dev/seed` uma vez para popular dados de demonstração (tickets, usuários, comentários). + +> Se o CLI perguntar sobre configuração do projeto Convex, escolha criar um novo deployment local (opção padrão) e confirme. As credenciais são armazenadas em `.convex/` automaticamente. + +### Documentação +- Índice de docs: `docs/README.md` +- Operações (produção): `docs/OPERATIONS.md` (versão EN) e `docs/OPERACAO-PRODUCAO.md` (PT-BR) +- Guia de DEV: `docs/DEV.md` +- Testes automatizados (Vitest/Playwright): `docs/testes-vitest.md` +- Stack Swarm: `stack.yml` (roteado por Traefik, rede `traefik_public`). + +### Variáveis de ambiente + +- Exemplo na raiz: `.env.example` — copie para `.env` e preencha segredos. +- App Desktop: `apps/desktop/.env.example` — copie para `apps/desktop/.env` e ajuste `VITE_APP_URL`. +- Nunca faça commit de arquivos `.env` com valores reais (já ignorados em `.gitignore`). + +### Guia de DEV (Prisma, Auth e Desktop/Tauri) + +Para fluxos detalhados de desenvolvimento — banco de dados local (PostgreSQL/Prisma), seed do Better Auth, ajustes do Prisma CLI no DEV e build do Desktop (Tauri) — consulte `docs/DEV.md`. + +## Scripts úteis + +- `bun run dev:bun` — padrão atual para o Next.js com runtime Bun (`bun run dev:webpack` permanece como fallback). +- `bun run convex:dev:bun` — runtime Bun para o Convex (`bun run convex:dev` mantém o fluxo antigo usando Node). +- `bun run build:bun` / `bun run start:bun` — build e serve com Bun usando Turbopack (padrão atual). +- `bun run dev:webpack` — fallback do Next.js em modo desenvolvimento (webpack). +- `bun run lint` — ESLint com as regras do projeto. +- `bun test` — suíte de testes unitários usando o runner do Bun (o teste de screenshot fica automaticamente ignorado se o matcher não existir). +- `bun run build` — executa `next build --turbopack` (runtime Node, caso prefira evitar o `--bun`). +- `bun run build:webpack` — executa `next build --webpack` como fallback oficial. +- `bun run auth:seed` — atualiza/cria contas padrao do Better Auth (credenciais em `agents.md`). +- `bunx prisma migrate deploy` — aplica migracoes ao banco PostgreSQL. +- `bun run convex:dev` — roda o Convex em modo desenvolvimento com Node, gerando tipos em `convex/_generated`. + +## Transferir dispositivo entre colaboradores + +Quando uma dispositivo trocar de responsável: + +1. Abra `Admin > Dispositivos`, selecione o equipamento e clique em **Resetar agente**. +2. No equipamento, execute o reset local do agente (`rever-agent reset` ou reinstale o serviço) e reprovisione com o código da empresa. +3. Após o agente gerar um novo token, associe a dispositivo ao novo colaborador no painel. + +Sem o reset de agente, o Convex reaproveita o token anterior e o inventário continua vinculado ao usuário antigo. + +## Estrutura principal + +- `app/` dentro de `src/` — rotas e layouts do Next.js (App Router). +- `components/` — componentes reutilizáveis (UI, formulários, layouts). +- `convex/` — queries, mutations e seeds do Convex. +- `prisma/` — schema e migracoes do Prisma (PostgreSQL). +- `scripts/` — utilitários em Node para sincronização e seeds adicionais. +- `agents.md` — guia operacional e contexto funcional (em PT-BR). +- `PROXIMOS_PASSOS.md` — backlog de melhorias futuras. + +## Credenciais de demonstração + +Após executar `bun run auth:seed`, as credenciais padrão ficam disponíveis conforme descrito em `agents.md` (seção “Credenciais padrão”). Ajuste variáveis `SEED_USER_*` se precisar sobrepor usuários ou senhas durante o seed. + +## Próximos passos + +Consulte `PROXIMOS_PASSOS.md` para acompanhar o backlog funcional e o progresso das iniciativas planejadas. + +### Executar com Bun + +- `bun install` é o fluxo padrão (o arquivo `bun.lock` deve ser versionado; use `bun install --frozen-lockfile` em CI). +- `bun run dev:bun`, `bun run convex:dev:bun`, `bun run build:bun` e `bun run start:bun` já estão configurados; internamente executam `bun run --bun + + +
+ + diff --git a/apps/desktop/package.json b/apps/desktop/package.json new file mode 100644 index 0000000..1c403b7 --- /dev/null +++ b/apps/desktop/package.json @@ -0,0 +1,37 @@ +{ + "name": "appsdesktop", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "tauri": "node ./scripts/tauri-with-stub.mjs", + "gen:icon": "node ./scripts/build-icon.mjs", + "build:service": "cd service && cargo build --release", + "build:all": "bun run build:service && bun run tauri build" + }, + "dependencies": { + "@radix-ui/react-scroll-area": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.13", + "@tauri-apps/api": "^2.9.1", + "@tauri-apps/plugin-dialog": "^2.4.2", + "@tauri-apps/plugin-opener": "^2", + "@tauri-apps/plugin-process": "^2", + "@tauri-apps/plugin-store": "^2", + "@tauri-apps/plugin-updater": "^2", + "convex": "^1.31.0", + "lucide-react": "^0.544.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@tauri-apps/cli": "^2", + "@vitejs/plugin-react": "^4.3.4", + "baseline-browser-mapping": "^2.9.2", + "png-to-ico": "^3.0.1", + "typescript": "~5.6.2", + "vite": "^6.0.3" + } +} diff --git a/apps/desktop/public/latest.json b/apps/desktop/public/latest.json new file mode 100644 index 0000000..8faf2f3 --- /dev/null +++ b/apps/desktop/public/latest.json @@ -0,0 +1,11 @@ +{ + "version": "0.1.6", + "notes": "Correções e melhorias do desktop", + "pub_date": "2025-10-14T12:00:00Z", + "platforms": { + "windows-x86_64": { + "signature": "ZFc1MGNuVnpkR1ZrSUdOdmJXMWxiblE2SUhOcFoyNWhkSFZ5WlNCbWNtOXRJSFJoZFhKcElITmxZM0psZENCclpYa0tVbFZVZDNFeFUwRlJRalJVUjJOU1NqUnpTVmhXU1ZoeVUwZElNSGxETW5KSE1FTnBWa3BWU1dzelVYVlRNV1JTV0Vrdk1XMUZVa0Z3YTBWc2QySnZhVnBxUWs5bVoyODNNbEZaYUZsMFVHTlRLMUFyT0hJMVdGZ3lWRkZYT1V3ekwzZG5QUXAwY25WemRHVmtJR052YlcxbGJuUTZJSFJwYldWemRHRnRjRG94TnpZd016azVOVEkzQ1dacGJHVTZVbUYyWlc1Zk1DNHhMalZmZURZMExYTmxkSFZ3TG1WNFpRcHdkME15THpOVlZtUXpiSG9yZGpRd1pFZHFhV1JvVkZCb0wzVnNabWh1ZURJdmFtUlZOalEwTkRSVVdVY3JUVGhLTUdrNU5scFNUSFZVWkRsc1lYVTJUR2dyWTNWeWJuWTVhRGh3ZVVnM1dFWjVhSFZDUVQwOUNnPT0=", + "url": "https://github.com/esdrasrenan/sistema-de-chamados/raw/main/apps/desktop/public/releases/Raven_0.1.6_x64-setup.exe" + } + } +} diff --git a/apps/desktop/public/logo-raven.png b/apps/desktop/public/logo-raven.png new file mode 100644 index 0000000..62b264e Binary files /dev/null and b/apps/desktop/public/logo-raven.png differ diff --git a/apps/desktop/public/releases/Raven_0.1.6_x64-setup.exe b/apps/desktop/public/releases/Raven_0.1.6_x64-setup.exe new file mode 100644 index 0000000..2d38474 Binary files /dev/null and b/apps/desktop/public/releases/Raven_0.1.6_x64-setup.exe differ diff --git a/apps/desktop/public/releases/Raven_0.1.6_x64-setup.exe.sig b/apps/desktop/public/releases/Raven_0.1.6_x64-setup.exe.sig new file mode 100644 index 0000000..3ce4efb --- /dev/null +++ b/apps/desktop/public/releases/Raven_0.1.6_x64-setup.exe.sig @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVUd3ExU0FRQjRUR2NSSjRzSVhWSVhyU0dIMHlDMnJHMENpVkpVSWszUXVTMWRSWEkvMW1FUkFwa0Vsd2JvaVpqQk9mZ283MlFZaFl0UGNTK1ArOHI1WFgyVFFXOUwzL3dnPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzYwMzk5NTI3CWZpbGU6UmF2ZW5fMC4xLjVfeDY0LXNldHVwLmV4ZQpwd0MyLzNVVmQzbHordjQwZEdqaWRoVFBoL3VsZmhueDIvamRVNjQ0NDRUWUcrTThKMGk5NlpSTHVUZDlsYXU2TGgrY3VybnY5aDhweUg3WEZ5aHVCQT09Cg== \ No newline at end of file diff --git a/apps/desktop/scripts/build-icon.mjs b/apps/desktop/scripts/build-icon.mjs new file mode 100644 index 0000000..ffb3649 --- /dev/null +++ b/apps/desktop/scripts/build-icon.mjs @@ -0,0 +1,38 @@ +#!/usr/bin/env node +import { promises as fs } from 'node:fs' +import path from 'node:path' +import pngToIco from 'png-to-ico' + +async function fileExists(p) { + try { await fs.access(p); return true } catch { return false } +} + +async function main() { + const root = path.resolve(process.cwd(), 'src-tauri', 'icons') + // Inclua apenas tamanhos suportados pelo NSIS (até 256px). + // Evite 512px para não gerar ICO inválido para o instalador. + const candidates = [ + 'icon-256.png', // preferencial + '128x128@2x.png', // alias de 256 + 'icon-128.png', + 'icon-64.png', + 'icon-32.png', + ] + const sources = [] + for (const name of candidates) { + const p = path.join(root, name) + if (await fileExists(p)) sources.push(p) + } + if (sources.length === 0) { + console.error('[gen:icon] Nenhuma imagem base encontrada em src-tauri/icons') + process.exit(1) + } + + console.log('[gen:icon] Gerando icon.ico a partir de:', sources.map((s) => path.basename(s)).join(', ')) + const buffer = await pngToIco(sources) + const outPath = path.join(root, 'icon.ico') + await fs.writeFile(outPath, buffer) + console.log('[gen:icon] Escrito:', outPath) +} + +main().catch((err) => { console.error(err); process.exit(1) }) diff --git a/apps/desktop/scripts/generate_icon_assets.py b/apps/desktop/scripts/generate_icon_assets.py new file mode 100644 index 0000000..73b0eec --- /dev/null +++ b/apps/desktop/scripts/generate_icon_assets.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +""" +Generate icon PNGs/ICO for the desktop installer using the high-resolution Raven artwork. + +The script reads the square logo (`logo-raven-fund-azul.png`) and resizes it to the +target sizes with a simple bilinear filter implemented with the Python standard library, +avoiding additional dependencies. +""" + +from __future__ import annotations + +import math +import struct +import zlib +from binascii import crc32 +from pathlib import Path + +ICON_DIR = Path(__file__).resolve().parents[1] / "src-tauri" / "icons" +BASE_IMAGE = ICON_DIR / "logo-raven-fund-azul.png" +TARGET_SIZES = [32, 64, 128, 256, 512] + + +def read_png(path: Path) -> tuple[int, int, list[list[tuple[int, int, int, int]]]]: + data = path.read_bytes() + if not data.startswith(b"\x89PNG\r\n\x1a\n"): + raise ValueError(f"{path} is not a PNG") + pos = 8 + width = height = bit_depth = color_type = None + compressed_parts = [] + while pos < len(data): + length = struct.unpack(">I", data[pos : pos + 4])[0] + pos += 4 + ctype = data[pos : pos + 4] + pos += 4 + chunk = data[pos : pos + length] + pos += length + pos += 4 # CRC + if ctype == b"IHDR": + width, height, bit_depth, color_type, _, _, _ = struct.unpack(">IIBBBBB", chunk) + if bit_depth != 8 or color_type not in (2, 6): + raise ValueError("Only 8-bit RGB/RGBA PNGs are supported") + elif ctype == b"IDAT": + compressed_parts.append(chunk) + elif ctype == b"IEND": + break + if width is None or height is None or bit_depth is None or color_type is None: + raise ValueError("PNG missing IHDR chunk") + + raw = zlib.decompress(b"".join(compressed_parts)) + bpp = 4 if color_type == 6 else 3 + stride = width * bpp + rows = [] + idx = 0 + prev = bytearray(stride) + for _ in range(height): + filter_type = raw[idx] + idx += 1 + row = bytearray(raw[idx : idx + stride]) + idx += stride + if filter_type == 1: + for i in range(stride): + left = row[i - bpp] if i >= bpp else 0 + row[i] = (row[i] + left) & 0xFF + elif filter_type == 2: + for i in range(stride): + row[i] = (row[i] + prev[i]) & 0xFF + elif filter_type == 3: + for i in range(stride): + left = row[i - bpp] if i >= bpp else 0 + up = prev[i] + row[i] = (row[i] + ((left + up) // 2)) & 0xFF + elif filter_type == 4: + for i in range(stride): + left = row[i - bpp] if i >= bpp else 0 + up = prev[i] + up_left = prev[i - bpp] if i >= bpp else 0 + p = left + up - up_left + pa = abs(p - left) + pb = abs(p - up) + pc = abs(p - up_left) + if pa <= pb and pa <= pc: + pr = left + elif pb <= pc: + pr = up + else: + pr = up_left + row[i] = (row[i] + pr) & 0xFF + elif filter_type not in (0,): + raise ValueError(f"Unsupported PNG filter type {filter_type}") + rows.append(bytes(row)) + prev[:] = row + + pixels: list[list[tuple[int, int, int, int]]] = [] + for row in rows: + if color_type == 6: + pixels.append([tuple(row[i : i + 4]) for i in range(0, len(row), 4)]) + else: + pixels.append([tuple(row[i : i + 3] + b"\xff") for i in range(0, len(row), 3)]) + return width, height, pixels + + +def write_png(path: Path, width: int, height: int, pixels: list[list[tuple[int, int, int, int]]]) -> None: + raw = bytearray() + for row in pixels: + raw.append(0) # filter type 0 + for r, g, b, a in row: + raw.extend((r & 0xFF, g & 0xFF, b & 0xFF, a & 0xFF)) + compressed = zlib.compress(raw, level=9) + + def chunk(name: bytes, payload: bytes) -> bytes: + return ( + struct.pack(">I", len(payload)) + + name + + payload + + struct.pack(">I", crc32(name + payload) & 0xFFFFFFFF) + ) + + ihdr = struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0) + out = bytearray(b"\x89PNG\r\n\x1a\n") + out += chunk(b"IHDR", ihdr) + out += chunk(b"IDAT", compressed) + out += chunk(b"IEND", b"") + path.write_bytes(out) + + +def bilinear_sample(pixels: list[list[tuple[int, int, int, int]]], x: float, y: float) -> tuple[int, int, int, int]: + height = len(pixels) + width = len(pixels[0]) + x = min(max(x, 0.0), width - 1.0) + y = min(max(y, 0.0), height - 1.0) + x0 = int(math.floor(x)) + y0 = int(math.floor(y)) + x1 = min(x0 + 1, width - 1) + y1 = min(y0 + 1, height - 1) + dx = x - x0 + dy = y - y0 + + def lerp(a: float, b: float, t: float) -> float: + return a + (b - a) * t + + result = [] + for channel in range(4): + c00 = pixels[y0][x0][channel] + c10 = pixels[y0][x1][channel] + c01 = pixels[y1][x0][channel] + c11 = pixels[y1][x1][channel] + top = lerp(c00, c10, dx) + bottom = lerp(c01, c11, dx) + result.append(int(round(lerp(top, bottom, dy)))) + return tuple(result) + + +def resize_image(pixels: list[list[tuple[int, int, int, int]]], target: int) -> list[list[tuple[int, int, int, int]]]: + src_height = len(pixels) + src_width = len(pixels[0]) + scale = min(target / src_width, target / src_height) + dest_width = max(1, int(round(src_width * scale))) + dest_height = max(1, int(round(src_height * scale))) + offset_x = (target - dest_width) // 2 + offset_y = (target - dest_height) // 2 + + background = (0, 0, 0, 0) + canvas = [[background for _ in range(target)] for _ in range(target)] + + for dy in range(dest_height): + src_y = (dy + 0.5) / scale - 0.5 + for dx in range(dest_width): + src_x = (dx + 0.5) / scale - 0.5 + canvas[offset_y + dy][offset_x + dx] = bilinear_sample(pixels, src_x, src_y) + return canvas + + +def build_ico(output: Path, png_paths: list[Path]) -> None: + entries = [] + offset = 6 + 16 * len(png_paths) + for path in png_paths: + data = path.read_bytes() + width, height, _ = read_png(path) + entries.append( + { + "width": width if width < 256 else 0, + "height": height if height < 256 else 0, + "size": len(data), + "offset": offset, + "payload": data, + } + ) + offset += len(data) + + header = struct.pack(" None: + width, height, pixels = read_png(BASE_IMAGE) + if width != height: + raise ValueError("Base icon must be square") + + generated: list[Path] = [] + for size in TARGET_SIZES: + resized = resize_image(pixels, size) + out_path = ICON_DIR / f"icon-{size}.png" + write_png(out_path, size, size, resized) + generated.append(out_path) + print(f"Generated {out_path} ({size}x{size})") + + largest = max(generated, key=lambda p: int(p.stem.split("-")[-1])) + (ICON_DIR / "icon.png").write_bytes(largest.read_bytes()) + + ico_sources = sorted( + [p for p in generated if int(p.stem.split("-")[-1]) <= 256], + key=lambda p: int(p.stem.split("-")[-1]), + ) + build_ico(ICON_DIR / "icon.ico", ico_sources) + print("icon.ico rebuilt.") + + +if __name__ == "__main__": + main() + diff --git a/apps/desktop/scripts/png_to_bmp.py b/apps/desktop/scripts/png_to_bmp.py new file mode 100644 index 0000000..cc5c11e --- /dev/null +++ b/apps/desktop/scripts/png_to_bmp.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +""" +Utility script to convert a PNG file (non-interlaced, 8-bit RGBA/RGB) +into a 24-bit BMP with optional letterboxing resize. + +The script is intentionally lightweight and relies only on Python's +standard library so it can run in constrained build environments. +""" + +from __future__ import annotations + +import argparse +import struct +import sys +import zlib +from pathlib import Path + + +PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n" + + +def parse_png(path: Path): + data = path.read_bytes() + if not data.startswith(PNG_SIGNATURE): + raise ValueError("Input is not a PNG file") + + idx = len(PNG_SIGNATURE) + width = height = bit_depth = color_type = None + compressed = bytearray() + interlaced = False + + while idx < len(data): + if idx + 8 > len(data): + raise ValueError("Corrupted PNG (unexpected EOF)") + length = struct.unpack(">I", data[idx : idx + 4])[0] + idx += 4 + chunk_type = data[idx : idx + 4] + idx += 4 + chunk_data = data[idx : idx + length] + idx += length + crc = data[idx : idx + 4] # noqa: F841 - crc skipped (validated by reader) + idx += 4 + + if chunk_type == b"IHDR": + width, height, bit_depth, color_type, compression, filter_method, interlace = struct.unpack( + ">IIBBBBB", chunk_data + ) + if compression != 0 or filter_method != 0: + raise ValueError("Unsupported PNG compression/filter method") + interlaced = interlace != 0 + elif chunk_type == b"IDAT": + compressed.extend(chunk_data) + elif chunk_type == b"IEND": + break + + if interlaced: + raise ValueError("Interlaced PNGs are not supported by this script") + if bit_depth != 8: + raise ValueError(f"Unsupported bit depth: {bit_depth}") + if color_type not in (2, 6): + raise ValueError(f"Unsupported color type: {color_type}") + + raw = zlib.decompress(bytes(compressed)) + bytes_per_pixel = 3 if color_type == 2 else 4 + stride = width * bytes_per_pixel + expected = (stride + 1) * height + if len(raw) != expected: + raise ValueError("Corrupted PNG data") + + # Apply PNG scanline filters + image = bytearray(width * height * 4) # Force RGBA output + prev_row = [0] * (stride) + + def paeth(a, b, c): + p = a + b - c + pa = abs(p - a) + pb = abs(p - b) + pc = abs(p - c) + if pa <= pb and pa <= pc: + return a + if pb <= pc: + return b + return c + + out_idx = 0 + for y in range(height): + offset = y * (stride + 1) + filter_type = raw[offset] + row = bytearray(raw[offset + 1 : offset + 1 + stride]) + if filter_type == 1: # Sub + for i in range(stride): + left = row[i - bytes_per_pixel] if i >= bytes_per_pixel else 0 + row[i] = (row[i] + left) & 0xFF + elif filter_type == 2: # Up + for i in range(stride): + row[i] = (row[i] + prev_row[i]) & 0xFF + elif filter_type == 3: # Average + for i in range(stride): + left = row[i - bytes_per_pixel] if i >= bytes_per_pixel else 0 + up = prev_row[i] + row[i] = (row[i] + ((left + up) >> 1)) & 0xFF + elif filter_type == 4: # Paeth + for i in range(stride): + left = row[i - bytes_per_pixel] if i >= bytes_per_pixel else 0 + up = prev_row[i] + up_left = prev_row[i - bytes_per_pixel] if i >= bytes_per_pixel else 0 + row[i] = (row[i] + paeth(left, up, up_left)) & 0xFF + elif filter_type != 0: + raise ValueError(f"Unsupported PNG filter type: {filter_type}") + + # Convert to RGBA + for x in range(width): + if color_type == 2: + r, g, b = row[x * 3 : x * 3 + 3] + a = 255 + else: + r, g, b, a = row[x * 4 : x * 4 + 4] + image[out_idx : out_idx + 4] = bytes((r, g, b, a)) + out_idx += 4 + + prev_row = list(row) + + return width, height, image + + +def resize_with_letterbox(image, width, height, target_w, target_h, background, scale_factor=1.0): + if width == target_w and height == target_h and abs(scale_factor - 1.0) < 1e-6: + return image, width, height + + bg_r, bg_g, bg_b = background + base_scale = min(target_w / width, target_h / height) + base_scale *= scale_factor + base_scale = max(base_scale, 1 / max(width, height)) # avoid zero / collapse + scaled_w = max(1, int(round(width * base_scale))) + scaled_h = max(1, int(round(height * base_scale))) + + output = bytearray(target_w * target_h * 4) + # Fill background + for i in range(0, len(output), 4): + output[i : i + 4] = bytes((bg_r, bg_g, bg_b, 255)) + + offset_x = (target_w - scaled_w) // 2 + offset_y = (target_h - scaled_h) // 2 + + for y in range(scaled_h): + src_y = min(height - 1, int(round(y / base_scale))) + for x in range(scaled_w): + src_x = min(width - 1, int(round(x / base_scale))) + src_idx = (src_y * width + src_x) * 4 + dst_idx = ((y + offset_y) * target_w + (x + offset_x)) * 4 + output[dst_idx : dst_idx + 4] = image[src_idx : src_idx + 4] + + return output, target_w, target_h + + +def blend_to_rgb(image): + rgb = bytearray(len(image) // 4 * 3) + for i in range(0, len(image), 4): + r, g, b, a = image[i : i + 4] + if a == 255: + rgb[(i // 4) * 3 : (i // 4) * 3 + 3] = bytes((b, g, r)) # BMP stores BGR + else: + alpha = a / 255.0 + bg = (255, 255, 255) + rr = int(round(r * alpha + bg[0] * (1 - alpha))) + gg = int(round(g * alpha + bg[1] * (1 - alpha))) + bb = int(round(b * alpha + bg[2] * (1 - alpha))) + rgb[(i // 4) * 3 : (i // 4) * 3 + 3] = bytes((bb, gg, rr)) + return rgb + + +def write_bmp(path: Path, width: int, height: int, rgb: bytearray): + row_stride = (width * 3 + 3) & ~3 # align to 4 bytes + padding = row_stride - width * 3 + pixel_data = bytearray() + + for y in range(height - 1, -1, -1): + start = y * width * 3 + end = start + width * 3 + pixel_data.extend(rgb[start:end]) + if padding: + pixel_data.extend(b"\0" * padding) + + file_size = 14 + 40 + len(pixel_data) + header = struct.pack("<2sIHHI", b"BM", file_size, 0, 0, 14 + 40) + dib_header = struct.pack( + " tuple[int, int]: + if not data.startswith(PNG_SIGNATURE): + raise ValueError("All inputs must be PNG files.") + width, height = struct.unpack(">II", data[16:24]) + return width, height + + +def build_icon(png_paths: list[Path], output: Path) -> None: + png_data = [p.read_bytes() for p in png_paths] + entries = [] + offset = 6 + 16 * len(png_data) # icon header + entries + + for data in png_data: + width, height = read_png_dimensions(data) + entry = { + "width": width if width < 256 else 0, + "height": height if height < 256 else 0, + "colors": 0, + "reserved": 0, + "planes": 1, + "bit_count": 32, + "size": len(data), + "offset": offset, + "data": data, + } + entries.append(entry) + offset += entry["size"] + + header = struct.pack(" None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("output", type=Path) + parser.add_argument("inputs", nargs="+", type=Path) + args = parser.parse_args() + + if not args.inputs: + raise SystemExit("Provide at least one PNG input.") + + build_icon(args.inputs, args.output) + + +if __name__ == "__main__": + main() diff --git a/apps/desktop/scripts/tauri-with-stub.mjs b/apps/desktop/scripts/tauri-with-stub.mjs new file mode 100644 index 0000000..add717a --- /dev/null +++ b/apps/desktop/scripts/tauri-with-stub.mjs @@ -0,0 +1,56 @@ +import { spawn } from "node:child_process" +import { fileURLToPath } from "node:url" +import { dirname, resolve } from "node:path" +import { existsSync } from "node:fs" + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) +const appRoot = resolve(__dirname, "..") + +const pathKey = process.platform === "win32" ? "Path" : "PATH" +const currentPath = process.env[pathKey] ?? process.env[pathKey.toUpperCase()] ?? "" +const separator = process.platform === "win32" ? ";" : ":" +const stubDir = resolve(__dirname) + +process.env[pathKey] = [stubDir, currentPath].filter(Boolean).join(separator) +if (pathKey !== "PATH") { + process.env.PATH = process.env[pathKey] +} + +if (!process.env.TAURI_BUNDLE_TARGETS) { + if (process.platform === "linux") { + process.env.TAURI_BUNDLE_TARGETS = "deb rpm" + } else if (process.platform === "win32") { + process.env.TAURI_BUNDLE_TARGETS = "nsis" + } +} + +// Assinatura: fallback seguro para builds locais/CI. Em prod, pode sobrescrever por env. +if (!process.env.TAURI_SIGNING_PRIVATE_KEY) { + process.env.TAURI_SIGNING_PRIVATE_KEY = + "dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5WkhWOUtzd1BvV0ZlSjEvNzYwaHYxdEloNnV4cmZlNGhha1BNbmNtZEkrZ0FBQkFBQUFBQUFBQUFBQUlBQUFBQS9JbCtsd3VFbHN4empFRUNiU0dva1hKK3ZYUzE2S1V6Q1FhYkRUWGtGMTBkUmJodi9PaXVub3hEMisyTXJoYU5UeEdwZU9aMklacG9ualNWR1NaTm1PMVBpVXYrNTltZU1YOFdwYzdkOHd2STFTc0x4ZktpNXFENnFTdW0xNzY3WC9EcGlIRGFmK2c9Cg==" +} +if (!process.env.TAURI_SIGNING_PRIVATE_KEY_PASSWORD) { + process.env.TAURI_SIGNING_PRIVATE_KEY_PASSWORD = "revertech" +} + +const winTauriPath = resolve(appRoot, "node_modules", ".bin", "tauri.cmd") +const usingWinTauri = process.platform === "win32" && existsSync(winTauriPath) +const executable = process.platform === "win32" && usingWinTauri ? "cmd.exe" : "tauri" +const args = + process.platform === "win32" && usingWinTauri + ? ["/C", winTauriPath, ...process.argv.slice(2)] + : process.argv.slice(2) +const child = spawn(executable, args, { + stdio: "inherit", + shell: false, + cwd: appRoot, +}) + +child.on("exit", (code, signal) => { + if (signal) { + process.kill(process.pid, signal) + } else { + process.exit(code ?? 0) + } +}) diff --git a/apps/desktop/scripts/xdg-open b/apps/desktop/scripts/xdg-open new file mode 100644 index 0000000..b081d99 --- /dev/null +++ b/apps/desktop/scripts/xdg-open @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +# Minimal stub to satisfy tools that expect xdg-open during bundling. +# Fails silently when the real binary is unavailable. +if command -v xdg-open >/dev/null 2>&1; then + exec xdg-open "$@" +else + exit 0 +fi diff --git a/apps/desktop/service/Cargo.lock b/apps/desktop/service/Cargo.lock new file mode 100644 index 0000000..da860fc --- /dev/null +++ b/apps/desktop/service/Cargo.lock @@ -0,0 +1,1931 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cc" +version = "1.2.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "doctest-file" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "interprocess" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d941b405bd2322993887859a8ee6ac9134945a24ec5ec763a8a962fc64dfec2d" +dependencies = [ + "doctest-file", + "futures-core", + "libc", + "recvmsg", + "tokio", + "widestring", + "windows-sys 0.52.0", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "raven-service" +version = "0.1.0" +dependencies = [ + "chrono", + "interprocess", + "once_cell", + "parking_lot", + "reqwest", + "serde", + "serde_json", + "sha2", + "thiserror 1.0.69", + "tokio", + "tracing", + "tracing-subscriber", + "uuid", + "windows", + "windows-service", + "winreg", +] + +[[package]] +name = "recvmsg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "reqwest" +version = "0.12.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustls" +version = "0.23.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-service" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24d6bcc7f734a4091ecf8d7a64c5f7d7066f45585c1861eba06449909609c8a" +dependencies = [ + "bitflags", + "widestring", + "windows-sys 0.52.0", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +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", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "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", + "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_aarch64_gnullvm" +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.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.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.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.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.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.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 = "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.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/apps/desktop/service/Cargo.toml b/apps/desktop/service/Cargo.toml new file mode 100644 index 0000000..a1334d5 --- /dev/null +++ b/apps/desktop/service/Cargo.toml @@ -0,0 +1,70 @@ +[package] +name = "raven-service" +version = "0.1.0" +description = "Raven Windows Service - Executa operacoes privilegiadas para o Raven Desktop" +authors = ["Esdras Renan"] +edition = "2021" + +[[bin]] +name = "raven-service" +path = "src/main.rs" + +[dependencies] +# Windows Service +windows-service = "0.7" + +# Async runtime +tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "io-util", "net", "signal"] } + +# IPC via Named Pipes +interprocess = { version = "2", features = ["tokio"] } + +# Serialization +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +# Logging +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +# Windows Registry +winreg = "0.55" + +# Error handling +thiserror = "1.0" + +# HTTP client (para RustDesk) +reqwest = { version = "0.12", features = ["json", "rustls-tls", "blocking"], default-features = false } + +# Date/time +chrono = { version = "0.4", features = ["serde"] } + +# Crypto (para RustDesk ID) +sha2 = "0.10" + +# UUID para request IDs +uuid = { version = "1", features = ["v4"] } + +# Parking lot para locks +parking_lot = "0.12" + +# Once cell para singletons +once_cell = "1.19" + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.58", features = [ + "Win32_Foundation", + "Win32_Security", + "Win32_System_Services", + "Win32_System_Threading", + "Win32_System_Pipes", + "Win32_System_IO", + "Win32_System_SystemServices", + "Win32_Storage_FileSystem", +] } + +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 +strip = true diff --git a/apps/desktop/service/src/ipc.rs b/apps/desktop/service/src/ipc.rs new file mode 100644 index 0000000..26091b6 --- /dev/null +++ b/apps/desktop/service/src/ipc.rs @@ -0,0 +1,290 @@ +//! Modulo IPC - Servidor de Named Pipes +//! +//! Implementa comunicacao entre o Raven UI e o Raven Service +//! usando Named Pipes do Windows com protocolo JSON-RPC simplificado. + +use crate::{rustdesk, usb_policy}; +use serde::{Deserialize, Serialize}; +use std::io::{BufRead, BufReader, Write}; +use thiserror::Error; +use tracing::{debug, info, warn}; + +#[derive(Debug, Error)] +pub enum IpcError { + #[error("Erro de IO: {0}")] + Io(#[from] std::io::Error), + + #[error("Erro de serializacao: {0}")] + Json(#[from] serde_json::Error), +} + +/// Requisicao JSON-RPC simplificada +#[derive(Debug, Deserialize)] +pub struct Request { + pub id: String, + pub method: String, + #[serde(default)] + pub params: serde_json::Value, +} + +/// Resposta JSON-RPC simplificada +#[derive(Debug, Serialize)] +pub struct Response { + pub id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +#[derive(Debug, Serialize)] +pub struct ErrorResponse { + pub code: i32, + pub message: String, +} + +impl Response { + pub fn success(id: String, result: serde_json::Value) -> Self { + Self { + id, + result: Some(result), + error: None, + } + } + + pub fn error(id: String, code: i32, message: String) -> Self { + Self { + id, + result: None, + error: Some(ErrorResponse { code, message }), + } + } +} + +/// Inicia o servidor de Named Pipes +pub async fn run_server(pipe_name: &str) -> Result<(), IpcError> { + info!("Iniciando servidor IPC em: {}", pipe_name); + + loop { + match accept_connection(pipe_name).await { + Ok(()) => { + debug!("Conexao processada com sucesso"); + } + Err(e) => { + warn!("Erro ao processar conexao: {}", e); + } + } + } +} + +/// Aceita uma conexao e processa requisicoes +async fn accept_connection(pipe_name: &str) -> Result<(), IpcError> { + use windows::Win32::Foundation::INVALID_HANDLE_VALUE; + use windows::Win32::Security::{ + InitializeSecurityDescriptor, SetSecurityDescriptorDacl, + PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES, SECURITY_DESCRIPTOR, + }; + use windows::Win32::Storage::FileSystem::PIPE_ACCESS_DUPLEX; + use windows::Win32::System::Pipes::{ + ConnectNamedPipe, CreateNamedPipeW, DisconnectNamedPipe, + PIPE_READMODE_MESSAGE, PIPE_TYPE_MESSAGE, PIPE_UNLIMITED_INSTANCES, PIPE_WAIT, + }; + use windows::Win32::System::SystemServices::SECURITY_DESCRIPTOR_REVISION; + use windows::core::PCWSTR; + + // Cria o named pipe com seguranca que permite acesso a todos os usuarios + let pipe_name_wide: Vec = pipe_name.encode_utf16().chain(std::iter::once(0)).collect(); + + // Cria security descriptor com DACL nulo (permite acesso a todos) + let mut sd = SECURITY_DESCRIPTOR::default(); + unsafe { + let sd_ptr = PSECURITY_DESCRIPTOR(&mut sd as *mut _ as *mut _); + let _ = InitializeSecurityDescriptor(sd_ptr, SECURITY_DESCRIPTOR_REVISION); + // DACL nulo = acesso irrestrito + let _ = SetSecurityDescriptorDacl(sd_ptr, true, None, false); + } + + let sa = SECURITY_ATTRIBUTES { + nLength: std::mem::size_of::() as u32, + lpSecurityDescriptor: &mut sd as *mut _ as *mut _, + bInheritHandle: false.into(), + }; + + let pipe_handle = unsafe { + CreateNamedPipeW( + PCWSTR::from_raw(pipe_name_wide.as_ptr()), + PIPE_ACCESS_DUPLEX, + PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, + PIPE_UNLIMITED_INSTANCES, + 4096, // out buffer + 4096, // in buffer + 0, // default timeout + Some(&sa), // seguranca permissiva + ) + }; + + // Verifica se o handle e valido + if pipe_handle == INVALID_HANDLE_VALUE { + return Err(IpcError::Io(std::io::Error::last_os_error())); + } + + // Aguarda conexao de um cliente + info!("Aguardando conexao de cliente..."); + let connect_result = unsafe { + ConnectNamedPipe(pipe_handle, None) + }; + + if let Err(e) = connect_result { + // ERROR_PIPE_CONNECTED (535) significa que o cliente ja estava conectado + // o que e aceitavel + let error_code = e.code().0 as u32; + if error_code != 535 { + warn!("Erro ao aguardar conexao: {:?}", e); + } + } + + info!("Cliente conectado"); + + // Processa requisicoes do cliente + let result = process_client(pipe_handle); + + // Desconecta o cliente + unsafe { + let _ = DisconnectNamedPipe(pipe_handle); + } + + result +} + +/// Processa requisicoes de um cliente conectado +fn process_client(pipe_handle: windows::Win32::Foundation::HANDLE) -> Result<(), IpcError> { + use std::os::windows::io::{FromRawHandle, RawHandle}; + use std::fs::File; + + // Cria File handle a partir do pipe + let raw_handle = pipe_handle.0 as RawHandle; + let file = unsafe { File::from_raw_handle(raw_handle) }; + + let reader = BufReader::new(file.try_clone()?); + let mut writer = file; + + // Le linhas (cada linha e uma requisicao JSON) + for line in reader.lines() { + let line = match line { + Ok(l) => l, + Err(e) => { + if e.kind() == std::io::ErrorKind::BrokenPipe { + info!("Cliente desconectou"); + break; + } + return Err(e.into()); + } + }; + + if line.is_empty() { + continue; + } + + debug!("Requisicao recebida: {}", line); + + // Parse da requisicao + let response = match serde_json::from_str::(&line) { + Ok(request) => handle_request(request), + Err(e) => Response::error( + "unknown".to_string(), + -32700, + format!("Parse error: {}", e), + ), + }; + + // Serializa e envia resposta + let response_json = serde_json::to_string(&response)?; + debug!("Resposta: {}", response_json); + + writeln!(writer, "{}", response_json)?; + writer.flush()?; + } + + // IMPORTANTE: Nao fechar o handle aqui, pois DisconnectNamedPipe precisa dele + std::mem::forget(writer); + + Ok(()) +} + +/// Processa uma requisicao e retorna a resposta +fn handle_request(request: Request) -> Response { + info!("Processando metodo: {}", request.method); + + match request.method.as_str() { + "health_check" => handle_health_check(request.id), + "apply_usb_policy" => handle_apply_usb_policy(request.id, request.params), + "get_usb_policy" => handle_get_usb_policy(request.id), + "provision_rustdesk" => handle_provision_rustdesk(request.id, request.params), + "get_rustdesk_status" => handle_get_rustdesk_status(request.id), + _ => Response::error( + request.id, + -32601, + format!("Metodo nao encontrado: {}", request.method), + ), + } +} + +// ============================================================================= +// Handlers de Requisicoes +// ============================================================================= + +fn handle_health_check(id: String) -> Response { + Response::success( + id, + serde_json::json!({ + "status": "ok", + "service": "RavenService", + "version": env!("CARGO_PKG_VERSION"), + "timestamp": chrono::Utc::now().timestamp_millis() + }), + ) +} + +fn handle_apply_usb_policy(id: String, params: serde_json::Value) -> Response { + let policy = match params.get("policy").and_then(|p| p.as_str()) { + Some(p) => p, + None => { + return Response::error(id, -32602, "Parametro 'policy' e obrigatorio".to_string()) + } + }; + + match usb_policy::apply_policy(policy) { + Ok(result) => Response::success(id, serde_json::to_value(result).unwrap()), + Err(e) => Response::error(id, -32000, format!("Erro ao aplicar politica: {}", e)), + } +} + +fn handle_get_usb_policy(id: String) -> Response { + match usb_policy::get_current_policy() { + Ok(policy) => Response::success( + id, + serde_json::json!({ + "policy": policy + }), + ), + Err(e) => Response::error(id, -32000, format!("Erro ao obter politica: {}", e)), + } +} + +fn handle_provision_rustdesk(id: String, params: serde_json::Value) -> Response { + let config_string = params.get("config").and_then(|c| c.as_str()).map(String::from); + let password = params.get("password").and_then(|p| p.as_str()).map(String::from); + let machine_id = params.get("machineId").and_then(|m| m.as_str()).map(String::from); + + match rustdesk::ensure_rustdesk(config_string.as_deref(), password.as_deref(), machine_id.as_deref()) { + Ok(result) => Response::success(id, serde_json::to_value(result).unwrap()), + Err(e) => Response::error(id, -32000, format!("Erro ao provisionar RustDesk: {}", e)), + } +} + +fn handle_get_rustdesk_status(id: String) -> Response { + match rustdesk::get_status() { + Ok(status) => Response::success(id, serde_json::to_value(status).unwrap()), + Err(e) => Response::error(id, -32000, format!("Erro ao obter status: {}", e)), + } +} diff --git a/apps/desktop/service/src/main.rs b/apps/desktop/service/src/main.rs new file mode 100644 index 0000000..208e22c --- /dev/null +++ b/apps/desktop/service/src/main.rs @@ -0,0 +1,268 @@ +//! Raven Service - Servico Windows para operacoes privilegiadas +//! +//! Este servico roda como LocalSystem e executa operacoes que requerem +//! privilegios de administrador, como: +//! - Aplicar politicas de USB +//! - Provisionar e configurar RustDesk +//! - Modificar chaves de registro em HKEY_LOCAL_MACHINE +//! +//! O app Raven UI comunica com este servico via Named Pipes. + +mod ipc; +mod rustdesk; +mod usb_policy; + +use std::ffi::OsString; +use std::time::Duration; +use tracing::{error, info}; +use windows_service::{ + define_windows_service, + service::{ + ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, + ServiceType, + }, + service_control_handler::{self, ServiceControlHandlerResult}, + service_dispatcher, +}; + +const SERVICE_NAME: &str = "RavenService"; +const SERVICE_DISPLAY_NAME: &str = "Raven Desktop Service"; +const SERVICE_DESCRIPTION: &str = "Servico do Raven Desktop para operacoes privilegiadas (USB, RustDesk)"; +const PIPE_NAME: &str = r"\\.\pipe\RavenService"; + +define_windows_service!(ffi_service_main, service_main); + +fn main() -> Result<(), Box> { + // Configura logging + init_logging(); + + // Verifica argumentos de linha de comando + let args: Vec = std::env::args().collect(); + + if args.len() > 1 { + match args[1].as_str() { + "install" => { + install_service()?; + return Ok(()); + } + "uninstall" => { + uninstall_service()?; + return Ok(()); + } + "run" => { + // Modo de teste: roda sem registrar como servico + info!("Executando em modo de teste (nao como servico)"); + run_standalone()?; + return Ok(()); + } + _ => {} + } + } + + // Inicia como servico Windows + info!("Iniciando Raven Service..."); + service_dispatcher::start(SERVICE_NAME, ffi_service_main)?; + Ok(()) +} + +fn init_logging() { + use tracing_subscriber::{fmt, prelude::*, EnvFilter}; + + // Tenta criar diretorio de logs + let log_dir = std::env::var("PROGRAMDATA") + .map(|p| std::path::PathBuf::from(p).join("RavenService").join("logs")) + .unwrap_or_else(|_| std::path::PathBuf::from("C:\\ProgramData\\RavenService\\logs")); + + let _ = std::fs::create_dir_all(&log_dir); + + // Arquivo de log + let log_file = log_dir.join("service.log"); + let file = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&log_file) + .ok(); + + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("info")); + + if let Some(file) = file { + tracing_subscriber::registry() + .with(filter) + .with(fmt::layer().with_writer(file).with_ansi(false)) + .init(); + } else { + tracing_subscriber::registry() + .with(filter) + .with(fmt::layer()) + .init(); + } +} + +fn service_main(arguments: Vec) { + if let Err(e) = run_service(arguments) { + error!("Erro ao executar servico: {}", e); + } +} + +fn run_service(_arguments: Vec) -> Result<(), Box> { + info!("Servico iniciando..."); + + // Canal para shutdown + let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>(); + let shutdown_tx = std::sync::Arc::new(std::sync::Mutex::new(Some(shutdown_tx))); + + // Registra handler de controle do servico + let shutdown_tx_clone = shutdown_tx.clone(); + let status_handle = service_control_handler::register(SERVICE_NAME, move |control| { + match control { + ServiceControl::Stop | ServiceControl::Shutdown => { + info!("Recebido comando de parada"); + if let Ok(mut guard) = shutdown_tx_clone.lock() { + if let Some(tx) = guard.take() { + let _ = tx.send(()); + } + } + ServiceControlHandlerResult::NoError + } + ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, + _ => ServiceControlHandlerResult::NotImplemented, + } + })?; + + // Atualiza status para Running + status_handle.set_service_status(ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::Running, + controls_accepted: ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN, + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + })?; + + info!("Servico em execucao, aguardando conexoes..."); + + // Cria runtime Tokio + let runtime = tokio::runtime::Runtime::new()?; + + // Executa servidor IPC + runtime.block_on(async { + tokio::select! { + result = ipc::run_server(PIPE_NAME) => { + if let Err(e) = result { + error!("Erro no servidor IPC: {}", e); + } + } + _ = async { + let _ = shutdown_rx.await; + } => { + info!("Shutdown solicitado"); + } + } + }); + + // Atualiza status para Stopped + status_handle.set_service_status(ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::Stopped, + controls_accepted: ServiceControlAccept::empty(), + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + })?; + + info!("Servico parado"); + Ok(()) +} + +fn run_standalone() -> Result<(), Box> { + let runtime = tokio::runtime::Runtime::new()?; + + runtime.block_on(async { + info!("Servidor IPC iniciando em modo standalone..."); + + tokio::select! { + result = ipc::run_server(PIPE_NAME) => { + if let Err(e) = result { + error!("Erro no servidor IPC: {}", e); + } + } + _ = tokio::signal::ctrl_c() => { + info!("Ctrl+C recebido, encerrando..."); + } + } + }); + + Ok(()) +} + +fn install_service() -> Result<(), Box> { + use windows_service::{ + service::{ServiceAccess, ServiceErrorControl, ServiceInfo, ServiceStartType}, + service_manager::{ServiceManager, ServiceManagerAccess}, + }; + + info!("Instalando servico..."); + + let manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CREATE_SERVICE)?; + + let exe_path = std::env::current_exe()?; + + let service_info = ServiceInfo { + name: OsString::from(SERVICE_NAME), + display_name: OsString::from(SERVICE_DISPLAY_NAME), + service_type: ServiceType::OWN_PROCESS, + start_type: ServiceStartType::AutoStart, + error_control: ServiceErrorControl::Normal, + executable_path: exe_path, + launch_arguments: vec![], + dependencies: vec![], + account_name: None, // LocalSystem + account_password: None, + }; + + let service = manager.create_service(&service_info, ServiceAccess::CHANGE_CONFIG)?; + + // Define descricao + service.set_description(SERVICE_DESCRIPTION)?; + + info!("Servico instalado com sucesso: {}", SERVICE_NAME); + println!("Servico '{}' instalado com sucesso!", SERVICE_DISPLAY_NAME); + println!("Para iniciar: sc start {}", SERVICE_NAME); + + Ok(()) +} + +fn uninstall_service() -> Result<(), Box> { + use windows_service::{ + service::ServiceAccess, + service_manager::{ServiceManager, ServiceManagerAccess}, + }; + + info!("Desinstalando servico..."); + + let manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)?; + + let service = manager.open_service( + SERVICE_NAME, + ServiceAccess::STOP | ServiceAccess::DELETE | ServiceAccess::QUERY_STATUS, + )?; + + // Tenta parar o servico primeiro + let status = service.query_status()?; + if status.current_state != ServiceState::Stopped { + info!("Parando servico..."); + let _ = service.stop(); + std::thread::sleep(Duration::from_secs(2)); + } + + // Remove o servico + service.delete()?; + + info!("Servico desinstalado com sucesso"); + println!("Servico '{}' removido com sucesso!", SERVICE_DISPLAY_NAME); + + Ok(()) +} diff --git a/apps/desktop/service/src/rustdesk.rs b/apps/desktop/service/src/rustdesk.rs new file mode 100644 index 0000000..0df60aa --- /dev/null +++ b/apps/desktop/service/src/rustdesk.rs @@ -0,0 +1,846 @@ +//! Modulo RustDesk - Provisionamento e gerenciamento do RustDesk +//! +//! Gerencia a instalacao, configuracao e provisionamento do RustDesk. +//! Como o servico roda como LocalSystem, nao precisa de elevacao. + +use chrono::Utc; +use once_cell::sync::Lazy; +use parking_lot::Mutex; +use reqwest::blocking::Client; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::env; +use std::ffi::OsStr; +use std::fs::{self, File, OpenOptions}; +use std::io::{self, Write}; +use std::os::windows::process::CommandExt; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::thread; +use std::time::Duration; +use thiserror::Error; +use tracing::{error, info, warn}; + +const RELEASES_API: &str = "https://api.github.com/repos/rustdesk/rustdesk/releases/latest"; +const USER_AGENT: &str = "RavenService/1.0"; +const SERVER_HOST: &str = "rust.rever.com.br"; +const SERVER_KEY: &str = "0mxocQKmK6GvTZQYKgjrG9tlNkKOqf81gKgqwAmnZuI="; +const DEFAULT_PASSWORD: &str = "FMQ9MA>e73r.FI> = Lazy::new(|| Mutex::new(())); + +#[derive(Debug, Error)] +pub enum RustdeskError { + #[error("HTTP error: {0}")] + Http(#[from] reqwest::Error), + + #[error("I/O error: {0}")] + Io(#[from] io::Error), + + #[error("Release asset nao encontrado para Windows x86_64")] + AssetMissing, + + #[error("Falha ao executar comando {command}: status {status:?}")] + CommandFailed { command: String, status: Option }, + + #[error("Falha ao detectar ID do RustDesk")] + MissingId, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RustdeskResult { + pub id: String, + pub password: String, + pub installed_version: Option, + pub updated: bool, + pub last_provisioned_at: i64, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RustdeskStatus { + pub installed: bool, + pub running: bool, + pub id: Option, + pub version: Option, +} + +#[derive(Debug, Deserialize)] +struct ReleaseAsset { + name: String, + browser_download_url: String, +} + +#[derive(Debug, Deserialize)] +struct ReleaseResponse { + tag_name: String, + assets: Vec, +} + +/// Provisiona o RustDesk +pub fn ensure_rustdesk( + config_string: Option<&str>, + password_override: Option<&str>, + machine_id: Option<&str>, +) -> Result { + let _guard = PROVISION_MUTEX.lock(); + info!("Iniciando provisionamento do RustDesk"); + + // Prepara ACLs dos diretorios de servico + if let Err(e) = ensure_service_profiles_writable() { + warn!("Aviso ao preparar ACL: {}", e); + } + + // Le ID existente antes de qualquer limpeza + let preserved_remote_id = read_remote_id_from_profiles(); + if let Some(ref id) = preserved_remote_id { + info!("ID existente preservado: {}", id); + } + + let exe_path = detect_executable_path(); + let (installed_version, freshly_installed) = ensure_installed(&exe_path)?; + + info!( + "RustDesk {}: {}", + if freshly_installed { "instalado" } else { "ja presente" }, + exe_path.display() + ); + + // Para processos existentes + let _ = stop_rustdesk_processes(); + + // Limpa perfis apenas se instalacao fresca + if freshly_installed { + let _ = purge_existing_rustdesk_profiles(); + } + + // Aplica configuracao + if let Some(config) = config_string.filter(|c| !c.trim().is_empty()) { + if let Err(e) = run_with_args(&exe_path, &["--config", config]) { + warn!("Falha ao aplicar config inline: {}", e); + } + } else { + let config_path = write_config_files()?; + if let Err(e) = apply_config(&exe_path, &config_path) { + warn!("Falha ao aplicar config via CLI: {}", e); + } + } + + // Define senha + let password = password_override + .map(|v| v.trim().to_string()) + .filter(|v| !v.is_empty()) + .unwrap_or_else(|| DEFAULT_PASSWORD.to_string()); + + if let Err(e) = set_password(&exe_path, &password) { + warn!("Falha ao definir senha: {}", e); + } else { + let _ = ensure_password_files(&password); + let _ = propagate_password_profile(); + } + + // Define ID customizado + let custom_id = if let Some(ref existing_id) = preserved_remote_id { + if !freshly_installed { + Some(existing_id.clone()) + } else { + define_custom_id(&exe_path, machine_id) + } + } else { + define_custom_id(&exe_path, machine_id) + }; + + // Inicia servico + if let Err(e) = ensure_service_running(&exe_path) { + warn!("Falha ao iniciar servico: {}", e); + } + + // Obtem ID final + let final_id = match query_id_with_retries(&exe_path, 5) { + Ok(id) => id, + Err(_) => { + read_remote_id_from_profiles() + .or_else(|| custom_id.clone()) + .ok_or(RustdeskError::MissingId)? + } + }; + + // Garante ID em todos os arquivos + ensure_remote_id_files(&final_id); + + let version = query_version(&exe_path).ok().or(installed_version); + let last_provisioned_at = Utc::now().timestamp_millis(); + + info!("Provisionamento concluido. ID: {}, Versao: {:?}", final_id, version); + + Ok(RustdeskResult { + id: final_id, + password, + installed_version: version, + updated: freshly_installed, + last_provisioned_at, + }) +} + +/// Retorna status do RustDesk +pub fn get_status() -> Result { + let exe_path = detect_executable_path(); + let installed = exe_path.exists(); + + let running = if installed { + query_service_state().map(|s| s == "running").unwrap_or(false) + } else { + false + }; + + let id = if installed { + query_id(&exe_path).ok().or_else(read_remote_id_from_profiles) + } else { + None + }; + + let version = if installed { + query_version(&exe_path).ok() + } else { + None + }; + + Ok(RustdeskStatus { + installed, + running, + id, + version, + }) +} + +// ============================================================================= +// Funcoes Auxiliares +// ============================================================================= + +fn detect_executable_path() -> PathBuf { + let program_files = env::var("PROGRAMFILES").unwrap_or_else(|_| "C:/Program Files".to_string()); + Path::new(&program_files).join("RustDesk").join("rustdesk.exe") +} + +fn ensure_installed(exe_path: &Path) -> Result<(Option, bool), RustdeskError> { + if exe_path.exists() { + return Ok((None, false)); + } + + let cache_root = PathBuf::from(env::var("PROGRAMDATA").unwrap_or_else(|_| "C:/ProgramData".to_string())) + .join(CACHE_DIR_NAME); + fs::create_dir_all(&cache_root)?; + + let (installer_path, version_tag) = download_latest_installer(&cache_root)?; + run_installer(&installer_path)?; + thread::sleep(Duration::from_secs(20)); + + Ok((Some(version_tag), true)) +} + +fn download_latest_installer(cache_root: &Path) -> Result<(PathBuf, String), RustdeskError> { + let client = Client::builder() + .user_agent(USER_AGENT) + .timeout(Duration::from_secs(60)) + .build()?; + + let release: ReleaseResponse = client.get(RELEASES_API).send()?.error_for_status()?.json()?; + + let asset = release + .assets + .iter() + .find(|a| a.name.ends_with("x86_64.exe")) + .ok_or(RustdeskError::AssetMissing)?; + + let target_path = cache_root.join(&asset.name); + if target_path.exists() { + return Ok((target_path, release.tag_name)); + } + + info!("Baixando RustDesk: {}", asset.name); + let mut response = client.get(&asset.browser_download_url).send()?.error_for_status()?; + let mut output = File::create(&target_path)?; + response.copy_to(&mut output)?; + + Ok((target_path, release.tag_name)) +} + +fn run_installer(installer_path: &Path) -> Result<(), RustdeskError> { + let status = hidden_command(installer_path) + .arg("--silent-install") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status()?; + + if !status.success() { + return Err(RustdeskError::CommandFailed { + command: format!("{} --silent-install", installer_path.display()), + status: status.code(), + }); + } + Ok(()) +} + +fn program_data_config_dir() -> PathBuf { + PathBuf::from(env::var("PROGRAMDATA").unwrap_or_else(|_| "C:/ProgramData".to_string())) + .join("RustDesk") + .join("config") +} + +/// Retorna todos os diretorios AppData\Roaming\RustDesk\config de usuarios do sistema +/// Como o servico roda como LocalSystem, precisamos enumerar os profiles de usuarios +fn all_user_appdata_config_dirs() -> Vec { + let mut dirs = Vec::new(); + + // Enumera C:\Users\*\AppData\Roaming\RustDesk\config + let users_dir = Path::new("C:\\Users"); + if let Ok(entries) = fs::read_dir(users_dir) { + for entry in entries.flatten() { + let path = entry.path(); + // Ignora pastas de sistema + let name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); + if name == "Public" || name == "Default" || name == "Default User" || name == "All Users" { + continue; + } + let rustdesk_config = path.join("AppData").join("Roaming").join("RustDesk").join("config"); + // Verifica se o diretorio pai existe (usuario real) + if path.join("AppData").join("Roaming").exists() { + dirs.push(rustdesk_config); + } + } + } + + // Tambem tenta o APPDATA do ambiente (pode ser util em alguns casos) + if let Ok(appdata) = env::var("APPDATA") { + let path = Path::new(&appdata).join("RustDesk").join("config"); + if !dirs.contains(&path) { + dirs.push(path); + } + } + + dirs +} + +fn service_profile_dirs() -> Vec { + vec![ + PathBuf::from(LOCAL_SERVICE_CONFIG), + PathBuf::from(LOCAL_SYSTEM_CONFIG), + ] +} + +fn remote_id_directories() -> Vec { + let mut dirs = Vec::new(); + dirs.push(program_data_config_dir()); + dirs.extend(service_profile_dirs()); + dirs.extend(all_user_appdata_config_dirs()); + dirs +} + +fn write_config_files() -> Result { + let config_contents = format!( + r#"[options] +key = "{key}" +relay-server = "{host}" +custom-rendezvous-server = "{host}" +api-server = "https://{host}" +verification-method = "{verification}" +approve-mode = "{approve}" +"#, + host = SERVER_HOST, + key = SERVER_KEY, + verification = SECURITY_VERIFICATION_VALUE, + approve = SECURITY_APPROVE_MODE_VALUE, + ); + + let main_path = program_data_config_dir().join("RustDesk2.toml"); + write_file(&main_path, &config_contents)?; + + for service_dir in service_profile_dirs() { + let service_profile = service_dir.join("RustDesk2.toml"); + let _ = write_file(&service_profile, &config_contents); + } + + Ok(main_path) +} + +fn write_file(path: &Path, contents: &str) -> Result<(), io::Error> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path)?; + file.write_all(contents.as_bytes()) +} + +fn apply_config(exe_path: &Path, config_path: &Path) -> Result<(), RustdeskError> { + run_with_args(exe_path, &["--import-config", &config_path.to_string_lossy()]) +} + +fn set_password(exe_path: &Path, secret: &str) -> Result<(), RustdeskError> { + run_with_args(exe_path, &["--password", secret]) +} + +fn define_custom_id(exe_path: &Path, machine_id: Option<&str>) -> Option { + let value = machine_id.and_then(|raw| { + let trimmed = raw.trim(); + if trimmed.is_empty() { None } else { Some(trimmed) } + })?; + + let custom_id = derive_numeric_id(value); + if run_with_args(exe_path, &["--set-id", &custom_id]).is_ok() { + info!("ID deterministico definido: {}", custom_id); + Some(custom_id) + } else { + None + } +} + +fn derive_numeric_id(machine_id: &str) -> String { + let mut hasher = Sha256::new(); + hasher.update(machine_id.as_bytes()); + let hash = hasher.finalize(); + let mut bytes = [0u8; 8]; + bytes.copy_from_slice(&hash[..8]); + let value = u64::from_le_bytes(bytes); + let num = (value % 900_000_000) + 100_000_000; + format!("{:09}", num) +} + +fn ensure_service_running(exe_path: &Path) -> Result<(), RustdeskError> { + ensure_service_installed(exe_path)?; + let _ = run_sc(&["config", SERVICE_NAME, "start=", "auto"]); + let _ = run_sc(&["start", SERVICE_NAME]); + remove_rustdesk_autorun_artifacts(); + Ok(()) +} + +fn ensure_service_installed(exe_path: &Path) -> Result<(), RustdeskError> { + if run_sc(&["query", SERVICE_NAME]).is_ok() { + return Ok(()); + } + run_with_args(exe_path, &["--install-service"]) +} + +fn stop_rustdesk_processes() -> Result<(), RustdeskError> { + let _ = run_sc(&["stop", SERVICE_NAME]); + thread::sleep(Duration::from_secs(2)); + + let status = hidden_command("taskkill") + .args(["/F", "/T", "/IM", "rustdesk.exe"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status()?; + + if status.success() || matches!(status.code(), Some(128)) { + Ok(()) + } else { + Err(RustdeskError::CommandFailed { + command: "taskkill".into(), + status: status.code(), + }) + } +} + +fn purge_existing_rustdesk_profiles() -> Result<(), String> { + let files = [ + "RustDesk.toml", + "RustDesk_local.toml", + "RustDesk2.toml", + "password", + "passwd", + "passwd.txt", + ]; + + for dir in remote_id_directories() { + if !dir.exists() { + continue; + } + for name in files { + let path = dir.join(name); + if path.exists() { + let _ = fs::remove_file(&path); + } + } + } + Ok(()) +} + +fn ensure_password_files(secret: &str) -> Result<(), String> { + for dir in remote_id_directories() { + let password_path = dir.join("RustDesk.toml"); + let _ = write_toml_kv(&password_path, "password", secret); + + let local_path = dir.join("RustDesk_local.toml"); + let _ = write_toml_kv(&local_path, "verification-method", SECURITY_VERIFICATION_VALUE); + let _ = write_toml_kv(&local_path, "approve-mode", SECURITY_APPROVE_MODE_VALUE); + } + Ok(()) +} + +fn propagate_password_profile() -> io::Result { + // Encontra um diretorio de usuario que tenha arquivos de config + let user_dirs = all_user_appdata_config_dirs(); + let src_dir = user_dirs.iter().find(|d| d.join("RustDesk.toml").exists()); + + let Some(src_dir) = src_dir else { + // Se nenhum usuario tem config, usa ProgramData como fonte + let pd = program_data_config_dir(); + if !pd.join("RustDesk.toml").exists() { + return Ok(false); + } + return propagate_from_dir(&pd); + }; + + propagate_from_dir(src_dir) +} + +fn propagate_from_dir(src_dir: &Path) -> io::Result { + let propagation_files = ["RustDesk.toml", "RustDesk_local.toml", "RustDesk2.toml"]; + let mut propagated = false; + + for filename in propagation_files { + let src_path = src_dir.join(filename); + if !src_path.exists() { + continue; + } + + for dest_root in remote_id_directories() { + if dest_root == src_dir { + continue; // Nao copiar para si mesmo + } + let target_path = dest_root.join(filename); + if copy_overwrite(&src_path, &target_path).is_ok() { + propagated = true; + } + } + } + + Ok(propagated) +} + +fn ensure_remote_id_files(id: &str) { + for dir in remote_id_directories() { + let path = dir.join("RustDesk_local.toml"); + let _ = write_remote_id_value(&path, id); + } +} + +fn write_remote_id_value(path: &Path, id: &str) -> io::Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + let replacement = format!("remote_id = '{}'\n", id); + if let Ok(existing) = fs::read_to_string(path) { + let mut replaced = false; + let mut buffer = String::with_capacity(existing.len() + replacement.len()); + for line in existing.lines() { + if line.trim_start().starts_with("remote_id") { + buffer.push_str(&replacement); + replaced = true; + } else { + buffer.push_str(line); + buffer.push('\n'); + } + } + if !replaced { + buffer.push_str(&replacement); + } + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path)?; + file.write_all(buffer.as_bytes()) + } else { + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path)?; + file.write_all(replacement.as_bytes()) + } +} + +fn write_toml_kv(path: &Path, key: &str, value: &str) -> io::Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + let sanitized = value.replace('\\', "\\\\").replace('"', "\\\""); + let replacement = format!("{key} = \"{sanitized}\"\n"); + let existing = fs::read_to_string(path).unwrap_or_default(); + let mut replaced = false; + let mut buffer = String::with_capacity(existing.len() + replacement.len()); + for line in existing.lines() { + let trimmed = line.trim_start(); + if trimmed.starts_with(&format!("{key} ")) || trimmed.starts_with(&format!("{key}=")) { + buffer.push_str(&replacement); + replaced = true; + } else { + buffer.push_str(line); + buffer.push('\n'); + } + } + if !replaced { + buffer.push_str(&replacement); + } + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path)?; + file.write_all(buffer.as_bytes()) +} + +fn read_remote_id_from_profiles() -> Option { + for dir in remote_id_directories() { + for candidate in [dir.join("RustDesk_local.toml"), dir.join("RustDesk.toml")] { + if let Some(id) = read_remote_id_file(&candidate) { + if !id.is_empty() { + return Some(id); + } + } + } + } + None +} + +fn read_remote_id_file(path: &Path) -> Option { + let content = fs::read_to_string(path).ok()?; + for line in content.lines() { + if let Some(value) = parse_assignment(line, "remote_id") { + return Some(value); + } + } + None +} + +fn parse_assignment(line: &str, key: &str) -> Option { + let trimmed = line.trim(); + if !trimmed.starts_with(key) { + return None; + } + let (_, rhs) = trimmed.split_once('=')?; + let value = rhs.trim().trim_matches(|c| c == '\'' || c == '"'); + if value.is_empty() { + None + } else { + Some(value.to_string()) + } +} + +fn query_id_with_retries(exe_path: &Path, attempts: usize) -> Result { + for attempt in 0..attempts { + match query_id(exe_path) { + Ok(value) if !value.trim().is_empty() => return Ok(value), + _ => {} + } + if attempt + 1 < attempts { + thread::sleep(Duration::from_millis(800)); + } + } + Err(RustdeskError::MissingId) +} + +fn query_id(exe_path: &Path) -> Result { + let output = hidden_command(exe_path).arg("--get-id").output()?; + if !output.status.success() { + return Err(RustdeskError::CommandFailed { + command: format!("{} --get-id", exe_path.display()), + status: output.status.code(), + }); + } + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if stdout.is_empty() { + return Err(RustdeskError::MissingId); + } + Ok(stdout) +} + +fn query_version(exe_path: &Path) -> Result { + let output = hidden_command(exe_path).arg("--version").output()?; + if !output.status.success() { + return Err(RustdeskError::CommandFailed { + command: format!("{} --version", exe_path.display()), + status: output.status.code(), + }); + } + Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) +} + +fn query_service_state() -> Option { + let output = hidden_command("sc") + .args(["query", SERVICE_NAME]) + .output() + .ok()?; + + if !output.status.success() { + return None; + } + + let stdout = String::from_utf8_lossy(&output.stdout); + for line in stdout.lines() { + let lower = line.to_lowercase(); + if lower.contains("running") { + return Some("running".to_string()); + } + if lower.contains("stopped") { + return Some("stopped".to_string()); + } + } + None +} + +fn run_sc(args: &[&str]) -> Result<(), RustdeskError> { + let status = hidden_command("sc") + .args(args) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status()?; + if !status.success() { + return Err(RustdeskError::CommandFailed { + command: format!("sc {}", args.join(" ")), + status: status.code(), + }); + } + Ok(()) +} + +fn run_with_args(exe_path: &Path, args: &[&str]) -> Result<(), RustdeskError> { + let status = hidden_command(exe_path) + .args(args) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status()?; + if !status.success() { + return Err(RustdeskError::CommandFailed { + command: format!("{} {}", exe_path.display(), args.join(" ")), + status: status.code(), + }); + } + Ok(()) +} + +fn remove_rustdesk_autorun_artifacts() { + // Remove atalhos de inicializacao automatica + let mut startup_paths: Vec = Vec::new(); + if let Ok(appdata) = env::var("APPDATA") { + startup_paths.push( + Path::new(&appdata) + .join("Microsoft\\Windows\\Start Menu\\Programs\\Startup\\RustDesk.lnk"), + ); + } + startup_paths.push(PathBuf::from( + r"C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup\RustDesk.lnk", + )); + + for path in startup_paths { + if path.exists() { + let _ = fs::remove_file(&path); + } + } + + // Remove entradas de registro + for hive in ["HKCU", "HKLM"] { + let reg_path = format!(r"{}\Software\Microsoft\Windows\CurrentVersion\Run", hive); + let _ = hidden_command("reg") + .args(["delete", ®_path, "/v", "RustDesk", "/f"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + } +} + +fn ensure_service_profiles_writable() -> Result<(), String> { + for dir in service_profile_dirs() { + if !can_write_dir(&dir) { + fix_profile_acl(&dir)?; + } + } + Ok(()) +} + +fn can_write_dir(dir: &Path) -> bool { + if fs::create_dir_all(dir).is_err() { + return false; + } + let probe = dir.join(".raven_acl_probe"); + match OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&probe) + { + Ok(mut file) => { + if file.write_all(b"ok").is_err() { + let _ = fs::remove_file(&probe); + return false; + } + let _ = fs::remove_file(&probe); + true + } + Err(_) => false, + } +} + +fn fix_profile_acl(target: &Path) -> Result<(), String> { + let target_str = target.display().to_string(); + + // Como ja estamos rodando como LocalSystem, podemos usar takeown/icacls diretamente + let _ = hidden_command("takeown") + .args(["/F", &target_str, "/R", "/D", "Y"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + + let status = hidden_command("icacls") + .args([ + &target_str, + "/grant", + "*S-1-5-32-544:(OI)(CI)F", + "*S-1-5-19:(OI)(CI)F", + "*S-1-5-32-545:(OI)(CI)M", + "/T", + "/C", + "/Q", + ]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map_err(|e| format!("Erro ao executar icacls: {}", e))?; + + if status.success() { + Ok(()) + } else { + Err(format!("icacls retornou codigo {}", status.code().unwrap_or(-1))) + } +} + +fn copy_overwrite(src: &Path, dst: &Path) -> io::Result<()> { + if let Some(parent) = dst.parent() { + fs::create_dir_all(parent)?; + } + if dst.is_dir() { + fs::remove_dir_all(dst)?; + } else if dst.exists() { + fs::remove_file(dst)?; + } + fs::copy(src, dst)?; + Ok(()) +} + +fn hidden_command(program: impl AsRef) -> Command { + let mut cmd = Command::new(program); + cmd.creation_flags(CREATE_NO_WINDOW); + cmd +} diff --git a/apps/desktop/service/src/usb_policy.rs b/apps/desktop/service/src/usb_policy.rs new file mode 100644 index 0000000..ed8144d --- /dev/null +++ b/apps/desktop/service/src/usb_policy.rs @@ -0,0 +1,259 @@ +//! Modulo USB Policy - Controle de dispositivos USB +//! +//! Implementa o controle de armazenamento USB no Windows. +//! Como o servico roda como LocalSystem, nao precisa de elevacao. + +use serde::{Deserialize, Serialize}; +use std::io; +use thiserror::Error; +use tracing::{error, info, warn}; +use winreg::enums::*; +use winreg::RegKey; + +// GUID para Removable Storage Devices (Disk) +const REMOVABLE_STORAGE_GUID: &str = "{53f56307-b6bf-11d0-94f2-00a0c91efb8b}"; + +// Chaves de registro +const REMOVABLE_STORAGE_PATH: &str = r"Software\Policies\Microsoft\Windows\RemovableStorageDevices"; +const USBSTOR_PATH: &str = r"SYSTEM\CurrentControlSet\Services\USBSTOR"; +const STORAGE_POLICY_PATH: &str = r"SYSTEM\CurrentControlSet\Control\StorageDevicePolicies"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum UsbPolicy { + Allow, + BlockAll, + Readonly, +} + +impl UsbPolicy { + pub fn from_str(s: &str) -> Option { + match s.to_uppercase().as_str() { + "ALLOW" => Some(Self::Allow), + "BLOCK_ALL" => Some(Self::BlockAll), + "READONLY" => Some(Self::Readonly), + _ => None, + } + } + + pub fn as_str(&self) -> &'static str { + match self { + Self::Allow => "ALLOW", + Self::BlockAll => "BLOCK_ALL", + Self::Readonly => "READONLY", + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UsbPolicyResult { + pub success: bool, + pub policy: String, + pub error: Option, + pub applied_at: Option, +} + +#[derive(Error, Debug)] +pub enum UsbControlError { + #[error("Politica USB invalida: {0}")] + InvalidPolicy(String), + + #[error("Erro de registro do Windows: {0}")] + RegistryError(String), + + #[error("Permissao negada")] + PermissionDenied, + + #[error("Erro de I/O: {0}")] + Io(#[from] io::Error), +} + +/// Aplica uma politica de USB +pub fn apply_policy(policy_str: &str) -> Result { + let policy = UsbPolicy::from_str(policy_str) + .ok_or_else(|| UsbControlError::InvalidPolicy(policy_str.to_string()))?; + + let now = chrono::Utc::now().timestamp_millis(); + + info!("Aplicando politica USB: {:?}", policy); + + // 1. Aplicar Removable Storage Policy + apply_removable_storage_policy(policy)?; + + // 2. Aplicar USBSTOR + apply_usbstor_policy(policy)?; + + // 3. Aplicar WriteProtect se necessario + if policy == UsbPolicy::Readonly { + apply_write_protect(true)?; + } else { + apply_write_protect(false)?; + } + + // 4. Atualizar Group Policy (opcional) + if let Err(e) = refresh_group_policy() { + warn!("Falha ao atualizar group policy: {}", e); + } + + info!("Politica USB aplicada com sucesso: {:?}", policy); + + Ok(UsbPolicyResult { + success: true, + policy: policy.as_str().to_string(), + error: None, + applied_at: Some(now), + }) +} + +/// Retorna a politica USB atual +pub fn get_current_policy() -> Result { + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + + // Verifica Removable Storage Policy primeiro + let full_path = format!(r"{}\{}", REMOVABLE_STORAGE_PATH, REMOVABLE_STORAGE_GUID); + + if let Ok(key) = hklm.open_subkey_with_flags(&full_path, KEY_READ) { + let deny_read: u32 = key.get_value("Deny_Read").unwrap_or(0); + let deny_write: u32 = key.get_value("Deny_Write").unwrap_or(0); + + if deny_read == 1 && deny_write == 1 { + return Ok("BLOCK_ALL".to_string()); + } + + if deny_read == 0 && deny_write == 1 { + return Ok("READONLY".to_string()); + } + } + + // Verifica USBSTOR como fallback + if let Ok(key) = hklm.open_subkey_with_flags(USBSTOR_PATH, KEY_READ) { + let start: u32 = key.get_value("Start").unwrap_or(3); + if start == 4 { + return Ok("BLOCK_ALL".to_string()); + } + } + + Ok("ALLOW".to_string()) +} + +fn apply_removable_storage_policy(policy: UsbPolicy) -> Result<(), UsbControlError> { + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + let full_path = format!(r"{}\{}", REMOVABLE_STORAGE_PATH, REMOVABLE_STORAGE_GUID); + + match policy { + UsbPolicy::Allow => { + // Tenta remover as restricoes, se existirem + if let Ok(key) = hklm.open_subkey_with_flags(&full_path, KEY_ALL_ACCESS) { + let _ = key.delete_value("Deny_Read"); + let _ = key.delete_value("Deny_Write"); + let _ = key.delete_value("Deny_Execute"); + } + // Tenta remover a chave inteira se estiver vazia + let _ = hklm.delete_subkey(&full_path); + } + UsbPolicy::BlockAll => { + let (key, _) = hklm + .create_subkey(&full_path) + .map_err(map_winreg_error)?; + + key.set_value("Deny_Read", &1u32) + .map_err(map_winreg_error)?; + key.set_value("Deny_Write", &1u32) + .map_err(map_winreg_error)?; + key.set_value("Deny_Execute", &1u32) + .map_err(map_winreg_error)?; + } + UsbPolicy::Readonly => { + let (key, _) = hklm + .create_subkey(&full_path) + .map_err(map_winreg_error)?; + + // Permite leitura, bloqueia escrita + key.set_value("Deny_Read", &0u32) + .map_err(map_winreg_error)?; + key.set_value("Deny_Write", &1u32) + .map_err(map_winreg_error)?; + key.set_value("Deny_Execute", &0u32) + .map_err(map_winreg_error)?; + } + } + + Ok(()) +} + +fn apply_usbstor_policy(policy: UsbPolicy) -> Result<(), UsbControlError> { + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + + let key = hklm + .open_subkey_with_flags(USBSTOR_PATH, KEY_ALL_ACCESS) + .map_err(map_winreg_error)?; + + match policy { + UsbPolicy::Allow => { + // Start = 3 habilita o driver + key.set_value("Start", &3u32) + .map_err(map_winreg_error)?; + } + UsbPolicy::BlockAll => { + // Start = 4 desabilita o driver + key.set_value("Start", &4u32) + .map_err(map_winreg_error)?; + } + UsbPolicy::Readonly => { + // Readonly mantem driver ativo + key.set_value("Start", &3u32) + .map_err(map_winreg_error)?; + } + } + + Ok(()) +} + +fn apply_write_protect(enable: bool) -> Result<(), UsbControlError> { + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + + if enable { + let (key, _) = hklm + .create_subkey(STORAGE_POLICY_PATH) + .map_err(map_winreg_error)?; + + key.set_value("WriteProtect", &1u32) + .map_err(map_winreg_error)?; + } else if let Ok(key) = hklm.open_subkey_with_flags(STORAGE_POLICY_PATH, KEY_ALL_ACCESS) { + let _ = key.set_value("WriteProtect", &0u32); + } + + Ok(()) +} + +fn refresh_group_policy() -> Result<(), UsbControlError> { + use std::os::windows::process::CommandExt; + use std::process::Command; + + const CREATE_NO_WINDOW: u32 = 0x08000000; + + let output = Command::new("gpupdate") + .args(["/target:computer", "/force"]) + .creation_flags(CREATE_NO_WINDOW) + .output() + .map_err(UsbControlError::Io)?; + + if !output.status.success() { + warn!( + "gpupdate retornou erro: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + + Ok(()) +} + +fn map_winreg_error(error: io::Error) -> UsbControlError { + if let Some(code) = error.raw_os_error() { + if code == 5 { + return UsbControlError::PermissionDenied; + } + } + UsbControlError::RegistryError(error.to_string()) +} diff --git a/apps/desktop/src-tauri/.gitignore b/apps/desktop/src-tauri/.gitignore new file mode 100644 index 0000000..b21bd68 --- /dev/null +++ b/apps/desktop/src-tauri/.gitignore @@ -0,0 +1,7 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Generated by Tauri +# will have schema files for capabilities auto-completion +/gen/schemas diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock new file mode 100644 index 0000000..f5d4b76 --- /dev/null +++ b/apps/desktop/src-tauri/Cargo.lock @@ -0,0 +1,6779 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +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 = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "appsdesktop" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "chrono", + "convex", + "dirs 5.0.1", + "futures-util", + "get_if_addrs", + "hostname", + "once_cell", + "parking_lot", + "reqwest", + "serde", + "serde_json", + "sha2", + "sysinfo", + "tauri", + "tauri-build", + "tauri-plugin-deep-link", + "tauri-plugin-dialog", + "tauri-plugin-notification", + "tauri-plugin-opener", + "tauri-plugin-process", + "tauri-plugin-single-instance", + "tauri-plugin-store", + "tauri-plugin-updater", + "thiserror 1.0.69", + "tokio", + "uuid", + "winreg", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "archery" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e0a5f99dfebb87bb342d0f53bb92c81842e100bbb915223e38349580e5441d" + +[[package]] +name = "ashpd" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.2", + "raw-window-handle", + "serde", + "serde_repr", + "tokio", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link 0.2.1", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +dependencies = [ + "serde", +] + +[[package]] +name = "bitmaps" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.3", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[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 = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +dependencies = [ + "serde", +] + +[[package]] +name = "c_linked_list" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4964518bd3b4a8190e832886cdc0da9794f12e8e6c1613a9e90ff331c4c8724b" + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.9.4", + "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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" +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.17", +] + +[[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.8", +] + +[[package]] +name = "cc" +version = "1.2.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +dependencies = [ + "find-msvc-tools", + "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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convex" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e7ab85cfc76e9a13d252da8a7933ab52f38b9c51de3f7bb8dbe4e2262bac04" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.13.1", + "bytes", + "convex_sync_types", + "futures", + "imbl", + "rand 0.9.2", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tokio-tungstenite", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "convex_sync_types" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f819ce8fd4370f235f2f5e345499fa219d7d1827cd88923f1fa42942853604a0" +dependencies = [ + "anyhow", + "base64 0.13.1", + "bytes", + "derive_more 2.1.0", + "headers", + "rand 0.9.2", + "serde", + "serde_json", + "strum", + "uuid", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.9.4", + "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.9.4", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +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-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +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 = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "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-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.106", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.106", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.106", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "deranged" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case 0.4.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.106", +] + +[[package]] +name = "derive_more" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.106", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", +] + +[[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 0.5.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.9.4", + "block2 0.6.2", + "libc", + "objc2 0.6.3", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[package]] +name = "dlopen2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi 0.3.9", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[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.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" + +[[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 = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "embed-resource" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.9.8", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[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 = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[package]] +name = "flate2" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "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.106", +] + +[[package]] +name = "foreign-types-shared" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +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.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gcc" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "get_if_addrs" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abddb55a898d32925f3148bd281174a68eeb68bbfd9a5938a57b18f506ee4ef7" +dependencies = [ + "c_linked_list", + "get_if_addrs-sys", + "libc", + "winapi 0.2.8", +] + +[[package]] +name = "get_if_addrs-sys" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d04f9fb746cf36b191c00f3ede8bde9c8e64f9f4b05ae2694a9ccf5e3f5ab48" +dependencies = [ + "gcc", + "libc", +] + +[[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]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasi 0.14.7+wasi-0.2.4", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[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 0.3.9", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.9.4", + "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.106", +] + +[[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 = "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 = "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.106", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64 0.22.1", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +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 = "hostname" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" +dependencies = [ + "cfg-if", + "libc", + "windows-link 0.1.3", +] + +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever", + "match_token", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "imbl" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4308a675e4cfc1920f36a8f4d8fb62d5533b7da106844bd1ec51c6f1fa94a0c" +dependencies = [ + "archery", + "bitmaps", + "imbl-sized-chunks", + "rand_core 0.9.3", + "rand_xoshiro", + "version_check", +] + +[[package]] +name = "imbl-sized-chunks" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4241005618a62f8d57b2febd02510fb96e0137304728543dfc5fd6f052c22d" +dependencies = [ + "bitmaps", +] + +[[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.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", + "serde", + "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 = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags 2.9.4", + "cfg-if", + "libc", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "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 = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[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", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[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 = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.9.4", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "kuchikiki" +version = "0.8.8-speedreader" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" +dependencies = [ + "cssparser", + "html5ever", + "indexmap 2.11.4", + "selectors", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[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.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi 0.3.9", +] + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.9.4", + "libc", + "redox_syscall", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "mac-notification-sys" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fd3f75411f4725061682ed91f131946e912859d0044d39c4ec0aac818d7621" +dependencies = [ + "cc", + "objc2 0.6.3", + "objc2-foundation 0.3.2", + "time", +] + +[[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", + "string_cache_codegen", + "tendril", +] + +[[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.106", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minisign-verify" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e856fdd13623a2f5f2f54676a4ee49502a96a80ef4a62bcedd23d52427c44d43" + +[[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.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "muda" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "png", + "serde", + "thiserror 2.0.17", + "windows-sys 0.60.2", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.9.4", + "jni-sys", + "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", +] + +[[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.9.4", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "notify-rust" +version = "4.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6442248665a5aa2514e794af3b39661a8e73033b1cc5e59899e1276117ee4400" +dependencies = [ + "futures-lite", + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +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.9.4", + "block2 0.6.2", + "libc", + "objc2 0.6.3", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.9.4", + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "bitflags 2.9.4", + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.9.4", + "dispatch2", + "objc2 0.6.3", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.9.4", + "dispatch2", + "objc2 0.6.3", + "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 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.9.4", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.9.4", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", +] + +[[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.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.9.4", + "block2 0.5.1", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.9.4", + "block2 0.6.2", + "libc", + "objc2 0.6.3", + "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.9.4", + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-javascript-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" +dependencies = [ + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.9.4", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-osa-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" +dependencies = [ + "bitflags 2.9.4", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.9.4", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.9.4", + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "bitflags 2.9.4", + "objc2 0.6.3", + "objc2-core-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.9.4", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.9.4", + "block2 0.6.2", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-javascript-core", + "objc2-security", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "open" +version = "5.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags 2.9.4", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-src" +version = "300.5.4+3.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "osakit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" +dependencies = [ + "objc2 0.6.3", + "objc2-foundation 0.3.2", + "objc2-osa-kit", + "serde", + "serde_json", + "thiserror 2.0.17", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "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 = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[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_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_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.5", +] + +[[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.5", +] + +[[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.106", +] + +[[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.1", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64 0.22.1", + "indexmap 2.11.4", + "quick-xml 0.38.3", + "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 = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.7", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[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.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.38.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[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.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" +dependencies = [ + "rand_core 0.9.3", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.9.4", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 2.0.17", +] + +[[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.106", +] + +[[package]] +name = "regex" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d8d02cacdb176ef4678de6c052efb4b3da14b78e4db683a4252762be5433" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "722166aa0d7438abbaa4d5cc2c649dac844e8c56d82fb3d33e9c34b5cd268fc6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3160422bbd54dd5ecfdca71e5fd59b7b8fe2b1697ab2baf64f6d05dcc66d298" + +[[package]] +name = "reqwest" +version = "0.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "rfd" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +dependencies = [ + "ashpd", + "block2 0.6.2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rust-ini" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.9.4", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[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.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +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.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +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.106", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.4", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "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", + "derive_more 0.99.20", + "fxhash", + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "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_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[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.106", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "indexmap 2.11.4", + "itoa", + "memchr", + "ryu", + "serde", + "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.106", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6093cd8c01b25262b84927e0f7151692158fab02d961e04c979d3903eba7ecc5" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.11.4", + "schemars 0.9.0", + "schemars 1.0.4", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7e6c180db0816026a61afa1cff5344fb7ebded7e4d3062772179f2501481c27" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[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.106", +] + +[[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 = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "softbuffer" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +dependencies = [ + "bytemuck", + "cfg_aliases", + "core-graphics", + "foreign-types 0.5.0", + "js-sys", + "log", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", + "raw-window-handle", + "redox_syscall", + "wasm-bindgen", + "web-sys", + "windows-sys 0.59.0", +] + +[[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 = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[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_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 = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "sysinfo" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows 0.57.0", +] + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" +dependencies = [ + "bitflags 2.9.4", + "block2 0.6.2", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dispatch", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-foundation 0.3.2", + "once_cell", + "parking_lot", + "raw-window-handle", + "scopeguard", + "tao-macros", + "unicode-segmentation", + "url", + "windows 0.61.3", + "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.106", +] + +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[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.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a3868da5508446a7cd08956d523ac3edf0a8bc20bf7e4038f9a95c2800d2033" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs 6.0.0", + "dunce", + "embed_plist", + "getrandom 0.3.3", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-foundation 0.3.2", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.17", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows 0.61.3", +] + +[[package]] +name = "tauri-build" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17fcb8819fd16463512a12f531d44826ce566f486d7ccd211c9c8cebdaec4e08" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs 6.0.0", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "toml 0.9.8", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa9844cefcf99554a16e0a278156ae73b0d8680bbc0e2ad1e4287aadd8489cf" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.106", + "tauri-utils", + "thiserror 2.0.17", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3764a12f886d8245e66b7ee9b43ccc47883399be2019a61d80cf0f4117446fde" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.106", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9946a3cede302eac0c6eb6c6070ac47b1768e326092d32efbb91f21ed58d978f" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "toml 0.9.8", + "walkdir", +] + +[[package]] +name = "tauri-plugin-deep-link" +version = "2.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e82759f7c7d51de3cbde51c04b3f2332de52436ed84541182cd8944b04e9e73" +dependencies = [ + "dunce", + "plist", + "rust-ini", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.17", + "tracing", + "url", + "windows-registry", + "windows-result 0.3.4", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "313f8138692ddc4a2127c4c9607d616a46f5c042e77b3722450866da0aad2f19" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.17", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47df422695255ecbe7bac7012440eddaeefd026656171eac9559f5243d3230d9" +dependencies = [ + "anyhow", + "dunce", + "glob", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.17", + "toml 0.9.8", + "url", +] + +[[package]] +name = "tauri-plugin-notification" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc" +dependencies = [ + "log", + "notify-rust", + "rand 0.9.2", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "thiserror 2.0.17", + "time", + "url", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "786156aa8e89e03d271fbd3fe642207da8e65f3c961baa9e2930f332bf80a1f5" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation 0.3.2", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.17", + "url", + "windows 0.61.3", + "zbus", +] + +[[package]] +name = "tauri-plugin-process" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7461c622a5ea00eb9cd9f7a08dbd3bf79484499fd5c21aa2964677f64ca651ab" +dependencies = [ + "tauri", + "tauri-plugin", +] + +[[package]] +name = "tauri-plugin-single-instance" +version = "2.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd707f8c86b4e3004e2c141fa24351f1909ba40ce1b8437e30d5ed5277dd3710" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.17", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + +[[package]] +name = "tauri-plugin-store" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85dd80d60a76ee2c2fdce09e9ef30877b239c2a6bb76e6d7d03708aa5f13a19" +dependencies = [ + "dunce", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "tauri-plugin-updater" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27cbc31740f4d507712550694749572ec0e43bdd66992db7599b89fbfd6b167b" +dependencies = [ + "base64 0.22.1", + "dirs 6.0.0", + "flate2", + "futures-util", + "http", + "infer", + "log", + "minisign-verify", + "osakit", + "percent-encoding", + "reqwest", + "semver", + "serde", + "serde_json", + "tar", + "tauri", + "tauri-plugin", + "tempfile", + "thiserror 2.0.17", + "time", + "tokio", + "url", + "windows-sys 0.60.2", + "zip", +] + +[[package]] +name = "tauri-runtime" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f766fe9f3d1efc4b59b17e7a891ad5ed195fa8d23582abb02e6c9a01137892" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2 0.6.3", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.17", + "url", + "webkit2gtk", + "webview2-com", + "windows 0.61.3", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "187a3f26f681bdf028f796ccf57cf478c1ee422c50128e5a0a6ebeb3f5910065" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-foundation 0.3.2", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows 0.61.3", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a423c51176eb3616ee9b516a9fa67fed5f0e78baaba680e44eb5dd2cc37490" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dunce", + "glob", + "html5ever", + "http", + "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.17", + "toml 0.9.8", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd21509dd1fa9bd355dc29894a6ff10635880732396aa38c0066c1e6c1ab8074" +dependencies = [ + "embed-resource", + "toml 0.9.8", +] + +[[package]] +name = "tauri-winrt-notification" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" +dependencies = [ + "quick-xml 0.37.5", + "thiserror 2.0.17", + "windows 0.61.3", + "windows-version", +] + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2", + "tokio-macros", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "native-tls", + "tokio", + "tokio-native-tls", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +dependencies = [ + "indexmap 2.11.4", + "serde_core", + "serde_spanned 1.0.3", + "toml_datetime 0.7.3", + "toml_parser", + "toml_writer", + "winnow 0.7.13", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.11.4", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.11.4", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +dependencies = [ + "indexmap 2.11.4", + "toml_datetime 0.7.3", + "toml_parser", + "winnow 0.7.13", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow 0.7.13", +] + +[[package]] +name = "toml_writer" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.4", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d92153331e7d02ec09137538996a7786fe679c629c279e82a6be762b7e6fe2" +dependencies = [ + "crossbeam-channel", + "dirs 6.0.0", + "libappindicator", + "muda", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "once_cell", + "png", + "serde", + "thiserror 2.0.17", + "windows-sys 0.59.0", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand 0.9.2", + "sha1", + "thiserror 2.0.17", + "url", + "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.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi 0.3.9", +] + +[[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 = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wayland-backend" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +dependencies = [ + "bitflags 2.9.4", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +dependencies = [ + "bitflags 2.9.4", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +dependencies = [ + "proc-macro2", + "quick-xml 0.37.5", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" +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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" +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 = "webpki-roots" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webview2-com" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-implement 0.60.2", + "windows-interface 0.59.3", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" +dependencies = [ + "thiserror 2.0.17", + "windows 0.61.3", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[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 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + +[[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.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "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]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[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.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[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]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "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]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "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]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +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 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]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[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.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "wry" +version = "0.53.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d78ec082b80fa088569a970d043bb3050abaabf4454101d44514ee8d9a8c9f6" +dependencies = [ + "base64 0.22.1", + "block2 0.6.2", + "cookie", + "crossbeam-channel", + "dirs 6.0.0", + "dpi", + "dunce", + "gdkx11", + "gtk", + "html5ever", + "http", + "javascriptcore-rs", + "jni", + "kuchikiki", + "libc", + "ndk", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.17", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.61.3", + "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 = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d07e46d035fb8e375b2ce63ba4e4ff90a7f73cf2ffb0138b29e1158d2eaadf7" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "nix", + "ordered-stream", + "serde", + "serde_repr", + "tokio", + "tracing", + "uds_windows", + "windows-sys 0.60.2", + "winnow 0.7.13", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e797a9c847ed3ccc5b6254e8bcce056494b375b511b3d6edcec0aeb4defaca" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.106", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +dependencies = [ + "serde", + "static_assertions", + "winnow 0.7.13", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "indexmap 2.11.4", + "memchr", +] + +[[package]] +name = "zvariant" +version = "5.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "999dd3be73c52b1fccd109a4a81e4fcd20fab1d3599c8121b38d04e1419498db" +dependencies = [ + "endi", + "enumflags2", + "serde", + "url", + "winnow 0.7.13", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6643fd0b26a46d226bd90d3f07c1b5321fe9bb7f04673cb37ac6d6883885b68e" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.106", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.106", + "winnow 0.7.13", +] diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml new file mode 100644 index 0000000..8e26952 --- /dev/null +++ b/apps/desktop/src-tauri/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "appsdesktop" +version = "0.1.0" +description = "A Tauri App" +authors = ["you"] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +# The `_lib` suffix may seem redundant but it is necessary +# to make the lib name unique and wouldn't conflict with the bin name. +# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 +name = "appsdesktop_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2.4.1", features = [] } + +[dependencies] +tauri = { version = "2.9", features = ["wry", "devtools", "tray-icon"] } +tauri-plugin-dialog = "2.4.2" +tauri-plugin-opener = "2.5.0" +tauri-plugin-store = "2.4.0" +tauri-plugin-updater = "2.9.0" +tauri-plugin-process = "2.3.0" +tauri-plugin-notification = "2" +tauri-plugin-deep-link = "2" +tauri-plugin-single-instance = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +sysinfo = { version = "0.31", default-features = false, features = ["multithread", "network", "system", "disk"] } +get_if_addrs = "0.5" +reqwest = { version = "0.12", features = ["json", "rustls-tls", "blocking", "stream"], default-features = false } +futures-util = "0.3" +tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] } +once_cell = "1.19" +thiserror = "1.0" +chrono = { version = "0.4", features = ["serde"] } +parking_lot = "0.12" +hostname = "0.4" +base64 = "0.22" +sha2 = "0.10" +convex = "0.10.2" +uuid = { version = "1", features = ["v4"] } +dirs = "5" +# SSE usa reqwest com stream, nao precisa de websocket + +[target.'cfg(windows)'.dependencies] +winreg = "0.55" diff --git a/apps/desktop/src-tauri/build.rs b/apps/desktop/src-tauri/build.rs new file mode 100644 index 0000000..0dfb1ae --- /dev/null +++ b/apps/desktop/src-tauri/build.rs @@ -0,0 +1,31 @@ +fn main() { + // Custom manifest keeps Common-Controls v6 dependency to avoid TaskDialogIndirect errors. + let windows = tauri_build::WindowsAttributes::new().app_manifest( + r#" + + + + + + + + + + + + + + +"#, + ); + + let attrs = tauri_build::Attributes::new().windows_attributes(windows); + + tauri_build::try_build(attrs).expect("failed to run Tauri build script"); +} diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json new file mode 100644 index 0000000..a0cf79b --- /dev/null +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -0,0 +1,33 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Capability for all windows", + "windows": ["main", "chat-*", "chat-hub"], + "permissions": [ + "core:default", + "core:event:default", + "core:event:allow-listen", + "core:event:allow-unlisten", + "core:event:allow-emit", + "core:window:default", + "core:window:allow-close", + "core:window:allow-hide", + "core:window:allow-show", + "core:window:allow-set-focus", + "core:window:allow-start-dragging", + "dialog:allow-open", + "opener:default", + "store:default", + "store:allow-load", + "store:allow-set", + "store:allow-get", + "store:allow-save", + "store:allow-delete", + "updater:default", + "process:default", + "notification:default", + "notification:allow-notify", + "notification:allow-request-permission", + "notification:allow-is-permission-granted" + ] +} diff --git a/apps/desktop/src-tauri/icons/128x128.png b/apps/desktop/src-tauri/icons/128x128.png new file mode 100644 index 0000000..ea1b4de Binary files /dev/null and b/apps/desktop/src-tauri/icons/128x128.png differ diff --git a/apps/desktop/src-tauri/icons/128x128@2x.png b/apps/desktop/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000..89582e7 Binary files /dev/null and b/apps/desktop/src-tauri/icons/128x128@2x.png differ diff --git a/apps/desktop/src-tauri/icons/32x32.png b/apps/desktop/src-tauri/icons/32x32.png new file mode 100644 index 0000000..96fd42a Binary files /dev/null and b/apps/desktop/src-tauri/icons/32x32.png differ diff --git a/apps/desktop/src-tauri/icons/64x64.png b/apps/desktop/src-tauri/icons/64x64.png new file mode 100644 index 0000000..bcd4b9c Binary files /dev/null and b/apps/desktop/src-tauri/icons/64x64.png differ diff --git a/apps/desktop/src-tauri/icons/Raven.png b/apps/desktop/src-tauri/icons/Raven.png new file mode 100644 index 0000000..ab005ba Binary files /dev/null and b/apps/desktop/src-tauri/icons/Raven.png differ diff --git a/apps/desktop/src-tauri/icons/Square107x107Logo.png b/apps/desktop/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000..0a503d5 Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square107x107Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square142x142Logo.png b/apps/desktop/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000..31765ac Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square142x142Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square150x150Logo.png b/apps/desktop/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000..8a666ce Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square150x150Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square284x284Logo.png b/apps/desktop/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000..9bbef05 Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square284x284Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square30x30Logo.png b/apps/desktop/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000..df1c649 Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square30x30Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square310x310Logo.bmp b/apps/desktop/src-tauri/icons/Square310x310Logo.bmp new file mode 100644 index 0000000..2aba9f9 Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square310x310Logo.bmp differ diff --git a/apps/desktop/src-tauri/icons/Square310x310Logo.png b/apps/desktop/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000..c277c69 Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square310x310Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square44x44Logo.png b/apps/desktop/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000..4608b37 Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square44x44Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square71x71Logo.png b/apps/desktop/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000..e98ccdc Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square71x71Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square89x89Logo.png b/apps/desktop/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000..c0b6c70 Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square89x89Logo.png differ diff --git a/apps/desktop/src-tauri/icons/StoreLogo.png b/apps/desktop/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000..6b3ea98 Binary files /dev/null and b/apps/desktop/src-tauri/icons/StoreLogo.png differ diff --git a/apps/desktop/src-tauri/icons/icon-128.png b/apps/desktop/src-tauri/icons/icon-128.png new file mode 100644 index 0000000..73fae2a Binary files /dev/null and b/apps/desktop/src-tauri/icons/icon-128.png differ diff --git a/apps/desktop/src-tauri/icons/icon-256.png b/apps/desktop/src-tauri/icons/icon-256.png new file mode 100644 index 0000000..598c10f Binary files /dev/null and b/apps/desktop/src-tauri/icons/icon-256.png differ diff --git a/apps/desktop/src-tauri/icons/icon-32.png b/apps/desktop/src-tauri/icons/icon-32.png new file mode 100644 index 0000000..cfd6dd7 Binary files /dev/null and b/apps/desktop/src-tauri/icons/icon-32.png differ diff --git a/apps/desktop/src-tauri/icons/icon-512.png b/apps/desktop/src-tauri/icons/icon-512.png new file mode 100644 index 0000000..bb67dd8 Binary files /dev/null and b/apps/desktop/src-tauri/icons/icon-512.png differ diff --git a/apps/desktop/src-tauri/icons/icon-64.png b/apps/desktop/src-tauri/icons/icon-64.png new file mode 100644 index 0000000..5c551e4 Binary files /dev/null and b/apps/desktop/src-tauri/icons/icon-64.png differ diff --git a/apps/desktop/src-tauri/icons/icon.icns b/apps/desktop/src-tauri/icons/icon.icns new file mode 100644 index 0000000..9c729c7 Binary files /dev/null and b/apps/desktop/src-tauri/icons/icon.icns differ diff --git a/apps/desktop/src-tauri/icons/icon.ico b/apps/desktop/src-tauri/icons/icon.ico new file mode 100644 index 0000000..4e2d151 Binary files /dev/null and b/apps/desktop/src-tauri/icons/icon.ico differ diff --git a/apps/desktop/src-tauri/icons/icon.png b/apps/desktop/src-tauri/icons/icon.png new file mode 100644 index 0000000..bb67dd8 Binary files /dev/null and b/apps/desktop/src-tauri/icons/icon.png differ diff --git a/apps/desktop/src-tauri/icons/logo-raven-fund-azul.png b/apps/desktop/src-tauri/icons/logo-raven-fund-azul.png new file mode 100644 index 0000000..9b95662 Binary files /dev/null and b/apps/desktop/src-tauri/icons/logo-raven-fund-azul.png differ diff --git a/apps/desktop/src-tauri/icons/logo-raven.png b/apps/desktop/src-tauri/icons/logo-raven.png new file mode 100644 index 0000000..62b264e Binary files /dev/null and b/apps/desktop/src-tauri/icons/logo-raven.png differ diff --git a/apps/desktop/src-tauri/icons/nsis-header.bmp b/apps/desktop/src-tauri/icons/nsis-header.bmp new file mode 100644 index 0000000..1313bac Binary files /dev/null and b/apps/desktop/src-tauri/icons/nsis-header.bmp differ diff --git a/apps/desktop/src-tauri/icons/nsis-sidebar.bmp b/apps/desktop/src-tauri/icons/nsis-sidebar.bmp new file mode 100644 index 0000000..061adc8 Binary files /dev/null and b/apps/desktop/src-tauri/icons/nsis-sidebar.bmp differ diff --git a/apps/desktop/src-tauri/installer-hooks.nsh b/apps/desktop/src-tauri/installer-hooks.nsh new file mode 100644 index 0000000..72de836 --- /dev/null +++ b/apps/desktop/src-tauri/installer-hooks.nsh @@ -0,0 +1,121 @@ +; Hooks customizadas do instalador NSIS (Tauri) +; +; Objetivo: +; - Remover a marca "Nullsoft Install System" exibida no canto inferior esquerdo +; - Instalar o Raven Service para operacoes privilegiadas sem UAC +; +; Nota: o bundler do Tauri injeta estes macros no script principal do instalador. + +BrandingText " " + +!macro NSIS_HOOK_PREINSTALL + ; Para e remove qualquer instancia anterior do servico antes de atualizar + DetailPrint "Parando servicos anteriores..." + + ; Para o servico + nsExec::ExecToLog 'sc stop RavenService' + + ; Aguarda o servico parar completamente (ate 10 segundos) + nsExec::ExecToLog 'powershell -Command "$$i=0; while((Get-Service RavenService -ErrorAction SilentlyContinue).Status -eq \"Running\" -and $$i -lt 10){Start-Sleep 1;$$i++}"' + + ; Remove o servico antigo (IMPORTANTE para reinstalacoes) + DetailPrint "Removendo servico antigo..." + IfFileExists "$INSTDIR\raven-service.exe" 0 +2 + nsExec::ExecToLog '"$INSTDIR\raven-service.exe" uninstall' + + ; Fallback: remove via sc delete se o executavel nao existir + nsExec::ExecToLog 'sc delete RavenService' + + ; Forca encerramento de processos remanescentes + nsExec::ExecToLog 'taskkill /F /IM raven-service.exe' + nsExec::ExecToLog 'taskkill /F /IM appsdesktop.exe' + + ; Aguarda liberacao dos arquivos e remocao completa do servico + Sleep 3000 +!macroend + +!macro NSIS_HOOK_POSTINSTALL + ; ========================================================================= + ; Instala e inicia o Raven Service + ; ========================================================================= + + DetailPrint "Instalando Raven Service..." + + ; Garante que nao ha servico residual + nsExec::ExecToLog 'sc delete RavenService' + Sleep 1000 + + ; O servico ja esta em $INSTDIR (copiado como resource pelo Tauri) + ; Registra o servico Windows + nsExec::ExecToLog '"$INSTDIR\raven-service.exe" install' + Pop $0 + + ${If} $0 != 0 + DetailPrint "Aviso: Falha ao registrar servico (codigo: $0)" + ; Tenta remover completamente e reinstalar + nsExec::ExecToLog '"$INSTDIR\raven-service.exe" uninstall' + nsExec::ExecToLog 'sc delete RavenService' + Sleep 1000 + nsExec::ExecToLog '"$INSTDIR\raven-service.exe" install' + Pop $0 + ${EndIf} + + ; Aguarda registro do servico + Sleep 500 + + ; Inicia o servico + DetailPrint "Iniciando Raven Service..." + nsExec::ExecToLog 'sc start RavenService' + Pop $0 + + ${If} $0 == 0 + DetailPrint "Raven Service iniciado com sucesso!" + ${Else} + ; Tenta novamente apos breve espera + Sleep 1000 + nsExec::ExecToLog 'sc start RavenService' + Pop $0 + ${If} $0 == 0 + DetailPrint "Raven Service iniciado com sucesso (segunda tentativa)!" + ${Else} + DetailPrint "Aviso: Servico sera iniciado na proxima reinicializacao (codigo: $0)" + ${EndIf} + ${EndIf} + + ; ========================================================================= + ; Verifica se RustDesk esta instalado + ; Se nao estiver, o Raven Service instalara automaticamente no primeiro uso + ; ========================================================================= + + IfFileExists "$PROGRAMFILES\RustDesk\rustdesk.exe" rustdesk_found rustdesk_not_found + + rustdesk_not_found: + DetailPrint "RustDesk sera instalado automaticamente pelo Raven Service." + Goto rustdesk_done + + rustdesk_found: + DetailPrint "RustDesk ja esta instalado." + + rustdesk_done: +!macroend + +!macro NSIS_HOOK_PREUNINSTALL + ; ========================================================================= + ; Para e remove o Raven Service + ; ========================================================================= + + DetailPrint "Parando Raven Service..." + nsExec::ExecToLog 'sc stop RavenService' + Sleep 1000 + + DetailPrint "Removendo Raven Service..." + nsExec::ExecToLog '"$INSTDIR\raven-service.exe" uninstall' + + ; Aguarda um pouco para garantir que o servico foi removido + Sleep 500 +!macroend + +!macro NSIS_HOOK_POSTUNINSTALL + ; Nada adicional necessario +!macroend + diff --git a/apps/desktop/src-tauri/src/agent.rs b/apps/desktop/src-tauri/src/agent.rs new file mode 100644 index 0000000..1d42d23 --- /dev/null +++ b/apps/desktop/src-tauri/src/agent.rs @@ -0,0 +1,1748 @@ +use std::sync::Arc; +use std::time::Duration; + +use chrono::{DateTime, Utc}; +use once_cell::sync::Lazy; +use parking_lot::Mutex; +use serde::Serialize; +use serde_json::json; +use std::collections::HashMap; +use sysinfo::{Networks, System}; +use tauri::async_runtime::{self, JoinHandle}; +use tokio::sync::Notify; + +#[derive(thiserror::Error, Debug)] +pub enum AgentError { + #[error("Falha ao obter hostname da dispositivo")] + Hostname, + #[error("Nenhum identificador de hardware disponível (MAC/serial)")] + MissingIdentifiers, + #[error("URL de API inválida")] + InvalidApiUrl, + #[error("Falha HTTP: {0}")] + Http(#[from] reqwest::Error), +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MachineOs { + pub name: String, + pub version: Option, + pub architecture: Option, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MachineMetrics { + pub collected_at: DateTime, + pub cpu_logical_cores: usize, + pub cpu_physical_cores: Option, + pub cpu_usage_percent: f32, + pub load_average_one: Option, + pub load_average_five: Option, + pub load_average_fifteen: Option, + pub memory_total_bytes: u64, + pub memory_used_bytes: u64, + pub memory_used_percent: f32, + pub uptime_seconds: u64, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MachineInventory { + pub cpu_brand: Option, + pub host_identifier: Option, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MachineProfile { + pub hostname: String, + pub os: MachineOs, + pub mac_addresses: Vec, + pub serial_numbers: Vec, + pub inventory: MachineInventory, + pub metrics: MachineMetrics, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct HeartbeatPayload { + machine_token: String, + status: Option, + hostname: Option, + os: Option, + metrics: Option, + metadata: Option, +} + +fn collect_mac_addresses() -> Vec { + let mut macs = Vec::new(); + let mut networks = Networks::new(); + networks.refresh_list(); + networks.refresh(); + + for (_, data) in networks.iter() { + let bytes = data.mac_address().0; + if bytes.iter().all(|byte| *byte == 0) { + continue; + } + + let formatted = bytes + .iter() + .map(|byte| format!("{:02x}", byte)) + .collect::>() + .join(":"); + + if !macs.contains(&formatted) { + macs.push(formatted); + } + } + + macs +} + +#[cfg(target_os = "linux")] +fn collect_serials_platform() -> Vec { + let mut out = Vec::new(); + for path in [ + "/sys/class/dmi/id/product_uuid", + "/sys/class/dmi/id/product_serial", + "/sys/class/dmi/id/board_serial", + "/etc/machine-id", + ] { + if let Ok(raw) = std::fs::read_to_string(path) { + let s = raw.trim().to_string(); + if !s.is_empty() && !out.contains(&s) { + out.push(s); + } + } + } + out +} + +#[cfg(any(target_os = "windows", target_os = "macos"))] +fn collect_serials_platform() -> Vec { + // Fase 1: sem coleta nativa; será implementada via WMI/ioreg na fase 2. + Vec::new() +} + +fn collect_serials() -> Vec { + collect_serials_platform() +} + +fn collect_network_addrs() -> Vec { + // Mapa name -> mac via sysinfo (mais estável que get_if_addrs para MAC) + let mut mac_by_name: HashMap = HashMap::new(); + let mut networks = Networks::new(); + networks.refresh_list(); + networks.refresh(); + for (name, data) in networks.iter() { + let bytes = data.mac_address().0; + if bytes.iter().any(|b| *b != 0) { + let mac = bytes + .iter() + .map(|b| format!("{:02x}", b)) + .collect::>() + .join(":"); + mac_by_name.insert(name.to_string(), mac); + } + } + + let mut entries = Vec::new(); + if let Ok(ifaces) = get_if_addrs::get_if_addrs() { + for iface in ifaces { + let name = iface.name.clone(); + let addr = iface.ip(); + let ip = addr.to_string(); + let mac = mac_by_name.get(&name).cloned(); + entries.push(json!({ + "name": name, + "mac": mac, + "ip": ip, + })); + } + } + entries +} + +fn collect_disks(_system: &System) -> Vec { + let mut out = Vec::new(); + let disks = sysinfo::Disks::new_with_refreshed_list(); + for disk in disks.list() { + let name = disk.name().to_string_lossy().to_string(); + let mount = disk.mount_point().to_string_lossy().to_string(); + let fs = disk.file_system().to_string_lossy().to_string(); + let total = disk.total_space(); + let avail = disk.available_space(); + out.push(json!({ + "name": if name.is_empty() { mount.clone() } else { name }, + "mountPoint": mount, + "fs": fs, + "totalBytes": total, + "availableBytes": avail, + })); + } + + out +} + +fn parse_u64(value: &serde_json::Value) -> Option { + if let Some(num) = value.as_u64() { + return Some(num); + } + if let Some(num) = value.as_f64() { + if num.is_finite() && num >= 0.0 { + return Some(num as u64); + } + } + if let Some(text) = value.as_str() { + if let Ok(parsed) = text.trim().parse::() { + return Some(parsed); + } + } + None +} + +fn push_gpu( + list: &mut Vec, + name: Option<&str>, + memory: Option, + vendor: Option<&str>, + driver: Option<&str>, +) { + if let Some(name) = name { + if name.trim().is_empty() { + return; + } + let mut obj = serde_json::Map::new(); + obj.insert("name".into(), json!(name.trim())); + if let Some(memory) = memory { + obj.insert("memoryBytes".into(), json!(memory)); + } + if let Some(vendor) = vendor { + if !vendor.trim().is_empty() { + obj.insert("vendor".into(), json!(vendor.trim())); + } + } + if let Some(driver) = driver { + if !driver.trim().is_empty() { + obj.insert("driver".into(), json!(driver.trim())); + } + } + list.push(serde_json::Value::Object(obj)); + } +} + +fn build_inventory_metadata(system: &System) -> serde_json::Value { + let cpu_brand = system + .cpus() + .first() + .map(|cpu| cpu.brand().to_string()) + .filter(|brand| !brand.trim().is_empty()); + // sysinfo 0.31 já retorna bytes em total_memory/used_memory + let mem_total_bytes = system.total_memory(); + let network = collect_network_addrs(); + let disks = collect_disks(system); + let mut inventory = json!({ + "cpu": { "brand": cpu_brand.clone() }, + "memory": { "totalBytes": mem_total_bytes }, + "network": network, + "disks": disks, + }); + + if let Some(obj) = inventory.as_object_mut() { + let mut hardware = serde_json::Map::new(); + if let Some(brand) = cpu_brand.clone() { + if !brand.trim().is_empty() { + hardware.insert("cpuType".into(), json!(brand.trim())); + } + } + if let Some(physical) = system.physical_core_count() { + hardware.insert("physicalCores".into(), json!(physical)); + } + hardware.insert("logicalCores".into(), json!(system.cpus().len())); + if mem_total_bytes > 0 { + hardware.insert("memoryBytes".into(), json!(mem_total_bytes)); + hardware.insert("memory".into(), json!(mem_total_bytes)); + } + if !hardware.is_empty() { + obj.insert("hardware".into(), serde_json::Value::Object(hardware)); + } + } + + #[cfg(target_os = "linux")] + { + // Softwares instalados (dpkg ou rpm) + let software = collect_software_linux(); + if let Some(obj) = inventory.as_object_mut() { + obj.insert("software".into(), software); + } + + // Serviços ativos (systemd) + let services = collect_services_linux(); + if let Some(obj) = inventory.as_object_mut() { + obj.insert("services".into(), services); + } + + // Informações estendidas (lsblk/lspci/lsusb/smartctl) + let extended = collect_linux_extended(); + if let Some(obj) = inventory.as_object_mut() { + obj.insert("extended".into(), extended); + } + } + + #[cfg(target_os = "windows")] + { + let mut extended = collect_windows_extended(); + // Fallback: se osInfo vier vazio, preenche com dados do sysinfo + if let Some(win) = extended.get_mut("windows").and_then(|v| v.as_object_mut()) { + let needs_os_info = match win.get("osInfo") { + Some(v) => v.as_object().map(|m| m.is_empty()).unwrap_or(true), + None => true, + }; + if needs_os_info { + let mut osmap = serde_json::Map::new(); + if let Some(name) = System::name() { + osmap.insert("ProductName".into(), json!(name)); + } + if let Some(ver) = System::os_version() { + osmap.insert("Version".into(), json!(ver)); + } + if let Some(build) = System::kernel_version() { + osmap.insert("BuildNumber".into(), json!(build)); + } + win.insert("osInfo".into(), serde_json::Value::Object(osmap)); + } + } + if let Some(obj) = inventory.as_object_mut() { + obj.insert("extended".into(), extended); + } + } + + #[cfg(target_os = "macos")] + { + let extended = collect_macos_extended(); + if let Some(obj) = inventory.as_object_mut() { + obj.insert("extended".into(), extended); + } + } + + // Normalização de software/serviços no topo do inventário + if let Some(obj) = inventory.as_object_mut() { + let extended_snapshot = obj.get("extended").and_then(|v| v.as_object()).cloned(); + // Merge software + let mut software: Vec = Vec::new(); + if let Some(existing) = obj.get("software").and_then(|v| v.as_array()) { + software.extend(existing.iter().cloned()); + } + if let Some(ext) = extended_snapshot.as_ref() { + // Windows normalize + if let Some(win) = ext.get("windows").and_then(|v| v.as_object()) { + if let Some(ws) = win.get("software").and_then(|v| v.as_array()) { + for item in ws { + let name = item + .get("DisplayName") + .or_else(|| item.get("name")) + .cloned() + .unwrap_or(json!(null)); + let version = item + .get("DisplayVersion") + .or_else(|| item.get("version")) + .cloned() + .unwrap_or(json!(null)); + let publisher = item.get("Publisher").cloned().unwrap_or(json!(null)); + software + .push(json!({ "name": name, "version": version, "source": publisher })); + } + } + } + // macOS normalize + if let Some(macos) = ext.get("macos").and_then(|v| v.as_object()) { + if let Some(pkgs) = macos.get("packages").and_then(|v| v.as_array()) { + for p in pkgs { + software.push(json!({ "name": p, "version": null, "source": "pkgutil" })); + } + } + } + } + if !software.is_empty() { + obj.insert("software".into(), json!(software)); + } + + // Merge services + let mut services: Vec = Vec::new(); + if let Some(existing) = obj.get("services").and_then(|v| v.as_array()) { + services.extend(existing.iter().cloned()); + } + if let Some(ext) = extended_snapshot.as_ref() { + if let Some(win) = ext.get("windows").and_then(|v| v.as_object()) { + if let Some(wsvc) = win.get("services").and_then(|v| v.as_array()) { + for s in wsvc { + let name = s.get("Name").cloned().unwrap_or(json!(null)); + let status = s.get("Status").cloned().unwrap_or(json!(null)); + let display = s.get("DisplayName").cloned().unwrap_or(json!(null)); + services.push( + json!({ "name": name, "status": status, "displayName": display }), + ); + } + } + } + } + if !services.is_empty() { + obj.insert("services".into(), json!(services)); + } + + let mut gpus: Vec = Vec::new(); + if let Some(ext) = extended_snapshot.as_ref() { + if let Some(win) = ext.get("windows").and_then(|v| v.as_object()) { + if let Some(video) = win.get("videoControllers").and_then(|v| v.as_array()) { + for controller in video { + let name = controller.get("Name").and_then(|v| v.as_str()); + let memory = controller.get("AdapterRAM").and_then(parse_u64); + let driver = controller.get("DriverVersion").and_then(|v| v.as_str()); + push_gpu(&mut gpus, name, memory, None, driver); + } + } + if obj + .get("disks") + .and_then(|v| v.as_array()) + .map(|arr| arr.is_empty()) + .unwrap_or(true) + { + if let Some(raw) = win.get("disks").and_then(|v| v.as_array()) { + let mapped = raw + .iter() + .map(|disk| { + let model = disk.get("Model").and_then(|v| v.as_str()).unwrap_or_default(); + let serial = disk.get("SerialNumber").and_then(|v| v.as_str()).unwrap_or_default(); + let size = parse_u64(disk.get("Size").unwrap_or(&serde_json::Value::Null)).unwrap_or(0); + let interface = disk.get("InterfaceType").and_then(|v| v.as_str()).unwrap_or(""); + let media = disk.get("MediaType").and_then(|v| v.as_str()).unwrap_or(""); + json!({ + "name": if !model.is_empty() { model } else { serial }, + "mountPoint": "", + "fs": if !media.is_empty() { media } else { "—" }, + "interface": if !interface.is_empty() { serde_json::Value::String(interface.to_string()) } else { serde_json::Value::Null }, + "serial": if !serial.is_empty() { serde_json::Value::String(serial.to_string()) } else { serde_json::Value::Null }, + "totalBytes": size, + "availableBytes": serde_json::Value::Null, + }) + }) + .collect::>(); + if !mapped.is_empty() { + obj.insert("disks".into(), json!(mapped)); + } + } + } + } + if let Some(linux) = ext.get("linux").and_then(|v| v.as_object()) { + if let Some(pci) = linux.get("pciList").and_then(|v| v.as_array()) { + for entry in pci { + if let Some(text) = entry.get("text").and_then(|v| v.as_str()) { + let lower = text.to_lowercase(); + if lower.contains(" vga ") + || lower.contains(" 3d controller") + || lower.contains("display controller") + { + push_gpu(&mut gpus, Some(text), None, None, None); + } + } + } + } + } + if let Some(macos) = ext.get("macos").and_then(|v| v.as_object()) { + if let Some(profiler) = macos.get("systemProfiler").and_then(|v| v.as_object()) { + if let Some(displays) = profiler + .get("SPDisplaysDataType") + .and_then(|v| v.as_array()) + { + for display in displays { + if let Some(d) = display.as_object() { + let name = d.get("_name").and_then(|v| v.as_str()); + let vram = d + .get("spdisplays_vram") + .and_then(|v| v.as_str()) + .and_then(|s| { + let digits = s.split_whitespace().next().unwrap_or(""); + digits.parse::().ok().map(|n| { + if s.to_lowercase().contains("gb") { + n * 1024 * 1024 * 1024 + } else if s.to_lowercase().contains("mb") { + n * 1024 * 1024 + } else { + n + } + }) + }); + push_gpu(&mut gpus, name, vram, None, None); + } + } + } + } + } + } + + if !gpus.is_empty() { + let entry = obj.entry("hardware").or_insert_with(|| json!({})); + if let Some(hardware) = entry.as_object_mut() { + hardware.insert("gpus".into(), json!(gpus.clone())); + if let Some(primary) = gpus.first() { + hardware.insert("primaryGpu".into(), primary.clone()); + } + } + } + } + + json!({ "inventory": inventory }) +} + +pub fn collect_inventory_plain() -> serde_json::Value { + let system = collect_system(); + let meta = build_inventory_metadata(&system); + match meta.get("inventory") { + Some(value) => value.clone(), + None => json!({}), + } +} + +#[cfg(target_os = "linux")] +fn collect_software_linux() -> serde_json::Value { + use std::process::Command; + // Tenta dpkg-query primeiro + let dpkg = Command::new("sh") + .arg("-lc") + .arg("dpkg-query -W -f='${binary:Package}\t${Version}\n' 2>/dev/null || true") + .output(); + if let Ok(out) = dpkg { + if out.status.success() { + let s = String::from_utf8_lossy(&out.stdout); + let mut items = Vec::new(); + for line in s.lines() { + let mut parts = line.split('\t'); + let name = parts.next().unwrap_or("").trim(); + let version = parts.next().unwrap_or("").trim(); + if !name.is_empty() { + items.push(json!({"name": name, "version": version, "source": "dpkg"})); + } + } + return json!(items); + } + } + + // Fallback rpm + let rpm = std::process::Command::new("sh") + .arg("-lc") + .arg("rpm -qa --qf '%{NAME}\t%{VERSION}-%{RELEASE}\n' 2>/dev/null || true") + .output(); + if let Ok(out) = rpm { + if out.status.success() { + let s = String::from_utf8_lossy(&out.stdout); + let mut items = Vec::new(); + for line in s.lines() { + let mut parts = line.split('\t'); + let name = parts.next().unwrap_or("").trim(); + let version = parts.next().unwrap_or("").trim(); + if !name.is_empty() { + items.push(json!({"name": name, "version": version, "source": "rpm"})); + } + } + return json!(items); + } + } + json!([]) +} + +#[cfg(target_os = "linux")] +fn collect_services_linux() -> serde_json::Value { + use std::process::Command; + let out = Command::new("sh") + .arg("-lc") + .arg("systemctl list-units --type=service --state=running --no-pager --no-legend 2>/dev/null || true") + .output(); + if let Ok(out) = out { + if out.status.success() { + let s = String::from_utf8_lossy(&out.stdout); + let mut items = Vec::new(); + for line in s.lines() { + // Typical format: UNIT LOAD ACTIVE SUB DESCRIPTION + // We take UNIT and ACTIVE + let cols: Vec<&str> = line.split_whitespace().collect(); + if cols.is_empty() { + continue; + } + let unit = cols.get(0).unwrap_or(&""); + let active = cols.get(2).copied().unwrap_or(""); + if !unit.is_empty() { + items.push(json!({"name": unit, "status": active})); + } + } + return json!(items); + } + } + json!([]) +} + +#[cfg(target_os = "linux")] +fn collect_linux_extended() -> serde_json::Value { + use std::process::Command; + // lsblk em JSON (block devices) + let block_json = Command::new("sh") + .arg("-lc") + .arg("lsblk -J -b 2>/dev/null || true") + .output() + .ok() + .and_then(|out| { + if out.status.success() { + Some(out.stdout) + } else { + Some(out.stdout) + } + }) + .and_then(|bytes| serde_json::from_slice::(&bytes).ok()) + .unwrap_or_else(|| json!({})); + + // lspci e lsusb — texto livre (depende de pacotes pciutils/usbutils) + let lspci = Command::new("sh") + .arg("-lc") + .arg("lspci 2>/dev/null || true") + .output() + .ok() + .map(|out| String::from_utf8_lossy(&out.stdout).to_string()) + .unwrap_or_default(); + let lsusb = Command::new("sh") + .arg("-lc") + .arg("lsusb 2>/dev/null || true") + .output() + .ok() + .map(|out| String::from_utf8_lossy(&out.stdout).to_string()) + .unwrap_or_default(); + // Parse básico de lspci/lsusb em listas + fn parse_lines_to_list(input: &str) -> Vec { + input + .lines() + .map(|l| l.trim()) + .filter(|l| !l.is_empty()) + .map(|l| json!({ "text": l })) + .collect::>() + } + let pci_list = parse_lines_to_list(&lspci); + let usb_list = parse_lines_to_list(&lsusb); + + // smartctl (se disponível) por disco + let mut smart: Vec = Vec::new(); + if let Ok(out) = std::process::Command::new("sh") + .arg("-lc") + .arg("command -v smartctl >/dev/null 2>&1 && echo yes || echo no") + .output() + { + if out.status.success() && String::from_utf8_lossy(&out.stdout).contains("yes") { + if let Some(devices) = block_json.get("blockdevices").and_then(|v| v.as_array()) { + for dev in devices { + let t = dev.get("type").and_then(|v| v.as_str()).unwrap_or(""); + let name = dev.get("name").and_then(|v| v.as_str()).unwrap_or(""); + if t == "disk" && !name.is_empty() { + let path = format!("/dev/{}", name); + if let Ok(out) = Command::new("sh") + .arg("-lc") + .arg(format!("smartctl -H -j {} 2>/dev/null || true", path)) + .output() + { + if out.status.success() || !out.stdout.is_empty() { + if let Ok(val) = + serde_json::from_slice::(&out.stdout) + { + smart.push(val); + } + } + } + } + } + } + } + } + + json!({ + "linux": { + "lsblk": block_json, + "lspci": lspci, + "lsusb": lsusb, + "pciList": pci_list, + "usbList": usb_list, + "smart": smart, + } + }) +} + +#[cfg(target_os = "windows")] +fn collect_windows_extended() -> serde_json::Value { + use base64::engine::general_purpose::STANDARD; + use base64::Engine as _; + use std::os::windows::process::CommandExt; + use std::process::Command; + const CREATE_NO_WINDOW: u32 = 0x08000000; + + fn decode_powershell_text(bytes: &[u8]) -> Option { + if bytes.is_empty() { + return None; + } + if bytes.starts_with(&[0xFF, 0xFE]) { + return decode_utf16_le_to_string(&bytes[2..]); + } + if bytes.len() >= 2 && bytes[1] == 0 { + if let Some(s) = decode_utf16_le_to_string(bytes) { + return Some(s); + } + } + if bytes.contains(&0) { + if let Some(s) = decode_utf16_le_to_string(bytes) { + return Some(s); + } + } + let text = std::str::from_utf8(bytes).ok()?.trim().to_string(); + if text.is_empty() { + None + } else { + Some(text) + } + } + + fn decode_utf16_le_to_string(bytes: &[u8]) -> Option { + if !bytes.len().is_multiple_of(2) { + return None; + } + let utf16: Vec = bytes + .chunks_exact(2) + .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]])) + .collect(); + let text = String::from_utf16(&utf16).ok()?; + let trimmed = text.trim(); + if trimmed.is_empty() { + None + } else { + Some(trimmed.to_string()) + } + } + + fn preview_base64(bytes: &[u8], max_len: usize) -> String { + if bytes.is_empty() { + return "".to_string(); + } + let prefix = if bytes.len() > max_len { + &bytes[..max_len] + } else { + bytes + }; + format!("base64:{}...", STANDARD.encode(prefix)) + } + + fn encode_ps_script(script: &str) -> String { + let mut bytes = Vec::with_capacity(script.len() * 2); + for unit in script.encode_utf16() { + bytes.extend_from_slice(&unit.to_le_bytes()); + } + STANDARD.encode(bytes) + } + + fn ps(cmd: &str) -> Option { + let script = format!( + "$ErrorActionPreference='SilentlyContinue';$ProgressPreference='SilentlyContinue';$result = & {{\n{}\n}};if ($null -eq $result) {{ return }};$json = $result | ConvertTo-Json -Depth 4 -Compress;if ([string]::IsNullOrWhiteSpace($json)) {{ return }};[Console]::OutputEncoding = [System.Text.Encoding]::UTF8;$json;", + cmd + ); + let encoded = encode_ps_script(&script); + let out = Command::new("powershell") + .creation_flags(CREATE_NO_WINDOW) + .arg("-NoProfile") + .arg("-NoLogo") + .arg("-NonInteractive") + .arg("-ExecutionPolicy") + .arg("Bypass") + .arg("-EncodedCommand") + .arg(encoded) + .output() + .ok()?; + let stdout_text = decode_powershell_text(&out.stdout); + if cfg!(test) { + if let Some(ref txt) = stdout_text { + let preview = txt.chars().take(512).collect::(); + eprintln!("[collect_windows_extended] stdout `{cmd}` => {preview}"); + } else { + let preview = preview_base64(&out.stdout, 512); + eprintln!( + "[collect_windows_extended] stdout `{cmd}` => " + ); + } + if !out.stderr.is_empty() { + if let Some(err) = decode_powershell_text(&out.stderr) { + eprintln!("[collect_windows_extended] stderr `{cmd}` => {err}"); + } else { + let preview = preview_base64(&out.stderr, 512); + eprintln!( + "[collect_windows_extended] stderr `{cmd}` => " + ); + } + } + } + stdout_text.and_then(|text| serde_json::from_str::(&text).ok()) + } + + let software = ps(r#"@(Get-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'; Get-ItemProperty 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*') | Where-Object { $_.DisplayName } | Select-Object DisplayName, DisplayVersion, Publisher"#) + .unwrap_or_else(|| json!([])); + let services = + ps("@(Get-Service | Select-Object Name,Status,DisplayName)").unwrap_or_else(|| json!([])); + let defender = ps("Get-MpComputerStatus | Select-Object AMRunningMode,AntivirusEnabled,RealTimeProtectionEnabled,AntispywareEnabled").unwrap_or_else(|| json!({})); + let hotfix = ps("Get-HotFix | Select-Object HotFixID,InstalledOn").unwrap_or_else(|| json!([])); + let bitlocker = ps( + "@(if (Get-Command -Name Get-BitLockerVolume -ErrorAction SilentlyContinue) { Get-BitLockerVolume | Select-Object MountPoint,VolumeStatus,ProtectionStatus,LockStatus,EncryptionMethod,EncryptionPercentage,CapacityGB,KeyProtector } else { @() })", + ) + .unwrap_or_else(|| json!([])); + let tpm = ps( + "if (Get-Command -Name Get-Tpm -ErrorAction SilentlyContinue) { Get-Tpm | Select-Object TpmPresent,TpmReady,TpmEnabled,TpmActivated,ManagedAuthLevel,OwnerAuth,ManufacturerId,ManufacturerIdTxt,ManufacturerVersion,ManufacturerVersionFull20,SpecVersion } else { $null }", + ) + .unwrap_or_else(|| json!({})); + let secure_boot = ps( + r#" + if (-not (Get-Command -Name Confirm-SecureBootUEFI -ErrorAction SilentlyContinue)) { + [PSCustomObject]@{ Supported = $false; Enabled = $null; Error = 'Cmdlet Confirm-SecureBootUEFI indisponível' } + } else { + try { + $enabled = Confirm-SecureBootUEFI + [PSCustomObject]@{ Supported = $true; Enabled = [bool]$enabled; Error = $null } + } catch { + [PSCustomObject]@{ Supported = $true; Enabled = $null; Error = $_.Exception.Message } + } + } + "#, + ) + .unwrap_or_else(|| json!({})); + let device_guard = ps( + "@(Get-CimInstance -ClassName Win32_DeviceGuard | Select-Object SecurityServicesConfigured,SecurityServicesRunning,RequiredSecurityProperties,AvailableSecurityProperties,VirtualizationBasedSecurityStatus)", + ) + .unwrap_or_else(|| json!([])); + let firewall_profiles = ps( + "@(Get-NetFirewallProfile | Select-Object Name,Enabled,DefaultInboundAction,DefaultOutboundAction,NotifyOnListen)", + ) + .unwrap_or_else(|| json!([])); + let windows_update = ps( + r#" + $reg = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update' -ErrorAction SilentlyContinue + if ($null -eq $reg) { return $null } + $last = $null + if ($reg.PSObject.Properties.Name -contains 'LastSuccessTime') { + $raw = $reg.LastSuccessTime + if ($raw) { + try { + if ($raw -is [DateTime]) { + $last = ($raw.ToUniversalTime()).ToString('o') + } elseif ($raw -is [string]) { + $last = $raw + } else { + $last = [DateTime]::FromFileTimeUtc([long]$raw).ToString('o') + } + } catch { + $last = $raw + } + } + } + [PSCustomObject]@{ + AUOptions = $reg.AUOptions + NoAutoUpdate = $reg.NoAutoUpdate + ScheduledInstallDay = $reg.ScheduledInstallDay + ScheduledInstallTime = $reg.ScheduledInstallTime + DetectionFrequency = $reg.DetectionFrequencyEnabled + LastSuccessTime = $last + } + "#, + ) + .unwrap_or_else(|| json!({})); + let computer_system = ps( + "Get-CimInstance Win32_ComputerSystem | Select-Object Manufacturer,Model,Domain,DomainRole,PartOfDomain,Workgroup,TotalPhysicalMemory,HypervisorPresent,PCSystemType,PCSystemTypeEx", + ) + .unwrap_or_else(|| json!({})); + let device_join = ps( + r#" + $output = & dsregcmd.exe /status 2>$null + if (-not $output) { return $null } + $map = [ordered]@{} + $current = $null + foreach ($line in $output) { + if ([string]::IsNullOrWhiteSpace($line)) { continue } + if ($line -match '^\[(.+)\]$') { + $current = $matches[1].Trim() + if (-not $map.Contains($current)) { + $map[$current] = [ordered]@{} + } + continue + } + if (-not $current) { continue } + $parts = $line.Split(':', 2) + if ($parts.Length -ne 2) { continue } + $key = $parts[0].Trim() + $value = $parts[1].Trim() + if ($key) { + ($map[$current])[$key] = $value + } + } + if ($map.Count -eq 0) { return $null } + $obj = [ordered]@{} + foreach ($entry in $map.GetEnumerator()) { + $obj[$entry.Key] = [PSCustomObject]$entry.Value + } + [PSCustomObject]$obj + "#, + ) + .unwrap_or_else(|| json!({})); + + // Informações de build/edição e ativação + let os_info = ps(r#" + $cv = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'; + $os = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue; + $lsItems = Get-CimInstance -Query "SELECT Name, LicenseStatus, PartialProductKey FROM SoftwareLicensingProduct WHERE PartialProductKey IS NOT NULL" | Where-Object { $_.Name -like 'Windows*' }; + $activatedItem = $lsItems | Where-Object { $_.LicenseStatus -eq 1 } | Select-Object -First 1; + $primaryItem = if ($activatedItem) { $activatedItem } else { $lsItems | Select-Object -First 1 }; + $lsCode = if ($primaryItem -and $primaryItem.LicenseStatus -ne $null) { [int]$primaryItem.LicenseStatus } else { 0 }; + [PSCustomObject]@{ + ProductName = $cv.ProductName + CurrentBuild = $cv.CurrentBuild + CurrentBuildNumber = $cv.CurrentBuildNumber + DisplayVersion = $cv.DisplayVersion + ReleaseId = $cv.ReleaseId + EditionID = $cv.EditionID + UBR = $cv.UBR + CompositionEditionID = $cv.CompositionEditionID + InstallationType = $cv.InstallationType + InstallDate = $cv.InstallDate + InstallationDate = $os.InstallDate + InstalledOn = $os.InstallDate + Version = $os.Version + BuildNumber = $os.BuildNumber + Caption = $os.Caption + FeatureExperiencePack = $cv.FeatureExperiencePack + LicenseStatus = $lsCode + IsActivated = ($activatedItem -ne $null) + } + "#).unwrap_or_else(|| json!({})); + + // Hardware detalhado (CPU/Board/BIOS/Memória/Vídeo/Discos) + let cpu = ps("Get-CimInstance Win32_Processor | Select-Object Name,Manufacturer,SocketDesignation,NumberOfCores,NumberOfLogicalProcessors,L2CacheSize,L3CacheSize,MaxClockSpeed").unwrap_or_else(|| json!({})); + let baseboard = ps( + "Get-CimInstance Win32_BaseBoard | Select-Object Product,Manufacturer,SerialNumber,Version", + ) + .unwrap_or_else(|| json!({})); + let bios = ps("Get-CimInstance Win32_BIOS | Select-Object Manufacturer,SMBIOSBIOSVersion,ReleaseDate,Version").unwrap_or_else(|| json!({})); + let memory = ps("@(Get-CimInstance Win32_PhysicalMemory | Select-Object BankLabel,Capacity,Manufacturer,PartNumber,SerialNumber,ConfiguredClockSpeed,Speed,ConfiguredVoltage)").unwrap_or_else(|| json!([])); + // Coleta de GPU com VRAM correta (nvidia-smi para NVIDIA, registro como fallback para >4GB) + let video = ps(r#" + $gpus = @() + $wmiGpus = Get-CimInstance Win32_VideoController | Select-Object Name,AdapterRAM,DriverVersion,PNPDeviceID + foreach ($gpu in $wmiGpus) { + $vram = $gpu.AdapterRAM + # Tenta nvidia-smi para GPUs NVIDIA (retorna valor correto para >4GB) + if ($gpu.Name -match 'NVIDIA') { + try { + $nvidiaSmi = & 'nvidia-smi' '--query-gpu=memory.total' '--format=csv,noheader,nounits' 2>$null + if ($nvidiaSmi) { + $vramMB = [int64]($nvidiaSmi.Trim()) + $vram = $vramMB * 1024 * 1024 + } + } catch {} + } + # Fallback: tenta registro do Windows (qwMemorySize é uint64) + if ($vram -le 4294967296 -and $vram -gt 0) { + try { + $regPath = 'HKLM:\SYSTEM\ControlSet001\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}\0*' + $regGpus = Get-ItemProperty $regPath -ErrorAction SilentlyContinue + foreach ($reg in $regGpus) { + if ($reg.DriverDesc -eq $gpu.Name -and $reg.'HardwareInformation.qwMemorySize') { + $vram = [int64]$reg.'HardwareInformation.qwMemorySize' + break + } + } + } catch {} + } + $gpus += [PSCustomObject]@{ + Name = $gpu.Name + AdapterRAM = $vram + DriverVersion = $gpu.DriverVersion + PNPDeviceID = $gpu.PNPDeviceID + } + } + @($gpus) + "#).unwrap_or_else(|| json!([])); + let disks = ps("@(Get-CimInstance Win32_DiskDrive | Select-Object Model,SerialNumber,Size,InterfaceType,MediaType)").unwrap_or_else(|| json!([])); + + // Bateria (notebooks/laptops) + let battery = ps(r#" + $batteries = @(Get-CimInstance Win32_Battery | Select-Object Name,DeviceID,Status,BatteryStatus,EstimatedChargeRemaining,EstimatedRunTime,DesignCapacity,FullChargeCapacity,DesignVoltage,Chemistry,BatteryRechargeTime) + if ($batteries.Count -eq 0) { + [PSCustomObject]@{ Present = $false; Batteries = @() } + } else { + # Mapeia status numérico para texto + $statusMap = @{ + 1 = 'Discharging' + 2 = 'AC Power' + 3 = 'Fully Charged' + 4 = 'Low' + 5 = 'Critical' + 6 = 'Charging' + 7 = 'Charging High' + 8 = 'Charging Low' + 9 = 'Charging Critical' + 10 = 'Undefined' + 11 = 'Partially Charged' + } + foreach ($b in $batteries) { + if ($b.BatteryStatus) { + $b | Add-Member -NotePropertyName 'BatteryStatusText' -NotePropertyValue ($statusMap[[int]$b.BatteryStatus] ?? 'Unknown') -Force + } + } + [PSCustomObject]@{ Present = $true; Batteries = $batteries } + } + "#).unwrap_or_else(|| json!({ "Present": false, "Batteries": [] })); + + // Sensores térmicos (temperatura CPU/GPU quando disponível) + let thermal = ps(r#" + $temps = @() + # Tenta WMI thermal zone (requer admin em alguns sistemas) + try { + $zones = Get-CimInstance -Namespace 'root/WMI' -ClassName MSAcpi_ThermalZoneTemperature -ErrorAction SilentlyContinue + foreach ($z in $zones) { + if ($z.CurrentTemperature) { + $celsius = [math]::Round(($z.CurrentTemperature - 2732) / 10, 1) + $temps += [PSCustomObject]@{ + Source = 'ThermalZone' + Name = $z.InstanceName + TemperatureCelsius = $celsius + CriticalTripPoint = if ($z.CriticalTripPoint) { [math]::Round(($z.CriticalTripPoint - 2732) / 10, 1) } else { $null } + } + } + } + } catch {} + # CPU temp via Open Hardware Monitor WMI (se instalado) + try { + $ohm = Get-CimInstance -Namespace 'root/OpenHardwareMonitor' -ClassName Sensor -ErrorAction SilentlyContinue | Where-Object { $_.SensorType -eq 'Temperature' } + foreach ($s in $ohm) { + $temps += [PSCustomObject]@{ + Source = 'OpenHardwareMonitor' + Name = $s.Name + TemperatureCelsius = $s.Value + Parent = $s.Parent + } + } + } catch {} + @($temps) + "#).unwrap_or_else(|| json!([])); + + // Adaptadores de rede (físicos e virtuais) + let network_adapters = ps(r#" + @(Get-CimInstance Win32_NetworkAdapter | Where-Object { $_.PhysicalAdapter -eq $true -or $_.NetConnectionStatus -ne $null } | Select-Object Name,Description,MACAddress,Speed,NetConnectionStatus,AdapterType,Manufacturer,NetConnectionID,PNPDeviceID | ForEach-Object { + $statusMap = @{ + 0 = 'Disconnected' + 1 = 'Connecting' + 2 = 'Connected' + 3 = 'Disconnecting' + 4 = 'Hardware not present' + 5 = 'Hardware disabled' + 6 = 'Hardware malfunction' + 7 = 'Media disconnected' + 8 = 'Authenticating' + 9 = 'Authentication succeeded' + 10 = 'Authentication failed' + 11 = 'Invalid address' + 12 = 'Credentials required' + } + $_ | Add-Member -NotePropertyName 'StatusText' -NotePropertyValue ($statusMap[[int]$_.NetConnectionStatus] ?? 'Unknown') -Force + $_ + }) + "#).unwrap_or_else(|| json!([])); + + // Monitores conectados + let monitors = ps(r#" + @(Get-CimInstance WmiMonitorID -Namespace root/wmi -ErrorAction SilentlyContinue | ForEach-Object { + $decode = { param($arr) if ($arr) { -join ($arr | Where-Object { $_ -ne 0 } | ForEach-Object { [char]$_ }) } else { $null } } + [PSCustomObject]@{ + ManufacturerName = & $decode $_.ManufacturerName + ProductCodeID = & $decode $_.ProductCodeID + SerialNumberID = & $decode $_.SerialNumberID + UserFriendlyName = & $decode $_.UserFriendlyName + YearOfManufacture = $_.YearOfManufacture + WeekOfManufacture = $_.WeekOfManufacture + } + }) + "#).unwrap_or_else(|| json!([])); + + // Fonte de alimentação / chassis + let power_supply = ps(r#" + $chassis = Get-CimInstance Win32_SystemEnclosure | Select-Object ChassisTypes,Manufacturer,SerialNumber,SMBIOSAssetTag + $chassisTypeMap = @{ + 1 = 'Other'; 2 = 'Unknown'; 3 = 'Desktop'; 4 = 'Low Profile Desktop' + 5 = 'Pizza Box'; 6 = 'Mini Tower'; 7 = 'Tower'; 8 = 'Portable' + 9 = 'Laptop'; 10 = 'Notebook'; 11 = 'Hand Held'; 12 = 'Docking Station' + 13 = 'All in One'; 14 = 'Sub Notebook'; 15 = 'Space-Saving'; 16 = 'Lunch Box' + 17 = 'Main Server Chassis'; 18 = 'Expansion Chassis'; 19 = 'SubChassis' + 20 = 'Bus Expansion Chassis'; 21 = 'Peripheral Chassis'; 22 = 'RAID Chassis' + 23 = 'Rack Mount Chassis'; 24 = 'Sealed-case PC'; 25 = 'Multi-system chassis' + 30 = 'Tablet'; 31 = 'Convertible'; 32 = 'Detachable' + } + $types = @() + if ($chassis.ChassisTypes) { + foreach ($t in $chassis.ChassisTypes) { + $types += $chassisTypeMap[[int]$t] ?? "Type$t" + } + } + [PSCustomObject]@{ + ChassisTypes = $chassis.ChassisTypes + ChassisTypesText = $types + Manufacturer = $chassis.Manufacturer + SerialNumber = $chassis.SerialNumber + SMBIOSAssetTag = $chassis.SMBIOSAssetTag + } + "#).unwrap_or_else(|| json!({})); + + // Último reinício e contagem de boots + let boot_info = ps(r#" + $os = Get-CimInstance Win32_OperatingSystem | Select-Object LastBootUpTime + $lastBoot = $os.LastBootUpTime + + # Calcula uptime + $uptime = if ($lastBoot) { (New-TimeSpan -Start $lastBoot -End (Get-Date)).TotalSeconds } else { 0 } + + # Conta eventos de boot (ID 6005) - últimos 30 dias para performance + $startDate = (Get-Date).AddDays(-30) + $bootEvents = @() + $bootCount = 0 + try { + $events = Get-WinEvent -FilterHashtable @{ + LogName = 'System' + ID = 6005 + StartTime = $startDate + } -MaxEvents 50 -ErrorAction SilentlyContinue + $bootCount = @($events).Count + $bootEvents = @($events | Select-Object -First 10 | ForEach-Object { + @{ + TimeCreated = $_.TimeCreated.ToString('o') + Computer = $_.MachineName + } + }) + } catch {} + + [PSCustomObject]@{ + LastBootTime = if ($lastBoot) { $lastBoot.ToString('o') } else { $null } + UptimeSeconds = [math]::Round($uptime) + BootCountLast30Days = $bootCount + RecentBoots = $bootEvents + } + "#).unwrap_or_else(|| json!({ "LastBootTime": null, "UptimeSeconds": 0, "BootCountLast30Days": 0, "RecentBoots": [] })); + + json!({ + "windows": { + "software": software, + "services": services, + "defender": defender, + "hotfix": hotfix, + "osInfo": os_info, + "cpu": cpu, + "baseboard": baseboard, + "bios": bios, + "memoryModules": memory, + "videoControllers": video, + "disks": disks, + "bitLocker": bitlocker, + "tpm": tpm, + "secureBoot": secure_boot, + "deviceGuard": device_guard, + "firewallProfiles": firewall_profiles, + "windowsUpdate": windows_update, + "computerSystem": computer_system, + "azureAdStatus": device_join, + "battery": battery, + "thermal": thermal, + "networkAdapters": network_adapters, + "monitors": monitors, + "chassis": power_supply, + "bootInfo": boot_info, + } + }) +} + +#[cfg(target_os = "macos")] +fn collect_macos_extended() -> serde_json::Value { + use std::process::Command; + // system_profiler em JSON (pode ser pesado; limitar a alguns tipos) + let profiler = Command::new("sh") + .arg("-lc") + .arg("system_profiler -json SPHardwareDataType SPSoftwareDataType SPNetworkDataType SPStorageDataType SPDisplaysDataType 2>/dev/null || true") + .output() + .ok() + .and_then(|out| serde_json::from_slice::(&out.stdout).ok()) + .unwrap_or_else(|| json!({})); + let pkgs = Command::new("sh") + .arg("-lc") + .arg("pkgutil --pkgs 2>/dev/null || true") + .output() + .ok() + .map(|out| { + String::from_utf8_lossy(&out.stdout) + .lines() + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect::>() + }) + .unwrap_or_default(); + let services_text = Command::new("sh") + .arg("-lc") + .arg("launchctl list 2>/dev/null || true") + .output() + .ok() + .map(|out| String::from_utf8_lossy(&out.stdout).to_string()) + .unwrap_or_default(); + + json!({ + "macos": { + "systemProfiler": profiler, + "packages": pkgs, + "launchctl": services_text, + } + }) +} + +fn collect_system() -> System { + let mut system = System::new_all(); + system.refresh_all(); + system +} + +fn collect_metrics(system: &System) -> MachineMetrics { + let collected_at = Utc::now(); + let total_memory = system.total_memory(); + let used_memory = system.used_memory(); + // sysinfo 0.31: valores já em bytes + let memory_total_bytes = total_memory; + let memory_used_bytes = used_memory; + let memory_used_percent = if total_memory > 0 { + (used_memory as f32 / total_memory as f32) * 100.0 + } else { + 0.0 + }; + + let load = System::load_average(); + let cpu_usage_percent = system.global_cpu_usage(); + let cpu_logical_cores = system.cpus().len(); + let cpu_physical_cores = system.physical_core_count(); + + MachineMetrics { + collected_at, + cpu_logical_cores, + cpu_physical_cores, + cpu_usage_percent, + load_average_one: Some(load.one), + load_average_five: Some(load.five), + load_average_fifteen: Some(load.fifteen), + memory_total_bytes, + memory_used_bytes, + memory_used_percent, + uptime_seconds: System::uptime(), + } +} + +pub fn collect_profile() -> Result { + let hostname = hostname::get() + .map_err(|_| AgentError::Hostname)? + .to_string_lossy() + .trim() + .to_string(); + + let system = collect_system(); + + let os_name = System::name() + .or_else(System::long_os_version) + .unwrap_or_else(|| "desconhecido".to_string()); + let os_version = System::os_version(); + let architecture = std::env::consts::ARCH.to_string(); + + let mac_addresses = collect_mac_addresses(); + let serials: Vec = collect_serials(); + + if mac_addresses.is_empty() && serials.is_empty() { + return Err(AgentError::MissingIdentifiers); + } + + let metrics = collect_metrics(&system); + let cpu_brand = system + .cpus() + .first() + .map(|cpu| cpu.brand().to_string()) + .filter(|brand| !brand.trim().is_empty()); + + let inventory = MachineInventory { + cpu_brand, + host_identifier: serials.first().cloned(), + }; + + Ok(MachineProfile { + hostname, + os: MachineOs { + name: os_name, + version: os_version, + architecture: Some(architecture), + }, + mac_addresses, + serial_numbers: serials, + inventory, + metrics, + }) +} + +static HTTP_CLIENT: Lazy = Lazy::new(|| { + reqwest::Client::builder() + .user_agent("sistema-de-chamados-agent/1.0") + .timeout(Duration::from_secs(20)) + .use_rustls_tls() + .build() + .expect("failed to build http client") +}); + +async fn post_heartbeat( + base_url: &str, + token: &str, + status: Option, +) -> Result<(), AgentError> { + let system = collect_system(); + let metrics = collect_metrics(&system); + let hostname = hostname::get() + .map_err(|_| AgentError::Hostname)? + .to_string_lossy() + .into_owned(); + let os = MachineOs { + name: System::name() + .or_else(System::long_os_version) + .unwrap_or_else(|| "desconhecido".to_string()), + version: System::os_version(), + architecture: Some(std::env::consts::ARCH.to_string()), + }; + + let payload = HeartbeatPayload { + machine_token: token.to_string(), + status, + hostname: Some(hostname), + os: Some(os), + metrics: Some(metrics), + metadata: Some(build_inventory_metadata(&system)), + }; + + let url = format!("{}/api/machines/heartbeat", base_url); + HTTP_CLIENT.post(url).json(&payload).send().await?; + Ok(()) +} + +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct UsbPolicyResponse { + pending: bool, + policy: Option, + #[allow(dead_code)] + applied_at: Option, +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +struct UsbPolicyStatusReport { + machine_token: String, + status: String, + error: Option, + current_policy: Option, +} + +async fn check_and_apply_usb_policy(base_url: &str, token: &str) { + crate::log_info!("Verificando politica USB pendente..."); + + let url = format!("{}/api/machines/usb-policy?machineToken={}", base_url, token); + + let response = match HTTP_CLIENT.get(&url).send().await { + Ok(resp) => { + crate::log_info!("Resposta da verificacao de politica USB: status={}", resp.status()); + resp + } + Err(e) => { + crate::log_error!("Falha ao verificar politica USB: {e}"); + return; + } + }; + + let policy_response: UsbPolicyResponse = match response.json().await { + Ok(data) => data, + Err(e) => { + crate::log_error!("Falha ao parsear resposta de politica USB: {e}"); + return; + } + }; + + if !policy_response.pending { + crate::log_info!("Nenhuma politica USB pendente"); + return; + } + + let policy_str = match policy_response.policy { + Some(p) => p, + None => { + crate::log_warn!("Politica USB pendente mas sem valor de policy"); + return; + } + }; + + crate::log_info!("Politica USB pendente encontrada: {}", policy_str); + + #[cfg(target_os = "windows")] + { + use crate::usb_control::{get_current_policy, UsbPolicy}; + use crate::service_client; + + let policy = match UsbPolicy::from_str(&policy_str) { + Some(p) => p, + None => { + crate::log_error!("Politica USB invalida: {}", policy_str); + report_usb_policy_status(base_url, token, "FAILED", Some(format!("Politica invalida: {}", policy_str)), None).await; + return; + } + }; + + // Verifica se a politica ja esta aplicada localmente + match get_current_policy() { + Ok(current) if current == policy => { + crate::log_info!("Politica USB ja esta aplicada localmente: {}", policy_str); + let reported = report_usb_policy_status(base_url, token, "APPLIED", None, Some(policy_str.clone())).await; + if !reported { + crate::log_error!("Falha ao reportar politica ja aplicada"); + } + return; + } + Ok(current) => { + crate::log_info!("Politica atual: {:?}, esperada: {:?}", current, policy); + } + Err(e) => { + crate::log_warn!("Nao foi possivel ler politica atual: {e}"); + } + } + + crate::log_info!("Aplicando politica USB: {}", policy_str); + + // Reporta APPLYING para progress bar real no frontend + let _ = report_usb_policy_status(base_url, token, "APPLYING", None, None).await; + + // Tenta primeiro via RavenService (privilegiado) + crate::log_info!("Tentando aplicar politica via RavenService..."); + match service_client::apply_usb_policy(&policy_str) { + Ok(result) => { + if result.success { + crate::log_info!("Politica USB aplicada com sucesso via RavenService: {:?}", result); + let reported = report_usb_policy_status(base_url, token, "APPLIED", None, Some(policy_str.clone())).await; + if !reported { + crate::log_error!("CRITICO: Politica aplicada mas falha ao reportar ao servidor!"); + let base_url = base_url.to_string(); + let token = token.to_string(); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(60)).await; + crate::log_info!("Retry agendado: reportando politica USB..."); + let _ = report_usb_policy_status(&base_url, &token, "APPLIED", None, Some(policy_str)).await; + }); + } + return; + } else { + let err_msg = result.error.unwrap_or_else(|| "Erro desconhecido".to_string()); + crate::log_error!("RavenService retornou erro: {}", err_msg); + report_usb_policy_status(base_url, token, "FAILED", Some(err_msg), None).await; + } + } + Err(service_client::ServiceClientError::ServiceUnavailable(msg)) => { + crate::log_warn!("RavenService nao disponivel: {}", msg); + // Tenta fallback direto (vai falhar se nao tiver privilegio) + crate::log_info!("Tentando aplicar politica diretamente..."); + match crate::usb_control::apply_usb_policy(policy) { + Ok(result) => { + crate::log_info!("Politica USB aplicada com sucesso (direto): {:?}", result); + let reported = report_usb_policy_status(base_url, token, "APPLIED", None, Some(policy_str.clone())).await; + if !reported { + crate::log_error!("CRITICO: Politica aplicada mas falha ao reportar ao servidor!"); + let base_url = base_url.to_string(); + let token = token.to_string(); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(60)).await; + crate::log_info!("Retry agendado: reportando politica USB..."); + let _ = report_usb_policy_status(&base_url, &token, "APPLIED", None, Some(policy_str)).await; + }); + } + } + Err(e) => { + let err_msg = format!("RavenService indisponivel e aplicacao direta falhou: {}. Instale ou inicie o RavenService.", e); + crate::log_error!("{}", err_msg); + report_usb_policy_status(base_url, token, "FAILED", Some(err_msg), None).await; + } + } + } + Err(e) => { + crate::log_error!("Falha ao comunicar com RavenService: {e}"); + report_usb_policy_status(base_url, token, "FAILED", Some(e.to_string()), None).await; + } + } + } + + #[cfg(not(target_os = "windows"))] + { + crate::log_warn!("Controle de USB nao suportado neste sistema operacional"); + report_usb_policy_status(base_url, token, "FAILED", Some("Sistema operacional nao suportado".to_string()), None).await; + } +} + +async fn report_usb_policy_status( + base_url: &str, + token: &str, + status: &str, + error: Option, + current_policy: Option, +) -> bool { + let url = format!("{}/api/machines/usb-policy", base_url); + + let report = UsbPolicyStatusReport { + machine_token: token.to_string(), + status: status.to_string(), + error, + current_policy, + }; + + crate::log_info!("Reportando status de politica USB: status={}", status); + + // Retry simples: 1 tentativa imediata + 1 retry após 2s + let delays = [2]; + let mut last_error = None; + + for (attempt, delay_secs) in delays.iter().enumerate() { + match HTTP_CLIENT.post(&url).json(&report).send().await { + Ok(response) => { + let status_code = response.status(); + if status_code.is_success() { + crate::log_info!( + "Report de politica USB enviado com sucesso na tentativa {}", + attempt + 1 + ); + return true; + } else { + let body = response.text().await.unwrap_or_default(); + last_error = Some(format!("HTTP {} - {}", status_code, body)); + crate::log_warn!( + "Report de politica USB falhou (tentativa {}): HTTP {}", + attempt + 1, + status_code + ); + } + } + Err(e) => { + last_error = Some(e.to_string()); + crate::log_warn!( + "Report de politica USB falhou (tentativa {}): {}", + attempt + 1, + e + ); + } + } + + if attempt < delays.len() - 1 { + crate::log_info!("Retentando report de politica USB em {}s...", delay_secs); + tokio::time::sleep(Duration::from_secs(*delay_secs)).await; + } + } + + if let Some(err) = last_error { + crate::log_error!( + "Falha ao reportar status de politica USB apos {} tentativas: {err}", + delays.len() + ); + } + + false +} + +struct HeartbeatHandle { + token: String, + base_url: String, + stop_signal: Arc, + join_handle: JoinHandle<()>, +} + +impl HeartbeatHandle { + fn stop(self) { + self.stop_signal.notify_waiters(); + self.join_handle.abort(); + } +} + +#[derive(Default, Clone)] +pub struct AgentRuntime { + inner: Arc>>, +} + +fn sanitize_base_url(input: &str) -> Result { + let trimmed = input.trim().trim_end_matches('/'); + if trimmed.is_empty() { + return Err(AgentError::InvalidApiUrl); + } + Ok(trimmed.to_string()) +} + +impl AgentRuntime { + pub fn new() -> Self { + Self { + inner: Arc::new(Mutex::new(None)), + } + } + + pub fn start_heartbeat( + &self, + base_url: String, + token: String, + status: Option, + interval_seconds: Option, + ) -> Result<(), AgentError> { + let sanitized_base = sanitize_base_url(&base_url)?; + let interval = interval_seconds.unwrap_or(300).max(60); + + { + let mut guard = self.inner.lock(); + if let Some(handle) = guard.take() { + if handle.token == token && handle.base_url == sanitized_base { + // Reuse existing heartbeat; keep running. + *guard = Some(handle); + return Ok(()); + } + handle.stop(); + } + } + + let stop_signal = Arc::new(Notify::new()); + let stop_signal_clone = stop_signal.clone(); + let token_clone = token.clone(); + let base_clone = sanitized_base.clone(); + let status_clone = status.clone(); + + let join_handle = async_runtime::spawn(async move { + crate::log_info!("Loop de agente iniciado"); + + if let Err(error) = + post_heartbeat(&base_clone, &token_clone, status_clone.clone()).await + { + crate::log_error!("Falha inicial ao enviar heartbeat: {error}"); + } else { + crate::log_info!("Heartbeat inicial enviado com sucesso"); + } + + // Verifica politica USB apos heartbeat inicial + check_and_apply_usb_policy(&base_clone, &token_clone).await; + + let mut heartbeat_ticker = tokio::time::interval(Duration::from_secs(interval)); + heartbeat_ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); + let mut usb_ticker = tokio::time::interval(Duration::from_secs(15)); + usb_ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); + + loop { + // Wait interval + tokio::select! { + _ = stop_signal_clone.notified() => { + crate::log_info!("Loop de agente encerrado por sinal de parada"); + break; + } + _ = heartbeat_ticker.tick() => {} + _ = usb_ticker.tick() => { + check_and_apply_usb_policy(&base_clone, &token_clone).await; + continue; + } + } + + if let Err(error) = + post_heartbeat(&base_clone, &token_clone, status_clone.clone()).await + { + crate::log_error!("Falha ao enviar heartbeat: {error}"); + } + + // Verifica politica USB apos cada heartbeat + check_and_apply_usb_policy(&base_clone, &token_clone).await; + } + }); + + let handle = HeartbeatHandle { + token, + base_url: sanitized_base, + stop_signal, + join_handle, + }; + + let mut guard = self.inner.lock(); + *guard = Some(handle); + + Ok(()) + } + + pub fn stop(&self) { + let mut guard = self.inner.lock(); + if let Some(handle) = guard.take() { + handle.stop(); + } + } +} + +#[cfg(all(test, target_os = "windows"))] +mod windows_tests { + use super::collect_windows_extended; + use serde_json::Value; + + fn expect_object<'a>(value: &'a Value, context: &str) -> &'a serde_json::Map { + value + .as_object() + .unwrap_or_else(|| panic!("{context} não é um objeto JSON: {value:?}")) + } + + #[test] + fn collects_activation_and_defender_status() { + let extended = collect_windows_extended(); + let windows = extended.get("windows").unwrap_or_else(|| { + panic!("payload windows ausente: {extended:?}"); + }); + let windows_obj = expect_object(windows, "windows"); + + let os_info = windows_obj + .get("osInfo") + .unwrap_or_else(|| panic!("windows.osInfo ausente: {windows_obj:?}")); + let os_info_obj = expect_object(os_info, "windows.osInfo"); + + let is_activated = os_info_obj.get("IsActivated").unwrap_or_else(|| { + panic!("campo IsActivated ausente em windows.osInfo: {os_info_obj:?}") + }); + assert!( + is_activated.as_bool().is_some(), + "esperava booleano em windows.osInfo.IsActivated, valor recebido: {is_activated:?}" + ); + + let license_status = os_info_obj.get("LicenseStatus").unwrap_or_else(|| { + panic!("campo LicenseStatus ausente em windows.osInfo: {os_info_obj:?}") + }); + assert!( + license_status.as_i64().is_some(), + "esperava número em windows.osInfo.LicenseStatus, valor recebido: {license_status:?}" + ); + + let defender = windows_obj.get("defender").unwrap_or_else(|| { + panic!("windows.defender ausente: {windows_obj:?}"); + }); + let defender_obj = expect_object(defender, "windows.defender"); + + let realtime = defender_obj + .get("RealTimeProtectionEnabled") + .unwrap_or_else(|| { + panic!( + "campo RealTimeProtectionEnabled ausente em windows.defender: {defender_obj:?}" + ) + }); + assert!( + realtime.as_bool().is_some(), + "esperava booleano em windows.defender.RealTimeProtectionEnabled, valor recebido: {realtime:?}" + ); + } +} diff --git a/apps/desktop/src-tauri/src/chat.rs b/apps/desktop/src-tauri/src/chat.rs new file mode 100644 index 0000000..57b5700 --- /dev/null +++ b/apps/desktop/src-tauri/src/chat.rs @@ -0,0 +1,1423 @@ +//! Modulo de Chat em Tempo Real +//! +//! Este modulo implementa o sistema de chat entre agentes (dashboard web) +//! e clientes (Raven desktop). Usa Server-Sent Events (SSE) como metodo +//! primario para atualizacoes em tempo real, com fallback para HTTP polling. + +use convex::{ConvexClient, FunctionResult, Value}; +use futures_util::StreamExt; +use once_cell::sync::Lazy; +use parking_lot::Mutex; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, HashMap}; +use std::fs; +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use tauri::async_runtime::JoinHandle; +use tauri::{Emitter, Manager, WebviewWindowBuilder, WebviewUrl}; +use tauri_plugin_notification::NotificationExt; + +// ============================================================================ +// TYPES +// ============================================================================ + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChatSession { + pub session_id: String, + pub ticket_id: String, + pub ticket_ref: u64, + pub ticket_subject: String, + pub agent_name: String, + pub agent_email: Option, + pub agent_avatar_url: Option, + pub unread_count: u32, + pub last_activity_at: i64, + pub started_at: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChatMessage { + pub id: String, + pub body: String, + pub author_name: String, + pub author_avatar_url: Option, + pub is_from_machine: bool, + pub created_at: i64, + pub attachments: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChatAttachment { + pub storage_id: String, + pub name: String, + pub size: Option, + #[serde(rename = "type")] + pub mime_type: Option, +} + +#[allow(dead_code)] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChatPollResponse { + pub has_active_sessions: bool, + pub sessions: Vec, + pub total_unread: u32, +} + +#[allow(dead_code)] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChatSessionSummary { + pub ticket_id: String, + pub ticket_ref: u64, + pub unread_count: u32, + pub last_activity_at: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChatMessagesResponse { + pub messages: Vec, + pub has_session: bool, + #[serde(default)] + pub unread_count: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SendMessageResponse { + pub message_id: String, + pub created_at: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionStartedEvent { + pub session: ChatSession, +} + +// ============================================================================ +// PERSISTENCIA DE ESTADO +// ============================================================================ + +/// Estado persistido do chat para sobreviver a restarts +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ChatPersistedState { + last_unread_count: u32, + sessions: Vec, + saved_at: u64, // Unix timestamp em ms +} + +const STATE_FILE_NAME: &str = "chat-state.json"; +const STATE_MAX_AGE_MS: u64 = 3600_000; // 1 hora - ignorar estados mais antigos + +fn get_state_file_path() -> Option { + dirs::data_local_dir().map(|p| p.join("Raven").join(STATE_FILE_NAME)) +} + +fn save_chat_state(last_unread: u32, sessions: &[ChatSession]) { + let Some(path) = get_state_file_path() else { + return; + }; + + // Criar diretorio se nao existir + if let Some(parent) = path.parent() { + let _ = fs::create_dir_all(parent); + } + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or(0); + + let state = ChatPersistedState { + last_unread_count: last_unread, + sessions: sessions.to_vec(), + saved_at: now, + }; + + if let Ok(json) = serde_json::to_string_pretty(&state) { + let _ = fs::write(&path, json); + crate::log_info!("[CHAT] Estado persistido: unread={}, sessions={}", last_unread, sessions.len()); + } +} + +fn load_chat_state() -> Option { + let path = get_state_file_path()?; + + let json = fs::read_to_string(&path).ok()?; + let state: ChatPersistedState = serde_json::from_str(&json).ok()?; + + // Verificar se estado nao esta muito antigo + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or(0); + + if now.saturating_sub(state.saved_at) > STATE_MAX_AGE_MS { + crate::log_info!("[CHAT] Estado persistido ignorado (muito antigo)"); + return None; + } + + crate::log_info!( + "[CHAT] Estado restaurado: unread={}, sessions={}", + state.last_unread_count, state.sessions.len() + ); + Some(state) +} + +// ============================================================================ +// HTTP CLIENT +// ============================================================================ + +static CHAT_CLIENT: Lazy = Lazy::new(|| { + Client::builder() + .user_agent("raven-chat/1.0") + .timeout(Duration::from_secs(15)) + .use_rustls_tls() + .build() + .expect("failed to build chat http client") +}); + +// ============================================================================ +// API FUNCTIONS +// ============================================================================ + +#[allow(dead_code)] +pub async fn poll_chat_updates( + base_url: &str, + token: &str, + last_checked_at: Option, +) -> Result { + let url = format!("{}/api/machines/chat/poll", base_url); + + let mut payload = serde_json::json!({ + "machineToken": token, + }); + + if let Some(ts) = last_checked_at { + payload["lastCheckedAt"] = serde_json::json!(ts); + } + + let response = CHAT_CLIENT + .post(&url) + .json(&payload) + .send() + .await + .map_err(|e| format!("Falha na requisicao de poll: {e}"))?; + + if !response.status().is_success() { + let status = response.status(); + let body = response.text().await.unwrap_or_default(); + return Err(format!("Poll falhou: status={}, body={}", status, body)); + } + + response + .json() + .await + .map_err(|e| format!("Falha ao parsear resposta de poll: {e}")) +} + +pub async fn fetch_sessions(base_url: &str, token: &str) -> Result, String> { + let url = format!("{}/api/machines/chat/sessions", base_url); + + let payload = serde_json::json!({ + "machineToken": token, + }); + + let response = CHAT_CLIENT + .post(&url) + .json(&payload) + .send() + .await + .map_err(|e| format!("Falha na requisicao de sessions: {e}"))?; + + if !response.status().is_success() { + let status = response.status(); + let body = response.text().await.unwrap_or_default(); + return Err(format!("Sessions falhou: status={}, body={}", status, body)); + } + + #[derive(Deserialize)] + struct SessionsResponse { + sessions: Vec, + } + + let data: SessionsResponse = response + .json() + .await + .map_err(|e| format!("Falha ao parsear resposta de sessions: {e}"))?; + + Ok(data.sessions) +} + +pub async fn fetch_messages( + base_url: &str, + token: &str, + ticket_id: &str, + since: Option, +) -> Result { + let url = format!("{}/api/machines/chat/messages", base_url); + + let mut payload = serde_json::json!({ + "machineToken": token, + "ticketId": ticket_id, + "action": "list", + "limit": 200, + }); + + if let Some(ts) = since { + payload["since"] = serde_json::json!(ts); + } + + let response = CHAT_CLIENT + .post(&url) + .json(&payload) + .send() + .await + .map_err(|e| format!("Falha na requisicao de messages: {e}"))?; + + if !response.status().is_success() { + let status = response.status(); + let body = response.text().await.unwrap_or_default(); + return Err(format!("Messages falhou: status={}, body={}", status, body)); + } + + response + .json() + .await + .map_err(|e| format!("Falha ao parsear resposta de messages: {e}")) +} + +pub async fn send_message( + base_url: &str, + token: &str, + ticket_id: &str, + body: &str, + attachments: Option>, +) -> Result { + let url = format!("{}/api/machines/chat/messages", base_url); + + let mut payload = serde_json::json!({ + "machineToken": token, + "ticketId": ticket_id, + "action": "send", + "body": body, + }); + + if let Some(atts) = attachments { + payload["attachments"] = serde_json::to_value(atts).unwrap_or_default(); + } + + let response = CHAT_CLIENT + .post(&url) + .json(&payload) + .send() + .await + .map_err(|e| format!("Falha na requisicao de send: {e}"))?; + + if !response.status().is_success() { + let status = response.status(); + let body = response.text().await.unwrap_or_default(); + return Err(format!("Send falhou: status={}, body={}", status, body)); + } + + response + .json() + .await + .map_err(|e| format!("Falha ao parsear resposta de send: {e}")) +} + +pub async fn mark_messages_read( + base_url: &str, + token: &str, + ticket_id: &str, + message_ids: &[String], +) -> Result<(), String> { + let url = format!("{}/api/machines/chat/read", base_url); + + let payload = serde_json::json!({ + "machineToken": token, + "ticketId": ticket_id, + "messageIds": message_ids, + }); + + let response = CHAT_CLIENT + .post(&url) + .json(&payload) + .send() + .await + .map_err(|e| format!("Falha na requisicao de mark read: {e}"))?; + + if !response.status().is_success() { + let status = response.status(); + let body = response.text().await.unwrap_or_default(); + return Err(format!("Mark read falhou: status={}, body={}", status, body)); + } + + Ok(()) +} + +// ============================================================================ +// UPLOAD DE ARQUIVOS +// ============================================================================ + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AttachmentPayload { + pub storage_id: String, + pub name: String, + pub size: Option, + #[serde(rename = "type")] + pub mime_type: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UploadUrlResponse { + pub upload_url: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UploadResult { + pub storage_id: String, +} + +// Extensoes permitidas +const ALLOWED_EXTENSIONS: &[&str] = &[ + ".jpg", ".jpeg", ".png", ".gif", ".webp", + ".pdf", ".txt", ".doc", ".docx", ".xls", ".xlsx", +]; + +// Tamanho maximo: 10MB +const MAX_FILE_SIZE: u64 = 10 * 1024 * 1024; + +pub fn is_allowed_file(file_name: &str, file_size: u64) -> Result<(), String> { + let ext = file_name + .to_lowercase() + .rsplit('.') + .next() + .map(|e| format!(".{}", e)) + .unwrap_or_default(); + + if !ALLOWED_EXTENSIONS.contains(&ext.as_str()) { + return Err(format!( + "Tipo de arquivo não permitido. Permitidos: {}", + ALLOWED_EXTENSIONS.join(", ") + )); + } + + if file_size > MAX_FILE_SIZE { + return Err(format!( + "Arquivo muito grande. Máximo: {}MB", + MAX_FILE_SIZE / 1024 / 1024 + )); + } + + Ok(()) +} + +pub fn get_mime_type(file_name: &str) -> String { + let lower = file_name.to_lowercase(); + let ext = lower.rsplit('.').next().unwrap_or(""); + + match ext { + "jpg" | "jpeg" => "image/jpeg", + "png" => "image/png", + "gif" => "image/gif", + "webp" => "image/webp", + "pdf" => "application/pdf", + "txt" => "text/plain", + "doc" => "application/msword", + "docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "xls" => "application/vnd.ms-excel", + "xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + _ => "application/octet-stream", + } + .to_string() +} + +pub async fn generate_upload_url( + base_url: &str, + token: &str, + file_name: &str, + file_type: &str, + file_size: u64, +) -> Result { + let url = format!("{}/api/machines/chat/upload", base_url); + + let payload = serde_json::json!({ + "machineToken": token, + "fileName": file_name, + "fileType": file_type, + "fileSize": file_size, + }); + + let response = CHAT_CLIENT + .post(&url) + .json(&payload) + .send() + .await + .map_err(|e| format!("Falha na requisicao de upload URL: {e}"))?; + + if !response.status().is_success() { + let status = response.status(); + let body = response.text().await.unwrap_or_default(); + return Err(format!("Upload URL falhou: status={}, body={}", status, body)); + } + + let data: UploadUrlResponse = response + .json() + .await + .map_err(|e| format!("Falha ao parsear resposta de upload URL: {e}"))?; + + Ok(data.upload_url) +} + +pub async fn upload_file( + upload_url: &str, + file_data: Vec, + content_type: &str, +) -> Result { + let response = CHAT_CLIENT + .post(upload_url) + .header("Content-Type", content_type) + .body(file_data) + .send() + .await + .map_err(|e| format!("Falha no upload: {e}"))?; + + if !response.status().is_success() { + let status = response.status(); + let body = response.text().await.unwrap_or_default(); + return Err(format!("Upload falhou: status={}, body={}", status, body)); + } + + let data: UploadResult = response + .json() + .await + .map_err(|e| format!("Falha ao parsear resposta de upload: {e}"))?; + + Ok(data.storage_id) +} + +// ============================================================================ +// CHAT RUNTIME +// ============================================================================ + +struct ChatRealtimeHandle { + stop_flag: Arc, + join_handle: JoinHandle<()>, +} + +impl ChatRealtimeHandle { + fn stop(self) { + self.stop_flag.store(true, Ordering::Relaxed); + self.join_handle.abort(); + } +} + +#[derive(Default, Clone)] +pub struct ChatRuntime { + inner: Arc>>, + last_sessions: Arc>>, + last_unread_count: Arc>, + is_connected: Arc, +} + +impl ChatRuntime { + pub fn new() -> Self { + // Tentar restaurar estado persistido + let (sessions, unread) = match load_chat_state() { + Some(state) => (state.sessions, state.last_unread_count), + None => (Vec::new(), 0), + }; + + Self { + inner: Arc::new(Mutex::new(None)), + last_sessions: Arc::new(Mutex::new(sessions)), + last_unread_count: Arc::new(Mutex::new(unread)), + is_connected: Arc::new(AtomicBool::new(false)), + } + } + + /// Retorna true se conexao WS Convex esta ativa + pub fn is_using_sse(&self) -> bool { + self.is_connected.load(Ordering::Relaxed) + } + + /// Inicia o sistema de atualizacoes de chat via WebSocket do Convex + pub fn start_polling( + &self, + base_url: String, + convex_url: String, + token: String, + app: tauri::AppHandle, + ) -> Result<(), String> { + let sanitized_base = base_url.trim().trim_end_matches('/').to_string(); + if sanitized_base.is_empty() { + return Err("URL base invalida".to_string()); + } + let sanitized_convex = convex_url.trim().trim_end_matches('/').to_string(); + if sanitized_convex.is_empty() { + return Err("URL do Convex inválida".to_string()); + } + + // Para polling/SSE existente + { + let mut guard = self.inner.lock(); + if let Some(handle) = guard.take() { + handle.stop(); + } + } + + let stop_flag = Arc::new(AtomicBool::new(false)); + let stop_clone = stop_flag.clone(); + let base_clone = sanitized_base.clone(); + let convex_clone = sanitized_convex.clone(); + let token_clone = token.clone(); + let last_sessions = self.last_sessions.clone(); + let last_unread_count = self.last_unread_count.clone(); + let is_connected = self.is_connected.clone(); + + let join_handle = tauri::async_runtime::spawn(async move { + crate::log_info!("[CHAT DEBUG] Iniciando sistema de chat"); + crate::log_info!("[CHAT DEBUG] Convex URL: {}", convex_clone); + crate::log_info!("[CHAT DEBUG] API Base URL: {}", base_clone); + + let mut backoff_ms: u64 = 1_000; + let max_backoff_ms: u64 = 30_000; + let poll_interval = Duration::from_secs(5); + let mut last_poll = Instant::now() - poll_interval; + + loop { + if stop_clone.load(Ordering::Relaxed) { + break; + } + + crate::log_info!("[CHAT DEBUG] Tentando conectar ao Convex..."); + let client_result = ConvexClient::new(&convex_clone).await; + let mut client = match client_result { + Ok(c) => { + crate::log_info!("[CHAT DEBUG] Cliente Convex criado com sucesso"); + c + } + Err(err) => { + is_connected.store(false, Ordering::Relaxed); + crate::log_warn!("[CHAT DEBUG] FALHA ao criar cliente Convex: {err:?}"); + + if last_poll.elapsed() >= poll_interval { + poll_and_process_chat_update( + &base_clone, + &token_clone, + &app, + &last_sessions, + &last_unread_count, + ) + .await; + last_poll = Instant::now(); + } + + tokio::time::sleep(Duration::from_millis(backoff_ms)).await; + backoff_ms = backoff_ms.saturating_mul(2).min(max_backoff_ms); + continue; + } + }; + + let mut args = BTreeMap::new(); + args.insert("machineToken".to_string(), token_clone.clone().into()); + + crate::log_info!("[CHAT DEBUG] Assinando liveChat:checkMachineUpdates..."); + let subscribe_result = client.subscribe("liveChat:checkMachineUpdates", args).await; + let mut subscription = match subscribe_result { + Ok(sub) => { + is_connected.store(true, Ordering::Relaxed); + backoff_ms = 1_000; + crate::log_info!("[CHAT DEBUG] CONECTADO ao Convex WebSocket com sucesso!"); + sub + } + Err(err) => { + is_connected.store(false, Ordering::Relaxed); + crate::log_warn!("[CHAT DEBUG] FALHA ao assinar checkMachineUpdates: {err:?}"); + + if last_poll.elapsed() >= poll_interval { + poll_and_process_chat_update( + &base_clone, + &token_clone, + &app, + &last_sessions, + &last_unread_count, + ) + .await; + last_poll = Instant::now(); + } + + tokio::time::sleep(Duration::from_millis(backoff_ms)).await; + backoff_ms = backoff_ms.saturating_mul(2).min(max_backoff_ms); + continue; + } + }; + + crate::log_info!("[CHAT DEBUG] Entrando no loop de escuta WebSocket..."); + let mut update_count: u64 = 0; + while let Some(next) = subscription.next().await { + update_count += 1; + if stop_clone.load(Ordering::Relaxed) { + crate::log_info!("[CHAT DEBUG] Stop flag detectado, saindo do loop"); + break; + } + match next { + FunctionResult::Value(Value::Object(obj)) => { + let has_active = obj + .get("hasActiveSessions") + .and_then(|v| match v { + Value::Boolean(b) => Some(*b), + _ => None, + }) + .unwrap_or(false); + let total_unread = obj + .get("totalUnread") + .and_then(|v| match v { + Value::Int64(i) => Some(*i as u32), + Value::Float64(f) => Some(*f as u32), + _ => None, + }) + .unwrap_or(0); + + crate::log_info!( + "[CHAT DEBUG] UPDATE #{} recebido via WebSocket: hasActive={}, totalUnread={}", + update_count, has_active, total_unread + ); + + process_chat_update( + &base_clone, + &token_clone, + &app, + &last_sessions, + &last_unread_count, + has_active, + total_unread, + ) + .await; + } + FunctionResult::ConvexError(err) => { + crate::log_warn!("[CHAT DEBUG] Convex error em checkMachineUpdates: {err:?}"); + } + FunctionResult::ErrorMessage(msg) => { + crate::log_warn!("[CHAT DEBUG] Erro em checkMachineUpdates: {msg}"); + } + FunctionResult::Value(other) => { + crate::log_warn!("[CHAT DEBUG] Payload inesperado em checkMachineUpdates: {other:?}"); + } + } + } + + is_connected.store(false, Ordering::Relaxed); + + if stop_clone.load(Ordering::Relaxed) { + crate::log_info!("[CHAT DEBUG] Stop flag detectado apos loop"); + break; + } + + crate::log_warn!("[CHAT DEBUG] WebSocket DESCONECTADO! Aplicando fallback e tentando reconectar..."); + if last_poll.elapsed() >= poll_interval { + poll_and_process_chat_update( + &base_clone, + &token_clone, + &app, + &last_sessions, + &last_unread_count, + ) + .await; + last_poll = Instant::now(); + } + + tokio::time::sleep(Duration::from_millis(backoff_ms)).await; + backoff_ms = backoff_ms.saturating_mul(2).min(max_backoff_ms); + } + + is_connected.store(false, Ordering::Relaxed); + crate::log_info!("Chat encerrado (realtime finalizado)"); + }); + + let mut guard = self.inner.lock(); + *guard = Some(ChatRealtimeHandle { + stop_flag, + join_handle, + }); + + Ok(()) + } + + pub fn stop(&self) { + let mut guard = self.inner.lock(); + if let Some(handle) = guard.take() { + handle.stop(); + } + self.is_connected.store(false, Ordering::Relaxed); + } + + pub fn get_sessions(&self) -> Vec { + self.last_sessions.lock().clone() + } +} + +// ============================================================================ +// SHARED UPDATE PROCESSING +// ============================================================================ + +async fn poll_and_process_chat_update( + base_url: &str, + token: &str, + app: &tauri::AppHandle, + last_sessions: &Arc>>, + last_unread_count: &Arc>, +) { + crate::log_info!("[CHAT DEBUG] Executando fallback HTTP polling..."); + match poll_chat_updates(base_url, token, None).await { + Ok(result) => { + crate::log_info!( + "[CHAT DEBUG] Polling OK: hasActive={}, totalUnread={}", + result.has_active_sessions, result.total_unread + ); + process_chat_update( + base_url, + token, + app, + last_sessions, + last_unread_count, + result.has_active_sessions, + result.total_unread, + ) + .await; + } + Err(err) => { + crate::log_warn!("[CHAT DEBUG] Fallback poll FALHOU: {err}"); + } + } +} + +async fn process_chat_update( + base_url: &str, + token: &str, + app: &tauri::AppHandle, + last_sessions: &Arc>>, + last_unread_count: &Arc>, + has_active_sessions: bool, + total_unread: u32, +) { + crate::log_info!( + "[CHAT DEBUG] process_chat_update: hasActive={}, totalUnread={}", + has_active_sessions, total_unread + ); + + // Buscar sessoes completas para ter dados corretos + let mut current_sessions = if has_active_sessions { + let sessions = fetch_sessions(base_url, token).await.unwrap_or_default(); + crate::log_info!("[CHAT DEBUG] Buscou {} sessoes ativas", sessions.len()); + sessions + } else { + crate::log_info!("[CHAT DEBUG] Sem sessoes ativas"); + Vec::new() + }; + + // Ordenar por ultima atividade (mais recente primeiro) para consistencia em UI/tray. + if current_sessions.len() > 1 { + current_sessions.sort_by(|a, b| { + b.last_activity_at + .cmp(&a.last_activity_at) + .then_with(|| b.started_at.cmp(&a.started_at)) + }); + } + + // Verificar sessoes anteriores + let prev_sessions: Vec = last_sessions.lock().clone(); + let prev_session_ids: Vec = prev_sessions.iter().map(|s| s.session_id.clone()).collect(); + let current_session_ids: Vec = current_sessions.iter().map(|s| s.session_id.clone()).collect(); + + // Detectar novas sessoes + for session in ¤t_sessions { + if !prev_session_ids.contains(&session.session_id) { + crate::log_info!( + "Nova sessao de chat: ticket={}, session={}", + session.ticket_id, + session.session_id + ); + let _ = app.emit( + "raven://chat/session-started", + SessionStartedEvent { + session: session.clone(), + }, + ); + + // NÃO abre janela aqui - só quando o agente enviar a primeira mensagem + // O chat aparecerá minimizado com badge quando houver novas mensagens + + crate::log_info!( + "Sessão de chat iniciada pelo agente {}. Aguardando primeira mensagem.", + session.agent_name + ); + } + } + + // Detectar sessoes encerradas + for prev_session in &prev_sessions { + if !current_session_ids.contains(&prev_session.session_id) { + crate::log_info!( + "Sessao de chat encerrada: ticket={}, session={}", + prev_session.ticket_id, + prev_session.session_id + ); + let _ = app.emit( + "raven://chat/session-ended", + serde_json::json!({ + "sessionId": prev_session.session_id, + "ticketId": prev_session.ticket_id + }), + ); + } + } + + // ========================================================================= + // DETECCAO ROBUSTA DE NOVAS MENSAGENS + // Usa DUAS estrategias: timestamp E contador (belt and suspenders) + // ========================================================================= + + let prev_unread = *last_unread_count.lock(); + + // Estrategia 1: Detectar por lastActivityAt de cada sessao + // Se alguma sessao teve atividade mais recente E tem mensagens nao lidas -> nova mensagem + let mut detected_by_activity = false; + let mut activity_details = String::new(); + + for session in ¤t_sessions { + let prev_activity = prev_sessions + .iter() + .find(|s| s.session_id == session.session_id) + .map(|s| s.last_activity_at) + .unwrap_or(0); + + // Se lastActivityAt aumentou E ha mensagens nao lidas -> nova mensagem do agente + if session.last_activity_at > prev_activity && session.unread_count > 0 { + detected_by_activity = true; + activity_details = format!( + "sessao={} activity: {} -> {} unread={}", + session.ticket_id, prev_activity, session.last_activity_at, session.unread_count + ); + break; + } + } + + // Estrategia 2: Fallback por contador total (metodo original) + let detected_by_count = total_unread > prev_unread; + + // Nova mensagem se QUALQUER estrategia detectar + let new_messages = detected_by_activity || detected_by_count; + + // Log detalhado para diagnostico + crate::log_info!( + "[CHAT] Deteccao: by_activity={} by_count={} (prev={} curr={}) resultado={}", + detected_by_activity, detected_by_count, prev_unread, total_unread, new_messages + ); + if detected_by_activity { + crate::log_info!("[CHAT] Detectado por atividade: {}", activity_details); + } + + // Atualizar caches APOS deteccao (importante: manter ordem) + *last_sessions.lock() = current_sessions.clone(); + *last_unread_count.lock() = total_unread; + + // Persistir estado para sobreviver a restarts + save_chat_state(total_unread, ¤t_sessions); + + // Sempre emitir unread-update + let _ = app.emit( + "raven://chat/unread-update", + serde_json::json!({ + "totalUnread": total_unread, + "sessions": current_sessions + }), + ); + + if current_sessions.is_empty() { + close_all_chat_windows(app); + let _ = close_hub_window(app); + return; + } + + // Notificar novas mensagens - mostrar chat minimizado com badge + if new_messages && total_unread > 0 { + let new_count = if total_unread > prev_unread { + total_unread - prev_unread + } else { + 1 // Se detectou por activity mas contador nao mudou, assumir 1 nova + }; + + crate::log_info!( + "[CHAT] NOVAS MENSAGENS! count={}, total={}, metodo={}", + new_count, total_unread, + if detected_by_activity { "activity" } else { "count" } + ); + + let _ = app.emit( + "raven://chat/new-message", + serde_json::json!({ + "totalUnread": total_unread, + "newCount": new_count, + "sessions": current_sessions + }), + ); + + // Escolher qual sessao/ticket deve ser mostrado quando ha multiplas sessoes. + // Preferencia: maior incremento de unread (delta) e, em empate, ultima atividade mais recente. + let mut best_session: Option<&ChatSession> = None; + let mut best_delta: u32 = 0; + + for session in ¤t_sessions { + let prev_unread_for_ticket = prev_sessions + .iter() + .find(|s| s.ticket_id == session.ticket_id) + .map(|s| s.unread_count) + .unwrap_or(0); + let delta = session.unread_count.saturating_sub(prev_unread_for_ticket); + + let is_better = if delta > best_delta { + true + } else if delta == best_delta { + match best_session { + Some(best) => session.last_activity_at > best.last_activity_at, + None => true, + } + } else { + false + }; + + if is_better { + best_delta = delta; + best_session = Some(session); + } + } + + // Se ha multiplas sessoes ativas, usar o hub quando nao houver chat expandido. + // + // Importante (UX): nao mostrar hub e chat ao mesmo tempo. + if current_sessions.len() > 1 { + if has_expanded_chat_window() { + let _ = close_hub_window(app); + } else { + close_all_chat_windows(app); + let _ = open_hub_window(app); + } + } else { + // Uma sessao - nao precisa de hub + let _ = close_hub_window(app); + + // Fallback: se nao conseguimos detectar delta, pega a sessao com mais unread e mais recente. + let session_to_show = if best_delta > 0 { + best_session + } else { + current_sessions.iter().max_by(|a, b| { + a.unread_count + .cmp(&b.unread_count) + .then_with(|| a.last_activity_at.cmp(&b.last_activity_at)) + }) + }; + + // Mostrar janela de chat (sempre minimizada/nao intrusiva) + if let Some(session) = session_to_show { + let _ = open_chat_window_internal(app, &session.ticket_id, session.ticket_ref, true); + } + } + + // Notificacao nativa + let notification_title = "Nova mensagem de suporte"; + let notification_body = if new_count == 1 { + "Você recebeu 1 nova mensagem no chat".to_string() + } else { + format!("Você recebeu {} novas mensagens no chat", new_count) + }; + let _ = app + .notification() + .builder() + .title(notification_title) + .body(¬ification_body) + .show(); + } else { + // Log para debug quando NAO ha novas mensagens + if total_unread == 0 { + crate::log_info!("[CHAT DEBUG] Sem mensagens nao lidas (total=0)"); + } else if !new_messages { + crate::log_info!( + "[CHAT DEBUG] Sem novas mensagens (prev={} >= total={})", + prev_unread, total_unread + ); + } + } +} + +// ============================================================================ +// WINDOW MANAGEMENT +// ============================================================================ + +// Serializa operacoes de janela para evitar race/deadlock no Windows (winit/WebView2). +static WINDOW_OP_LOCK: Lazy> = Lazy::new(|| Mutex::new(())); +static CHAT_WINDOW_STATE: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); + +fn set_chat_window_state(label: &str, minimized: bool) { + CHAT_WINDOW_STATE.lock().insert(label.to_string(), minimized); +} + +fn clear_chat_window_state(label: &str) { + CHAT_WINDOW_STATE.lock().remove(label); +} + +fn has_expanded_chat_window() -> bool { + CHAT_WINDOW_STATE.lock().values().any(|minimized| !*minimized) +} + +fn close_all_chat_windows(app: &tauri::AppHandle) { + let labels: Vec = app + .webview_windows() + .keys() + .filter(|label| label.starts_with("chat-") && *label != HUB_WINDOW_LABEL) + .cloned() + .collect(); + for label in labels { + if let Some(window) = app.get_webview_window(&label) { + let _ = window.close(); + } + clear_chat_window_state(&label); + } +} + +fn hide_other_chat_windows(app: &tauri::AppHandle, active_label: &str) { + for (label, window) in app.webview_windows() { + if !label.starts_with("chat-") { + continue; + } + if label == active_label { + continue; + } + let _ = window.hide(); + set_chat_window_state(&label, true); + } + if let Some(hub) = app.get_webview_window(HUB_WINDOW_LABEL) { + let _ = hub.hide(); + } +} + +fn resolve_chat_window_position( + app: &tauri::AppHandle, + window: Option<&tauri::WebviewWindow>, + width: f64, + height: f64, +) -> (f64, f64) { + let margin = 20.0; + let taskbar_height = 50.0; + + let monitor = window + .and_then(|w| w.current_monitor().ok().flatten()) + .or_else(|| { + app.get_webview_window("main") + .and_then(|w| w.current_monitor().ok().flatten()) + }) + .or_else(|| app.available_monitors().ok().and_then(|monitors| monitors.into_iter().next())); + + let Some(monitor) = monitor else { + return (100.0, 100.0); + }; + + let size = monitor.size(); + let pos = monitor.position(); + let scale = monitor.scale_factor(); + + // Converter coordenadas do monitor para coordenadas logicas (multi-monitor pode ter origem negativa). + let monitor_x = pos.x as f64 / scale; + let monitor_y = pos.y as f64 / scale; + let monitor_width = size.width as f64 / scale; + let monitor_height = size.height as f64 / scale; + + let max_x = monitor_x + monitor_width - width - margin; + let max_y = monitor_y + monitor_height - height - margin - taskbar_height; + + let x = if max_x.is_finite() { max_x.max(monitor_x) } else { 100.0 }; + let y = if max_y.is_finite() { max_y.max(monitor_y) } else { 100.0 }; + + (x, y) +} + +fn open_chat_window_internal(app: &tauri::AppHandle, ticket_id: &str, ticket_ref: u64, start_minimized: bool) -> Result<(), String> { + let _guard = WINDOW_OP_LOCK.lock(); + open_chat_window_with_state(app, ticket_id, ticket_ref, start_minimized) +} + +/// Abre janela de chat com estado inicial de minimizacao configuravel +fn open_chat_window_with_state(app: &tauri::AppHandle, ticket_id: &str, ticket_ref: u64, start_minimized: bool) -> Result<(), String> { + let label = format!("chat-{}", ticket_id); + crate::log_info!( + "[WINDOW] open_chat_window: label={} ticket_ref={} start_minimized={}", + label, + ticket_ref, + start_minimized + ); + + if !start_minimized { + hide_other_chat_windows(app, &label); + } + + // Verificar se ja existe + if let Some(window) = app.get_webview_window(&label) { + let _ = window.set_ignore_cursor_events(false); + crate::log_info!("[WINDOW] {}: window existe -> show()", label); + window.show().map_err(|e| e.to_string())?; + let _ = window.unminimize(); + if !start_minimized { + crate::log_info!("[WINDOW] {}: window existe -> set_focus()", label); + window.set_focus().map_err(|e| e.to_string())?; + } + // Expandir a janela se estiver minimizada (quando clicado na lista) + if !start_minimized { + crate::log_info!("[WINDOW] {}: window existe -> set_chat_minimized(false)", label); + let _ = set_chat_minimized_unlocked(app, ticket_id, false); + } + crate::log_info!("[WINDOW] {}: open_chat_window OK (reuso)", label); + if !start_minimized { + set_chat_window_state(&label, false); + } + return Ok(()); + } + + // Dimensoes baseadas no estado inicial + let (width, height) = if start_minimized { + (240.0, 52.0) // Tamanho minimizado (chip com badge) + } else { + (380.0, 520.0) // Tamanho expandido + }; + + // Posicionar no canto inferior direito (acima da barra de tarefas). + let (x, y) = resolve_chat_window_position(app, None, width, height); + + // Usar query param ao inves de path para compatibilidade com SPA + let url_path = format!("index.html?view=chat&ticketId={}&ticketRef={}", ticket_id, ticket_ref); + + crate::log_info!( + "[WINDOW] {}: build() inicio size={}x{} pos=({},{}) url={}", + label, + width, + height, + x, + y, + url_path + ); + + let window = WebviewWindowBuilder::new( + app, + &label, + WebviewUrl::App(url_path.into()), + ) + .title("Chat de Suporte") + .inner_size(width, height) // Abre ja no tamanho correto + .min_inner_size(240.0, 52.0) // Tamanho minimo para modo minimizado com badge + .position(x, y) + .decorations(false) // Sem decoracoes nativas - usa header customizado + .transparent(true) // Permite fundo transparente + .shadow(false) // Desabilitar sombra para transparencia funcionar corretamente + .resizable(false) // Desabilitar redimensionamento manual + // Mantem o chat acessivel mesmo ao trocar de janela/app (skip_taskbar=true). + .always_on_top(true) + .skip_taskbar(true) + .focused(!start_minimized) + .visible(true) + .build() + .map_err(|e| e.to_string())?; + + crate::log_info!("[WINDOW] {}: build() OK", label); + + // IMPORTANTE: Garantir que a janela receba eventos de cursor (evita click-through) + let _ = window.set_ignore_cursor_events(false); + + crate::log_info!("[WINDOW] {}: pos-build set_chat_minimized({}) inicio", label, start_minimized); + // Reaplica layout/posicao logo apos criar a janela. + // Isso evita que a primeira abertura apareca no canto superior esquerdo em alguns ambientes. + let _ = set_chat_minimized_unlocked(app, ticket_id, start_minimized); + crate::log_info!("[WINDOW] {}: pos-build set_chat_minimized({}) fim", label, start_minimized); + + crate::log_info!("Janela de chat aberta (minimizada={}): {}", start_minimized, label); + Ok(()) +} + +pub fn open_chat_window(app: &tauri::AppHandle, ticket_id: &str, ticket_ref: u64) -> Result<(), String> { + // Quando chamado explicitamente (ex: clique no hub), abre expandida + open_chat_window_internal(app, ticket_id, ticket_ref, false) +} + +pub fn close_chat_window(app: &tauri::AppHandle, ticket_id: &str) -> Result<(), String> { + let _guard = WINDOW_OP_LOCK.lock(); + let label = format!("chat-{}", ticket_id); + if let Some(window) = app.get_webview_window(&label) { + window.close().map_err(|e| e.to_string())?; + } + clear_chat_window_state(&label); + Ok(()) +} + +pub fn minimize_chat_window(app: &tauri::AppHandle, ticket_id: &str) -> Result<(), String> { + let _guard = WINDOW_OP_LOCK.lock(); + let label = format!("chat-{}", ticket_id); + if let Some(window) = app.get_webview_window(&label) { + window.hide().map_err(|e| e.to_string())?; + } + set_chat_window_state(&label, true); + Ok(()) +} + +/// Redimensiona a janela de chat para modo minimizado (chip) ou expandido +fn set_chat_minimized_unlocked(app: &tauri::AppHandle, ticket_id: &str, minimized: bool) -> Result<(), String> { + let label = format!("chat-{}", ticket_id); + let window = app.get_webview_window(&label).ok_or("Janela não encontrada")?; + + if minimized { + hide_other_chat_windows(app, &label); + } + + // Tamanhos - chip minimizado com margem extra para badge (absolute -top-1 -right-1) + let (width, height) = if minimized { + (240.0, 52.0) // Tamanho com folga para "Ticket #XXX" e badge + } else { + (380.0, 520.0) // Tamanho expandido + }; + + // Calcular posicao no canto inferior direito do monitor atual (com fallback seguro). + let (x, y) = resolve_chat_window_position(app, Some(&window), width, height); + + // Aplicar novo tamanho e posicao + crate::log_info!("[WINDOW] {}: set_chat_minimized({}) set_size inicio", label, minimized); + window.set_size(tauri::LogicalSize::new(width, height)).map_err(|e| e.to_string())?; + crate::log_info!("[WINDOW] {}: set_chat_minimized({}) set_size OK", label, minimized); + crate::log_info!("[WINDOW] {}: set_chat_minimized({}) set_position inicio", label, minimized); + window.set_position(tauri::LogicalPosition::new(x, y)).map_err(|e| e.to_string())?; + crate::log_info!("[WINDOW] {}: set_chat_minimized({}) set_position OK", label, minimized); + + set_chat_window_state(&label, minimized); + crate::log_info!("Chat {} -> minimized={}", ticket_id, minimized); + Ok(()) +} + +pub fn set_chat_minimized(app: &tauri::AppHandle, ticket_id: &str, minimized: bool) -> Result<(), String> { + let _guard = WINDOW_OP_LOCK.lock(); + set_chat_minimized_unlocked(app, ticket_id, minimized) +} + +// ============================================================================ +// HUB WINDOW MANAGEMENT (Lista de todas as sessoes) +// ============================================================================ + +const HUB_WINDOW_LABEL: &str = "chat-hub"; + +pub fn open_hub_window(app: &tauri::AppHandle) -> Result<(), String> { + let _guard = WINDOW_OP_LOCK.lock(); + open_hub_window_with_state(app, true) // Por padrao abre minimizada +} + +fn open_hub_window_with_state(app: &tauri::AppHandle, start_minimized: bool) -> Result<(), String> { + // Verificar se ja existe + if let Some(window) = app.get_webview_window(HUB_WINDOW_LABEL) { + let _ = window.set_ignore_cursor_events(false); + window.show().map_err(|e| e.to_string())?; + let _ = window.unminimize(); + if !start_minimized { + window.set_focus().map_err(|e| e.to_string())?; + } + return Ok(()); + } + + // Dimensoes baseadas no estado inicial + let (width, height) = if start_minimized { + (200.0, 52.0) // Tamanho minimizado (chip) + } else { + (400.0, 520.0) // Tamanho expandido (igual ao web) + }; + + // Posicionar no canto inferior direito + let (x, y) = resolve_chat_window_position(app, None, width, height); + + // URL para modo hub + let url_path = "index.html?view=chat&hub=true"; + + WebviewWindowBuilder::new( + app, + HUB_WINDOW_LABEL, + WebviewUrl::App(url_path.into()), + ) + .title("Chats de Suporte") + .inner_size(width, height) + .min_inner_size(200.0, 52.0) + .position(x, y) + .decorations(false) + .transparent(true) + .shadow(false) + .resizable(false) // Desabilitar redimensionamento manual + // Mantem o hub acessivel mesmo ao trocar de janela/app (skip_taskbar=true). + .always_on_top(true) + .skip_taskbar(true) + .focused(!start_minimized) + .visible(true) + .build() + .map_err(|e| e.to_string())?; + + // IMPORTANTE: Garantir que a janela receba eventos de cursor (evita click-through) + if let Some(hub) = app.get_webview_window(HUB_WINDOW_LABEL) { + let _ = hub.set_ignore_cursor_events(false); + if !start_minimized { + let _ = hub.set_focus(); + } + } + + // REMOVIDO TEMPORARIAMENTE: set_hub_minimized logo apos build pode causar + // "resize em cima do resize" no timing errado do WebView2 + // let _ = set_hub_minimized(app, start_minimized); + + crate::log_info!("Hub window aberta (minimizada={})", start_minimized); + Ok(()) +} + +pub fn close_hub_window(app: &tauri::AppHandle) -> Result<(), String> { + let _guard = WINDOW_OP_LOCK.lock(); + if let Some(window) = app.get_webview_window(HUB_WINDOW_LABEL) { + window.close().map_err(|e| e.to_string())?; + } + Ok(()) +} + +pub fn set_hub_minimized(app: &tauri::AppHandle, minimized: bool) -> Result<(), String> { + let _guard = WINDOW_OP_LOCK.lock(); + let window = app.get_webview_window(HUB_WINDOW_LABEL).ok_or("Hub window não encontrada")?; + + let (width, height) = if minimized { + (200.0, 52.0) // Chip minimizado + } else { + (400.0, 520.0) // Lista expandida (igual ao web) + }; + + let (x, y) = resolve_chat_window_position(app, Some(&window), width, height); + + // IGUAL AO CHAT: primeiro size, depois position (ordem importa para hit-test no Windows) + window.set_size(tauri::LogicalSize::new(width, height)).map_err(|e| e.to_string())?; + window.set_position(tauri::LogicalPosition::new(x, y)).map_err(|e| e.to_string())?; + + // Foco apenas quando expandir (evita roubar foco ao minimizar apos abrir um chat). + if !minimized { + let _ = window.set_focus(); + } + + crate::log_info!("Hub -> minimized={}, size={}x{}, pos=({},{})", minimized, width, height, x, y); + Ok(()) +} diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs new file mode 100644 index 0000000..5e3939e --- /dev/null +++ b/apps/desktop/src-tauri/src/lib.rs @@ -0,0 +1,837 @@ +mod agent; +mod chat; +#[cfg(target_os = "windows")] +mod rustdesk; +#[cfg(target_os = "windows")] +mod service_client; +mod usb_control; + +use agent::{collect_inventory_plain, collect_profile, AgentRuntime, MachineProfile}; +use chat::{ChatRuntime, ChatSession, ChatMessagesResponse, SendMessageResponse}; +use chrono::Local; +use usb_control::{UsbPolicy, UsbPolicyResult}; +use tauri::{Emitter, Listener, Manager, WindowEvent}; +use tauri_plugin_store::Builder as StorePluginBuilder; +use std::fs::OpenOptions; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::sync::OnceLock; +#[cfg(target_os = "windows")] +use tauri::menu::{MenuBuilder, MenuItemBuilder}; +#[cfg(target_os = "windows")] +use tauri::tray::TrayIconBuilder; +#[cfg(target_os = "windows")] +use winreg::enums::*; +#[cfg(target_os = "windows")] +use winreg::RegKey; + +const DEFAULT_CONVEX_URL: &str = "https://convex.esdrasrenan.com.br"; + +// ============================================================================ +// Sistema de Logging para Agente +// ============================================================================ + +static AGENT_LOG_FILE: OnceLock> = OnceLock::new(); + +pub fn init_agent_logging() -> Result<(), String> { + let dir = logs_directory() + .ok_or("LOCALAPPDATA indisponivel para logging")?; + + std::fs::create_dir_all(&dir) + .map_err(|e| format!("Falha ao criar diretorio de logs: {e}"))?; + + let path = dir.join("raven-agent.log"); + let file = OpenOptions::new() + .create(true) + .append(true) + .open(&path) + .map_err(|e| format!("Falha ao abrir raven-agent.log: {e}"))?; + + let _ = AGENT_LOG_FILE.set(std::sync::Mutex::new(file)); + Ok(()) +} + +pub fn log_agent(level: &str, message: &str) { + let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); + let line = format!("[{timestamp}] [{level}] {message}\n"); + + // Escreve para stderr (util em dev/debug) + eprint!("{line}"); + + // Escreve para arquivo + if let Some(mutex) = AGENT_LOG_FILE.get() { + if let Ok(mut file) = mutex.lock() { + let _ = file.write_all(line.as_bytes()); + let _ = file.flush(); + } + } +} + +#[macro_export] +macro_rules! log_info { + ($($arg:tt)*) => { + $crate::log_agent("INFO", format!($($arg)*).as_str()) + }; +} + +#[macro_export] +macro_rules! log_error { + ($($arg:tt)*) => { + $crate::log_agent("ERROR", format!($($arg)*).as_str()) + }; +} + +#[macro_export] +macro_rules! log_warn { + ($($arg:tt)*) => { + $crate::log_agent("WARN", format!($($arg)*).as_str()) + }; +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RustdeskProvisioningResult { + pub id: String, + pub password: String, + pub installed_version: Option, + pub updated: bool, + pub last_provisioned_at: i64, +} + +#[tauri::command] +fn collect_machine_profile() -> Result { + collect_profile().map_err(|error| error.to_string()) +} + +#[tauri::command] +fn collect_machine_inventory() -> Result { + Ok(collect_inventory_plain()) +} + +#[tauri::command] +fn start_machine_agent( + state: tauri::State, + base_url: String, + token: String, + status: Option, + interval_seconds: Option, +) -> Result<(), String> { + state + .start_heartbeat(base_url, token, status, interval_seconds) + .map_err(|error| error.to_string()) +} + +#[tauri::command] +fn stop_machine_agent(state: tauri::State) -> Result<(), String> { + state.stop(); + Ok(()) +} + +#[tauri::command] +fn open_devtools(window: tauri::WebviewWindow) -> Result<(), String> { + window.open_devtools(); + Ok(()) +} + +#[tauri::command] +fn log_app_event(message: String) -> Result<(), String> { + append_app_log(&message) +} + +fn append_app_log(message: &str) -> Result<(), String> { + let Some(dir) = logs_directory() else { + return Err("LOCALAPPDATA indisponivel para gravar logs".to_string()); + }; + + std::fs::create_dir_all(&dir) + .map_err(|error| format!("Falha ao criar pasta de logs: {error}"))?; + + let path = dir.join("app.log"); + let mut file = OpenOptions::new() + .create(true) + .append(true) + .open(&path) + .map_err(|error| format!("Falha ao abrir app.log: {error}"))?; + + let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S"); + writeln!(file, "[{timestamp}] {message}") + .map_err(|error| format!("Falha ao escrever log: {error}"))?; + + Ok(()) +} + +fn logs_directory() -> Option { + let base = std::env::var("LOCALAPPDATA").ok()?; + Some(Path::new(&base).join("br.com.esdrasrenan.sistemadechamados").join("logs")) +} + +#[tauri::command] +async fn ensure_rustdesk_and_emit( + app: tauri::AppHandle, + config_string: Option, + password: Option, + machine_id: Option, +) -> Result { + let result = tauri::async_runtime::spawn_blocking(move || { + run_rustdesk_ensure(config_string, password, machine_id) + }) + .await + .map_err(|error| error.to_string())??; + + if let Err(error) = app.emit("raven://remote-access/provisioned", &result) { + eprintln!("[rustdesk] falha ao emitir evento raven://remote-access/provisioned: {error}"); + } + + Ok(result) +} + +#[cfg(target_os = "windows")] +fn run_rustdesk_ensure( + config_string: Option, + password: Option, + machine_id: Option, +) -> Result { + // Tenta usar o servico primeiro (sem UAC) + if service_client::is_service_available() { + log_info!("Usando Raven Service para provisionar RustDesk"); + match service_client::provision_rustdesk( + config_string.as_deref(), + password.as_deref(), + machine_id.as_deref(), + ) { + Ok(result) => { + return Ok(RustdeskProvisioningResult { + id: result.id, + password: result.password, + installed_version: result.installed_version, + updated: result.updated, + last_provisioned_at: result.last_provisioned_at, + }); + } + Err(e) => { + log_warn!("Falha ao usar servico para RustDesk: {e}"); + // Continua para fallback + } + } + } + + // Fallback: chamada direta (pode pedir UAC) + log_info!("Usando chamada direta para provisionar RustDesk (pode pedir UAC)"); + rustdesk::ensure_rustdesk( + config_string.as_deref(), + password.as_deref(), + machine_id.as_deref(), + ) + .map_err(|error| error.to_string()) +} + +#[cfg(not(target_os = "windows"))] +fn run_rustdesk_ensure( + _config_string: Option, + _password: Option, + _machine_id: Option, +) -> Result { + Err("Provisionamento automático do RustDesk está disponível apenas no Windows.".to_string()) +} + +#[tauri::command] +fn apply_usb_policy(policy: String) -> Result { + // Valida a politica primeiro + let _policy_enum = UsbPolicy::from_str(&policy) + .ok_or_else(|| format!("Politica USB invalida: {}. Use ALLOW, BLOCK_ALL ou READONLY.", policy))?; + + // Tenta usar o servico primeiro (sem UAC) + #[cfg(target_os = "windows")] + if service_client::is_service_available() { + log_info!("Usando Raven Service para aplicar politica USB: {}", policy); + match service_client::apply_usb_policy(&policy) { + Ok(result) => { + return Ok(UsbPolicyResult { + success: result.success, + policy: result.policy, + error: result.error, + applied_at: result.applied_at, + }); + } + Err(e) => { + log_warn!("Falha ao usar servico para USB policy: {e}"); + // Continua para fallback + } + } + } + + // Fallback: chamada direta (pode pedir UAC) + log_info!("Usando chamada direta para aplicar politica USB (pode pedir UAC)"); + usb_control::apply_usb_policy(_policy_enum).map_err(|e| e.to_string()) +} + +#[tauri::command] +fn get_usb_policy() -> Result { + // Tenta usar o servico primeiro + #[cfg(target_os = "windows")] + if service_client::is_service_available() { + match service_client::get_usb_policy() { + Ok(policy) => return Ok(policy), + Err(e) => { + log_warn!("Falha ao obter USB policy via servico: {e}"); + // Continua para fallback + } + } + } + + // Fallback: leitura direta (nao precisa elevacao para ler) + usb_control::get_current_policy() + .map(|p| p.as_str().to_string()) + .map_err(|e| e.to_string()) +} + +#[tauri::command] +fn refresh_usb_policy() -> Result<(), String> { + usb_control::refresh_group_policy().map_err(|e| e.to_string()) +} + +// ============================================================================ +// COMANDOS DE CHAT +// ============================================================================ + +#[tauri::command] +fn start_chat_polling( + state: tauri::State, + app: tauri::AppHandle, + base_url: String, + convex_url: Option, + token: String, +) -> Result<(), String> { + let url = convex_url.unwrap_or_else(|| DEFAULT_CONVEX_URL.to_string()); + state.start_polling(base_url, url, token, app) +} + +#[tauri::command] +fn stop_chat_polling(state: tauri::State) -> Result<(), String> { + state.stop(); + Ok(()) +} + +#[tauri::command] +fn is_chat_using_realtime(state: tauri::State) -> bool { + state.is_using_sse() +} + +#[tauri::command] +fn get_chat_sessions(state: tauri::State) -> Vec { + state.get_sessions() +} + +#[tauri::command] +async fn fetch_chat_sessions(base_url: String, token: String) -> Result, String> { + chat::fetch_sessions(&base_url, &token).await +} + +#[tauri::command] +async fn fetch_chat_messages( + base_url: String, + token: String, + ticket_id: String, + since: Option, +) -> Result { + chat::fetch_messages(&base_url, &token, &ticket_id, since).await +} + +#[tauri::command] +async fn send_chat_message( + base_url: String, + token: String, + ticket_id: String, + body: String, + attachments: Option>, +) -> Result { + chat::send_message(&base_url, &token, &ticket_id, &body, attachments).await +} + +#[tauri::command] +async fn mark_chat_messages_read( + base_url: String, + token: String, + ticket_id: String, + message_ids: Vec, +) -> Result<(), String> { + if message_ids.is_empty() { + return Ok(()); + } + chat::mark_messages_read(&base_url, &token, &ticket_id, &message_ids).await +} + +#[tauri::command] +async fn upload_chat_file( + base_url: String, + token: String, + file_path: String, +) -> Result { + use std::path::Path; + + // Ler o arquivo + let path = Path::new(&file_path); + let file_name = path + .file_name() + .and_then(|n| n.to_str()) + .ok_or("Nome de arquivo inválido")? + .to_string(); + + let file_data = std::fs::read(&file_path) + .map_err(|e| format!("Falha ao ler arquivo: {e}"))?; + + let file_size = file_data.len() as u64; + + // Validar arquivo + chat::is_allowed_file(&file_name, file_size)?; + + // Obter tipo MIME + let mime_type = chat::get_mime_type(&file_name); + + // Gerar URL de upload + let upload_url = chat::generate_upload_url( + &base_url, + &token, + &file_name, + &mime_type, + file_size, + ) + .await?; + + // Fazer upload + let storage_id = chat::upload_file(&upload_url, file_data, &mime_type).await?; + + Ok(chat::AttachmentPayload { + storage_id, + name: file_name, + size: Some(file_size), + mime_type: Some(mime_type), + }) +} + +#[tauri::command] +async fn open_chat_window(app: tauri::AppHandle, ticket_id: String, ticket_ref: u64) -> Result<(), String> { + log_info!("[CMD] open_chat_window called: ticket_id={}, ticket_ref={}", ticket_id, ticket_ref); + let app_handle = app.clone(); + let ticket_id_for_task = ticket_id.clone(); + let result = tauri::async_runtime::spawn_blocking(move || { + chat::open_chat_window(&app_handle, &ticket_id_for_task, ticket_ref) + }) + .await + .map_err(|err| format!("Falha ao abrir chat (join): {err}"))?; + log_info!("[CMD] open_chat_window result: {:?}", result); + result +} + +#[tauri::command] +fn close_chat_window(app: tauri::AppHandle, ticket_id: String) -> Result<(), String> { + chat::close_chat_window(&app, &ticket_id) +} + +#[tauri::command] +fn minimize_chat_window(app: tauri::AppHandle, ticket_id: String) -> Result<(), String> { + chat::minimize_chat_window(&app, &ticket_id) +} + +#[tauri::command] +fn set_chat_minimized(app: tauri::AppHandle, ticket_id: String, minimized: bool) -> Result<(), String> { + chat::set_chat_minimized(&app, &ticket_id, minimized) +} + +#[tauri::command] +async fn open_hub_window(app: tauri::AppHandle) -> Result<(), String> { + let app_handle = app.clone(); + tauri::async_runtime::spawn_blocking(move || { + chat::open_hub_window(&app_handle) + }) + .await + .map_err(|err| format!("Falha ao abrir hub (join): {err}"))? +} + +#[tauri::command] +fn close_hub_window(app: tauri::AppHandle) -> Result<(), String> { + chat::close_hub_window(&app) +} + +#[tauri::command] +fn set_hub_minimized(app: tauri::AppHandle, minimized: bool) -> Result<(), String> { + chat::set_hub_minimized(&app, minimized) +} + +// ============================================================================ +// Handler de Deep Link (raven://) +// ============================================================================ + +/// Processa URLs do protocolo raven:// +/// Formatos suportados: +/// - raven://ticket/{token} - Abre visualizacao do chamado +/// - raven://chat/{ticketId}?token={token} - Abre chat do chamado +/// - raven://rate/{token} - Abre avaliacao do chamado +fn handle_deep_link(app: &tauri::AppHandle, url: &str) { + log_info!("Processando deep link: {url}"); + + // Remove o prefixo raven:// + let path = url.trim_start_matches("raven://"); + + // Parse do path + let parts: Vec<&str> = path.split('/').collect(); + + if parts.is_empty() { + log_warn!("Deep link invalido: path vazio"); + return; + } + + match parts[0] { + "ticket" => { + if parts.len() > 1 { + let token = parts[1].split('?').next().unwrap_or(parts[1]); + log_info!("Abrindo ticket com token: {token}"); + + // Mostra a janela principal + if let Some(window) = app.get_webview_window("main") { + let _ = window.show(); + let _ = window.set_focus(); + + // Emite evento para o frontend navegar para o ticket + let _ = app.emit("raven://deep-link/ticket", serde_json::json!({ + "token": token + })); + } + } + } + "chat" => { + if parts.len() > 1 { + let ticket_id = parts[1].split('?').next().unwrap_or(parts[1]); + log_info!("Abrindo chat do ticket: {ticket_id}"); + + // Abre janela de chat (ticket_ref 0 quando vem de deeplink) + if let Err(e) = chat::open_chat_window(app, ticket_id, 0) { + log_error!("Falha ao abrir chat: {e}"); + } + } + } + "rate" => { + if parts.len() > 1 { + let token = parts[1].split('?').next().unwrap_or(parts[1]); + log_info!("Abrindo avaliacao com token: {token}"); + + // Mostra a janela principal + if let Some(window) = app.get_webview_window("main") { + let _ = window.show(); + let _ = window.set_focus(); + + // Emite evento para o frontend navegar para avaliacao + let _ = app.emit("raven://deep-link/rate", serde_json::json!({ + "token": token + })); + } + } + } + _ => { + log_warn!("Deep link desconhecido: {path}"); + } + } +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .manage(AgentRuntime::new()) + .manage(ChatRuntime::new()) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_opener::init()) + .plugin(StorePluginBuilder::default().build()) + .plugin(tauri_plugin_updater::Builder::new().build()) + .plugin(tauri_plugin_process::init()) + .plugin(tauri_plugin_notification::init()) + .plugin(tauri_plugin_deep_link::init()) + .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| { + // Quando uma segunda instância tenta iniciar, foca a janela existente + if let Some(window) = app.get_webview_window("main") { + let _ = window.show(); + let _ = window.unminimize(); + let _ = window.set_focus(); + } + })) + .on_window_event(|window, event| { + if let WindowEvent::CloseRequested { api, .. } = event { + api.prevent_close(); + let _ = window.hide(); + } + }) + .setup(|app| { + // Inicializa sistema de logging primeiro + if let Err(e) = init_agent_logging() { + eprintln!("[raven] Falha ao inicializar logging: {e}"); + } + + log_info!("Raven iniciando..."); + + // Configura handler de deep link (raven://) + #[cfg(desktop)] + { + let handle = app.handle().clone(); + app.listen("deep-link://new-url", move |event| { + let urls = event.payload(); + log_info!("Deep link recebido: {urls}"); + handle_deep_link(&handle, urls); + }); + } + + #[cfg(target_os = "windows")] + { + let start_in_background = std::env::args().any(|arg| arg == "--background"); + setup_raven_autostart(); + setup_tray(app.handle())?; + if start_in_background { + if let Some(win) = app.get_webview_window("main") { + let _ = win.hide(); + } + } + + // Tenta iniciar o agente e chat em background se houver credenciais salvas + let app_handle = app.handle().clone(); + let agent_runtime = app.state::().inner().clone(); + let chat_runtime = app.state::().inner().clone(); + tauri::async_runtime::spawn(async move { + // Aguarda um pouco para o app estabilizar + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + if let Err(e) = try_start_background_agent(&app_handle, agent_runtime, chat_runtime).await { + log_warn!("Agente nao iniciado em background: {e}"); + } + }); + } + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + collect_machine_profile, + collect_machine_inventory, + start_machine_agent, + stop_machine_agent, + open_devtools, + log_app_event, + ensure_rustdesk_and_emit, + apply_usb_policy, + get_usb_policy, + refresh_usb_policy, + // Chat commands + start_chat_polling, + stop_chat_polling, + is_chat_using_realtime, + get_chat_sessions, + fetch_chat_sessions, + fetch_chat_messages, + send_chat_message, + mark_chat_messages_read, + upload_chat_file, + open_chat_window, + close_chat_window, + minimize_chat_window, + set_chat_minimized, + // Hub commands + open_hub_window, + close_hub_window, + set_hub_minimized + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} + +#[cfg(target_os = "windows")] +fn setup_raven_autostart() { + let exe_path = match std::env::current_exe() { + Ok(p) => p, + Err(e) => { + log_error!("Falha ao obter caminho do executavel: {e}"); + return; + } + }; + + let path_str = exe_path.display().to_string(); + // Adiciona flag --background para indicar inicio automatico + let value = format!("\"{}\" --background", path_str); + + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + + let key = match hkcu.create_subkey(r"Software\Microsoft\Windows\CurrentVersion\Run") { + Ok((key, _)) => key, + Err(e) => { + log_error!("Falha ao criar/abrir chave de registro Run: {e}"); + return; + } + }; + + if let Err(e) = key.set_value("Raven", &value) { + log_error!("Falha ao definir valor de auto-start no registro: {e}"); + return; + } + + log_info!("Auto-start configurado: {value}"); + + // Valida que foi salvo corretamente + match key.get_value::("Raven") { + Ok(saved) => { + if saved == value { + log_info!("Auto-start validado: entrada existe no registro"); + } else { + log_warn!("Auto-start: valor difere. Esperado: {value}, Salvo: {saved}"); + } + } + Err(e) => { + log_warn!("Auto-start: nao foi possivel validar entrada: {e}"); + } + } +} + +#[cfg(target_os = "windows")] +fn setup_tray(app: &tauri::AppHandle) -> tauri::Result<()> { + let show_item = MenuItemBuilder::with_id("show", "Mostrar").build(app)?; + let chat_item = MenuItemBuilder::with_id("chat", "Abrir Chat").build(app)?; + let quit_item = MenuItemBuilder::with_id("quit", "Sair").build(app)?; + let menu = MenuBuilder::new(app) + .items(&[&show_item, &chat_item, &quit_item]) + .build()?; + + let mut builder = TrayIconBuilder::new() + .menu(&menu) + .on_menu_event(|tray, event| { + match event.id().as_ref() { + "show" => { + if let Some(win) = tray.app_handle().get_webview_window("main") { + let _ = win.show(); + let _ = win.set_focus(); + } + // Reabrir chat se houver sessao ativa + if let Some(chat_runtime) = tray.app_handle().try_state::() { + let sessions = chat_runtime.get_sessions(); + if let Some(session) = sessions.first() { + let _ = chat::open_chat_window(tray.app_handle(), &session.ticket_id, session.ticket_ref); + } + } + } + "chat" => { + // Abrir janela de chat se houver sessao ativa + if let Some(chat_runtime) = tray.app_handle().try_state::() { + let sessions = chat_runtime.get_sessions(); + if sessions.len() > 1 { + // Multiplas sessoes - abrir hub + if let Err(e) = chat::open_hub_window(tray.app_handle()) { + log_error!("Falha ao abrir hub de chat: {e}"); + } + } else if let Some(session) = sessions.first() { + // Uma sessao - abrir diretamente + if let Err(e) = chat::open_chat_window(tray.app_handle(), &session.ticket_id, session.ticket_ref) { + log_error!("Falha ao abrir janela de chat: {e}"); + } + } + } + } + "quit" => { + tray.app_handle().exit(0); + } + _ => {} + } + }) + .on_tray_icon_event(|tray, event| { + if let tauri::tray::TrayIconEvent::DoubleClick { .. } = event { + if let Some(win) = tray.app_handle().get_webview_window("main") { + let _ = win.show(); + let _ = win.set_focus(); + } + // Reabrir chat se houver sessao ativa + if let Some(chat_runtime) = tray.app_handle().try_state::() { + let sessions = chat_runtime.get_sessions(); + if let Some(session) = sessions.first() { + let _ = chat::open_chat_window(tray.app_handle(), &session.ticket_id, session.ticket_ref); + } + } + } + }); + + if let Some(icon) = app.default_window_icon() { + builder = builder.icon(icon.clone()); + } + + builder = builder.tooltip("Raven"); + + builder.build(app)?; + Ok(()) +} + +#[cfg(target_os = "windows")] +async fn try_start_background_agent( + app: &tauri::AppHandle, + agent_runtime: AgentRuntime, + chat_runtime: ChatRuntime, +) -> Result<(), String> { + log_info!("Verificando credenciais salvas para iniciar agente..."); + + let app_data = app + .path() + .app_local_data_dir() + .map_err(|e| format!("Falha ao obter diretorio de dados: {e}"))?; + + let store_path = app_data.join("machine-agent.json"); + + if !store_path.exists() { + return Err("Nenhuma configuracao encontrada".to_string()); + } + + // Ler arquivo JSON diretamente + let content = std::fs::read_to_string(&store_path) + .map_err(|e| format!("Falha ao ler machine-agent.json: {e}"))?; + + let data: serde_json::Value = serde_json::from_str(&content) + .map_err(|e| format!("Falha ao parsear machine-agent.json: {e}"))?; + + let token = data + .get("token") + .and_then(|v| v.as_str()) + .filter(|t| !t.is_empty()) + .ok_or("Token nao encontrado ou vazio")?; + + let config = data.get("config"); + + let api_base_url = config + .and_then(|c| c.get("apiBaseUrl")) + .and_then(|v| v.as_str()) + .unwrap_or("https://tickets.esdrasrenan.com.br"); + + let convex_url = config + .and_then(|c| c.get("convexUrl")) + .and_then(|v| v.as_str()) + .unwrap_or(DEFAULT_CONVEX_URL); + + let interval = config + .and_then(|c| c.get("heartbeatIntervalSec")) + .and_then(|v| v.as_u64()) + .unwrap_or(300); + + log_info!( + "Iniciando agente em background: url={}, interval={}s", + api_base_url, + interval + ); + + agent_runtime + .start_heartbeat( + api_base_url.to_string(), + token.to_string(), + Some("online".to_string()), + Some(interval), + ) + .map_err(|e| format!("Falha ao iniciar heartbeat: {e}"))?; + + // Iniciar sistema de chat (WebSocket + fallback HTTP polling) + if let Err(e) = + chat_runtime.start_polling(api_base_url.to_string(), convex_url.to_string(), token.to_string(), app.clone()) + { + log_warn!("Falha ao iniciar chat em background: {e}"); + } else { + log_info!("Chat iniciado com sucesso (Convex WebSocket)"); + } + + log_info!("Agente iniciado com sucesso em background"); + + Ok(()) +} diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs new file mode 100644 index 0000000..abafa26 --- /dev/null +++ b/apps/desktop/src-tauri/src/main.rs @@ -0,0 +1,6 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + appsdesktop_lib::run() +} diff --git a/apps/desktop/src-tauri/src/rustdesk.rs b/apps/desktop/src-tauri/src/rustdesk.rs new file mode 100644 index 0000000..8c6cee4 --- /dev/null +++ b/apps/desktop/src-tauri/src/rustdesk.rs @@ -0,0 +1,1588 @@ +use crate::RustdeskProvisioningResult; +use chrono::{Local, Utc}; +use once_cell::sync::Lazy; +use parking_lot::Mutex; +use reqwest::blocking::Client; +use serde::Deserialize; +use serde_json::{Map as JsonMap, Value as JsonValue}; +use sha2::{Digest, Sha256}; +use std::env; +use std::ffi::OsStr; +use std::fs::{self, File, OpenOptions}; +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::thread; +use std::time::Duration; +use thiserror::Error; +use std::os::windows::process::CommandExt; + +const RELEASES_API: &str = "https://api.github.com/repos/rustdesk/rustdesk/releases/latest"; +const USER_AGENT: &str = "RavenDesktop/1.0"; +const SERVER_HOST: &str = "rust.rever.com.br"; +const SERVER_KEY: &str = "0mxocQKmK6GvTZQYKgjrG9tlNkKOqf81gKgqwAmnZuI="; +const DEFAULT_PASSWORD: &str = "FMQ9MA>e73r.FI> = Lazy::new(|| Mutex::new(())); + +#[derive(Debug, Error)] +pub enum RustdeskError { + #[error("HTTP error: {0}")] + Http(#[from] reqwest::Error), + #[error("I/O error: {0}")] + Io(#[from] io::Error), + #[error("Release asset não encontrado para Windows x86_64")] + AssetMissing, + #[error("Falha ao executar comando {command}: status {status:?}")] + CommandFailed { command: String, status: Option }, + #[error("Falha ao detectar ID do RustDesk")] + MissingId, +} + +#[derive(Debug, Deserialize)] +struct ReleaseAsset { + name: String, + browser_download_url: String, +} + +#[derive(Debug, Deserialize)] +struct ReleaseResponse { + tag_name: String, + assets: Vec, +} + +/// Auxiliar para definir ID customizado baseado no machine_id +fn define_custom_id_from_machine(exe_path: &Path, machine_id: Option<&str>) -> Option { + if let Some(value) = machine_id.and_then(|raw| { + let trimmed = raw.trim(); + if trimmed.is_empty() { None } else { Some(trimmed) } + }) { + match set_custom_id(exe_path, value) { + Ok(custom) => { + log_event(format!("ID determinístico definido: {custom}")); + Some(custom) + } + Err(error) => { + log_event(format!("Falha ao definir ID determinístico: {error}")); + None + } + } + } else { + None + } +} + +pub fn ensure_rustdesk( + config_string: Option<&str>, + password_override: Option<&str>, + machine_id: Option<&str>, +) -> Result { + let _guard = PROVISION_MUTEX.lock(); + log_event("Iniciando preparo do RustDesk"); + + if let Err(error) = ensure_service_profiles_writable_preflight() { + log_event(format!( + "Aviso: não foi possível preparar ACL dos perfis do serviço ({error}). Continuando mesmo assim; o serviço pode não aplicar a senha." + )); + } + + // IMPORTANTE: Ler o ID existente ANTES de qualquer limpeza + // Isso preserva o ID quando o Raven é reinstalado mas o RustDesk permanece + let preserved_remote_id = read_remote_id_from_profiles(); + if let Some(ref id) = preserved_remote_id { + log_event(format!("ID existente preservado antes da limpeza: {}", id)); + } + + let exe_path = detect_executable_path(); + let (installed_version, freshly_installed) = ensure_installed(&exe_path)?; + log_event(if freshly_installed { + "RustDesk instalado a partir do instalador mais recente" + } else { + "RustDesk já instalado, usando binário existente" + }); + + match stop_rustdesk_processes() { + Ok(_) => log_event("Instâncias existentes do RustDesk encerradas"), + Err(error) => log_event(format!( + "Aviso: não foi possível parar completamente o RustDesk antes da reprovisionamento ({error})" + )), + } + + // So limpa perfis se for instalacao fresca (RustDesk nao existia) + // Se ja existia, preservamos o ID para manter consistencia + if freshly_installed { + match purge_existing_rustdesk_profiles() { + Ok(_) => log_event("Configurações antigas do RustDesk limpas (instalação fresca)"), + Err(error) => log_event(format!( + "Aviso: não foi possível limpar completamente os perfis existentes do RustDesk ({error})" + )), + } + } else { + log_event("Mantendo perfis existentes do RustDesk (preservando ID)"); + } + + if let Some(value) = config_string.and_then(|raw| { + let trimmed = raw.trim(); + if trimmed.is_empty() { None } else { Some(trimmed) } + }) { + if let Err(error) = run_with_args(&exe_path, &["--config", value]) { + log_event(format!("Falha ao aplicar configuração inline: {error}")); + } else { + log_event("Configuração aplicada via --config"); + } + } else { + let config_path = write_config_files()?; + log_event(format!( + "Arquivo de configuração atualizado em {}", + config_path.display() + )); + + if let Err(error) = apply_config(&exe_path, &config_path) { + log_event(format!("Falha ao aplicar configuração via CLI: {error}")); + } else { + log_event("Configuração aplicada via CLI"); + } + } + + let password = password_override + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_PASSWORD.to_string()); + + if let Err(error) = set_password(&exe_path, &password) { + log_event(format!("Falha ao definir senha padrão: {error}")); + } else { + log_event("Senha padrão definida com sucesso"); + log_event("Aplicando senha nos perfis do RustDesk"); + match ensure_password_files(&password) { + Ok(_) => { + log_event("Senha e flags de segurança gravadas em todos os perfis do RustDesk"); + log_password_replication(&password); + } + Err(error) => log_event(format!("Falha ao persistir senha nos perfis: {error}")), + } + + match propagate_password_profile() { + Ok(_) => log_event("Perfil base propagado para ProgramData e perfis de serviço"), + Err(error) => log_event(format!("Falha ao copiar perfil de senha: {error}")), + } + + match replicate_password_artifacts() { + Ok(_) => log_event("Artefatos de senha replicados para o serviço do RustDesk"), + Err(error) => log_event(format!("Falha ao replicar artefatos de senha: {error}")), + } + + if let Err(error) = enforce_security_flags() { + log_event(format!("Falha ao reforçar configuração de senha permanente: {error}")); + } + } + + // Se ja existe um ID preservado E o RustDesk nao foi recem-instalado, usa o ID existente + // Isso garante que reinstalar o Raven nao muda o ID do RustDesk + let custom_id = if let Some(ref existing_id) = preserved_remote_id { + if !freshly_installed { + log_event(format!("Reutilizando ID existente do RustDesk: {}", existing_id)); + Some(existing_id.clone()) + } else { + // Instalacao fresca - define novo ID baseado no machine_id + define_custom_id_from_machine(&exe_path, machine_id) + } + } else { + // Sem ID preservado - define novo ID baseado no machine_id + define_custom_id_from_machine(&exe_path, machine_id) + }; + + if let Err(error) = ensure_service_running(&exe_path) { + log_event(format!("Falha ao reiniciar serviço do RustDesk: {error}")); + } else { + log_event("Serviço RustDesk reiniciado/run ativo"); + } + + let reported_id = match query_id_with_retries(&exe_path, 5) { + Ok(value) => value, + Err(error) => { + log_event(format!("Falha ao obter ID após múltiplas tentativas: {error}")); + match read_remote_id_from_profiles().or_else(|| custom_id.clone()) { + Some(value) => { + log_event(format!("ID obtido via arquivos de perfil: {value}")); + value + } + None => return Err(error), + } + } + }; + + let mut final_id = reported_id.clone(); + + if let Some(expected) = custom_id.as_ref() { + if expected != &reported_id { + log_event(format!( + "ID retornado difere do determinístico ({expected}) -> reaplicando ID determinístico" + )); + + let mut enforced = false; + + match set_custom_id(&exe_path, expected) { + Ok(_) => match query_id_with_retries(&exe_path, 3) { + Ok(rechecked) => { + if &rechecked == expected { + log_event(format!("ID determinístico aplicado com sucesso: {rechecked}")); + final_id = rechecked; + enforced = true; + } else { + log_event(format!( + "ID ainda difere após reaplicação (esperado {expected}, reportado {rechecked}); usando ID reportado" + )); + final_id = rechecked; + } + } + Err(error) => { + log_event(format!( + "Falha ao consultar ID após reaplicação: {error}; usando ID reportado ({reported_id})" + )); + final_id = reported_id.clone(); + } + }, + Err(error) => { + log_event(format!( + "Falha ao reaplicar ID determinístico ({expected}): {error}; usando ID reportado ({reported_id})" + )); + final_id = reported_id.clone(); + } + } + + if !enforced && final_id != *expected { + log_event("Aviso: não foi possível aplicar o ID determinístico; manteremos o ID real fornecido pelo serviço"); + } + } + } + + ensure_remote_id_files(&final_id); + + let version = query_version(&exe_path).ok().or(installed_version); + + let last_provisioned_at = Utc::now().timestamp_millis(); + let result = RustdeskProvisioningResult { + id: final_id.clone(), + password: password.clone(), + installed_version: version.clone(), + updated: freshly_installed, + last_provisioned_at, + }; + + // Salva os dados do RustDesk diretamente no arquivo machine-agent.json + // para evitar conflitos com o Tauri Store do TypeScript + let rustdesk_data = serde_json::json!({ + "id": final_id, + "password": password, + "installedVersion": version, + "updated": freshly_installed, + "lastProvisionedAt": last_provisioned_at, + "lastSyncedAt": serde_json::Value::Null, + "lastError": serde_json::Value::Null + }); + if let Err(error) = upsert_machine_store_value("rustdesk", rustdesk_data) { + log_event(format!("Aviso: falha ao salvar dados do RustDesk no store: {error}")); + } else { + log_event("Dados do RustDesk salvos no machine-agent.json"); + } + + // Sincroniza com o backend imediatamente apos provisionar + // O Rust faz o HTTP direto, sem passar pelo CSP do webview + if let Err(error) = sync_remote_access_with_backend(&result) { + log_event(format!("Aviso: falha ao sincronizar com backend: {error}")); + } else { + log_event("Acesso remoto sincronizado com backend"); + // Atualiza lastSyncedAt no store + let synced_data = serde_json::json!({ + "id": final_id, + "password": password, + "installedVersion": version, + "updated": freshly_installed, + "lastProvisionedAt": last_provisioned_at, + "lastSyncedAt": Utc::now().timestamp_millis(), + "lastError": serde_json::Value::Null + }); + if let Err(e) = upsert_machine_store_value("rustdesk", synced_data) { + log_event(format!("Aviso: falha ao atualizar lastSyncedAt: {e}")); + } else { + log_event("lastSyncedAt atualizado com sucesso"); + } + } + + log_event(format!("Provisionamento concluído. ID final: {final_id}. Versão: {:?}", version)); + + Ok(result) +} + +fn detect_executable_path() -> PathBuf { + let program_files = env::var("PROGRAMFILES").unwrap_or_else(|_| "C:/Program Files".to_string()); + Path::new(&program_files).join("RustDesk").join("rustdesk.exe") +} + +fn ensure_installed(exe_path: &Path) -> Result<(Option, bool), RustdeskError> { + if exe_path.exists() { + return Ok((None, false)); + } + + let cache_root = PathBuf::from(env::var("PROGRAMDATA").unwrap_or_else(|_| "C:/ProgramData".to_string())) + .join(CACHE_DIR_NAME); + fs::create_dir_all(&cache_root)?; + + let (installer_path, version_tag) = download_latest_installer(&cache_root)?; + run_installer(&installer_path)?; + thread::sleep(Duration::from_secs(20)); + + Ok((Some(version_tag), true)) +} + +fn download_latest_installer(cache_root: &Path) -> Result<(PathBuf, String), RustdeskError> { + let client = Client::builder() + .user_agent(USER_AGENT) + .timeout(Duration::from_secs(60)) + .build()?; + let release: ReleaseResponse = client.get(RELEASES_API).send()?.error_for_status()?.json()?; + let asset = release + .assets + .iter() + .find(|a| a.name.ends_with("x86_64.exe")) + .ok_or(RustdeskError::AssetMissing)?; + let target_path = cache_root.join(&asset.name); + if target_path.exists() { + return Ok((target_path, release.tag_name)); + } + + let mut response = client.get(&asset.browser_download_url).send()?.error_for_status()?; + let mut output = File::create(&target_path)?; + response.copy_to(&mut output)?; + Ok((target_path, release.tag_name)) +} + +fn run_installer(installer_path: &Path) -> Result<(), RustdeskError> { + let status = hidden_command(installer_path) + .arg("--silent-install") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status()?; + if !status.success() { + return Err(RustdeskError::CommandFailed { + command: format!("{} --silent-install", installer_path.display()), + status: status.code(), + }); + } + Ok(()) +} + +fn write_config_files() -> Result { + let config_contents = build_config_contents(); + let main_path = program_data_config_dir().join("RustDesk2.toml"); + write_file(&main_path, &config_contents)?; + log_event(format!( + "Config principal gravada em {}", + main_path.display() + )); + + let _ = ensure_service_profiles_writable_preflight(); + for service_dir in service_profile_dirs() { + let service_profile = service_dir.join("RustDesk2.toml"); + if let Err(error) = write_file(&service_profile, &config_contents) { + log_event(format!( + "Falha ao gravar config no perfil do serviço ({}): {error}", + service_profile.display() + )); + } + } + + if let Some(appdata_path) = user_appdata_config_path("RustDesk2.toml") { + if let Err(error) = write_file(&appdata_path, &config_contents) { + log_event(format!( + "Falha ao atualizar config no AppData do usuário: {error}" + )); + } + } + + Ok(main_path) +} + +fn write_file(path: &Path, contents: &str) -> Result<(), io::Error> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path)?; + file.write_all(contents.as_bytes()) +} + +fn program_data_config_dir() -> PathBuf { + PathBuf::from(env::var("PROGRAMDATA").unwrap_or_else(|_| "C:/ProgramData".to_string())) + .join("RustDesk") + .join("config") +} + +fn user_appdata_config_dir() -> Option { + env::var("APPDATA") + .ok() + .map(|value| Path::new(&value).join("RustDesk").join("config")) +} + +fn user_appdata_config_path(filename: &str) -> Option { + user_appdata_config_dir().map(|dir| dir.join(filename)) +} + +fn build_config_contents() -> String { + format!( + r#"[options] +key = "{key}" +relay-server = "{host}" +custom-rendezvous-server = "{host}" +api-server = "https://{host}" +verification-method = "{verification}" +approve-mode = "{approve}" +"#, + host = SERVER_HOST, + key = SERVER_KEY, + verification = SECURITY_VERIFICATION_VALUE, + approve = SECURITY_APPROVE_MODE_VALUE, + ) +} + +fn apply_config(exe_path: &Path, config_path: &Path) -> Result<(), RustdeskError> { + let status = hidden_command(exe_path) + .arg("--import-config") + .arg(config_path) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status()?; + if !status.success() { + return Err(RustdeskError::CommandFailed { + command: format!("{} --import-config {}", exe_path.display(), config_path.display()), + status: status.code(), + }); + } + Ok(()) +} + +fn set_password(exe_path: &Path, secret: &str) -> Result<(), RustdeskError> { + run_with_args(exe_path, &["--password", secret]) +} + +fn set_custom_id(exe_path: &Path, machine_id: &str) -> Result { + let custom_id = derive_numeric_id(machine_id); + run_with_args(exe_path, &["--set-id", &custom_id])?; + Ok(custom_id) +} + +fn derive_numeric_id(machine_id: &str) -> String { + let mut hasher = Sha256::new(); + hasher.update(machine_id.as_bytes()); + let hash = hasher.finalize(); + let mut bytes = [0u8; 8]; + bytes.copy_from_slice(&hash[..8]); + let value = u64::from_le_bytes(bytes); + let num = (value % 900_000_000) + 100_000_000; + format!("{:09}", num) +} + +fn ensure_service_running(exe_path: &Path) -> Result<(), RustdeskError> { + ensure_service_installed(exe_path)?; + + if let Err(error) = configure_service_startup() { + log_event(format!( + "Aviso: não foi possível reforçar autostart/recuperação do serviço RustDesk: {error}" + )); + } + + fn start_sequence() -> Result<(), RustdeskError> { + let _ = run_sc(&["stop", SERVICE_NAME]); + thread::sleep(Duration::from_secs(2)); + let _ = run_sc(&["config", SERVICE_NAME, &format!("start= {}", "auto")]); + run_sc(&["start", SERVICE_NAME]) + } + + let _ = match start_sequence() { + Ok(_) => Ok(()), + Err(RustdeskError::CommandFailed { command: _, status: Some(5), .. }) => { + log_event("SC retornou acesso negado; tentando ajustar ACL dos perfis do serviço..."); + ensure_service_profiles_writable_preflight().map_err(|error| RustdeskError::CommandFailed { + command: format!("fix_acl ({error})"), + status: Some(5), + })?; + let _ = run_sc(&["stop", SERVICE_NAME]); + let _ = start_sequence(); + Ok(()) + } + Err(error) => Err(error), + }; + + remove_rustdesk_autorun_artifacts(); + + // Revalida se o serviço realmente subiu; se não, reinstala e tenta novamente. + match query_service_state() { + Some(state) if state.eq_ignore_ascii_case("running") => Ok(()), + _ => { + log_event("Serviço RustDesk não está em execução após tentativa de start; reaplicando --install-service e start"); + let _ = run_with_args(exe_path, &["--install-service"]); + let _ = run_sc(&["config", SERVICE_NAME, &format!("start= {}", "auto")]); + if let Err(error) = start_sequence() { + log_event(format!( + "Falha ao subir o serviço RustDesk mesmo após reinstalação: {error}" + )); + } + Ok(()) + } + } +} + +fn configure_service_startup() -> Result<(), RustdeskError> { + let start_arg = format!("start= {}", "auto"); + run_sc(&["config", SERVICE_NAME, &start_arg])?; + + let reset_arg = format!("reset= {}", "86400"); + let actions_arg = "actions= restart/5000/restart/5000/restart/5000"; + let failure_actions_applied = run_sc(&["failure", SERVICE_NAME, &reset_arg, actions_arg]).is_ok(); + let _ = run_sc(&["failureflag", SERVICE_NAME, "1"]); + + if failure_actions_applied { + log_event("Serviço RustDesk configurado para reiniciar automaticamente em caso de falha"); + } else { + log_event("Aviso: não foi possível configurar recuperação automática do serviço RustDesk"); + } + + Ok(()) +} + +fn query_service_state() -> Option { + let output = hidden_command("sc") + .args(["query", SERVICE_NAME]) + .output() + .ok()?; + if !output.status.success() { + return None; + } + let stdout = String::from_utf8_lossy(&output.stdout); + for line in stdout.lines() { + if let Some(pos) = line.find("STATE") { + // Example: " STATE : 4 RUNNING" + let state = line[pos..].to_string(); + if state.to_lowercase().contains("running") { + return Some("running".to_string()); + } + if state.to_lowercase().contains("stopped") { + return Some("stopped".to_string()); + } + } + } + None +} + +fn remove_rustdesk_autorun_artifacts() { + // Remove atalhos de inicialização automática para evitar abrir GUI a cada boot/login. + let mut startup_paths: Vec = Vec::new(); + if let Ok(appdata) = env::var("APPDATA") { + startup_paths.push( + Path::new(&appdata) + .join("Microsoft") + .join("Windows") + .join("Start Menu") + .join("Programs") + .join("Startup") + .join("RustDesk.lnk"), + ); + } + startup_paths.push( + Path::new("C:\\ProgramData") + .join("Microsoft") + .join("Windows") + .join("Start Menu") + .join("Programs") + .join("Startup") + .join("RustDesk.lnk"), + ); + + for path in startup_paths { + if path.exists() { + match fs::remove_file(&path) { + Ok(_) => log_event(format!("Atalho de inicialização do RustDesk removido: {}", path.display())), + Err(error) => log_event(format!( + "Falha ao remover atalho de inicialização do RustDesk ({}): {}", + path.display(), + error + )), + } + } + } + + for hive in ["HKCU", "HKLM"] { + let reg_path = format!(r"{}\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", hive); + let status = hidden_command("reg") + .args(["delete", ®_path, "/v", "RustDesk", "/f"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + if let Ok(code) = status { + if code.success() { + log_event(format!("Entrada de auto-run RustDesk removida de {}", reg_path)); + } + } + } +} + +fn stop_rustdesk_processes() -> Result<(), RustdeskError> { + if let Err(error) = try_stop_service() { + log_event(format!( + "Não foi possível parar o serviço RustDesk antes da sincronização: {error}" + )); + } + + let status = hidden_command("taskkill") + .args(["/F", "/T", "/IM", "rustdesk.exe"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status()?; + + if status.success() || matches!(status.code(), Some(128)) { + Ok(()) + } else { + Err(RustdeskError::CommandFailed { + command: "taskkill /F /T /IM rustdesk.exe".into(), + status: status.code(), + }) + } +} + +fn try_stop_service() -> Result<(), RustdeskError> { + match run_sc(&["stop", SERVICE_NAME]) { + Ok(_) => { + thread::sleep(Duration::from_secs(2)); + Ok(()) + } + Err(RustdeskError::CommandFailed { status: Some(code), .. }) if code == 1060 || code == 1062 => Ok(()), + Err(RustdeskError::CommandFailed { status: Some(5), .. }) => { + stop_service_elevated().map_err(|error| RustdeskError::CommandFailed { + command: format!("stop_service_elevated ({error})"), + status: Some(5), + }) + } + Err(error) => Err(error), + } +} + +fn run_sc(args: &[&str]) -> Result<(), RustdeskError> { + let status = hidden_command("sc") + .args(args) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status()?; + if !status.success() { + return Err(RustdeskError::CommandFailed { + command: format!("sc {}", args.join(" ")), + status: status.code(), + }); + } + Ok(()) +} + +fn ensure_service_installed(exe_path: &Path) -> Result<(), RustdeskError> { + if run_sc(&["query", SERVICE_NAME]).is_ok() { + return Ok(()); + } + + log_event("Serviço RustDesk não encontrado; instalando via CLI"); + run_with_args(exe_path, &["--install-service"])?; + Ok(()) +} + +fn query_id_with_retries(exe_path: &Path, attempts: usize) -> Result { + let mut last_error: Option = None; + for attempt in 0..attempts { + match query_id(exe_path) { + Ok(value) if !value.trim().is_empty() => return Ok(value), + Ok(_) => { + last_error = Some(RustdeskError::MissingId); + } + Err(error) => { + last_error = Some(error); + } + } + if attempt + 1 < attempts { + thread::sleep(Duration::from_millis(800)); + } + } + Err(last_error.unwrap_or(RustdeskError::MissingId)) +} + +fn query_id(exe_path: &Path) -> Result { + let output = hidden_command(exe_path) + .arg("--get-id") + .output()?; + if !output.status.success() { + return Err(RustdeskError::CommandFailed { + command: format!("{} --get-id", exe_path.display()), + status: output.status.code(), + }); + } + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if stdout.is_empty() { + return Err(RustdeskError::MissingId); + } + Ok(stdout) +} + +fn query_version(exe_path: &Path) -> Result { + let output = hidden_command(exe_path) + .arg("--version") + .output()?; + if !output.status.success() { + return Err(RustdeskError::CommandFailed { + command: format!("{} --version", exe_path.display()), + status: output.status.code(), + }); + } + Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) +} + +fn ensure_remote_id_files(id: &str) { + for dir in remote_id_directories() { + let path = dir.join("RustDesk_local.toml"); + match write_remote_id_value(&path, id) { + Ok(_) => log_event(format!( + "remote_id atualizado para {} em {}", + id, + path.display() + )), + Err(error) => log_event(format!( + "Falha ao atualizar remote_id em {}: {error}", + path.display() + )), + } + } +} + +fn remote_id_directories() -> Vec { + let mut dirs = Vec::new(); + dirs.push(program_data_config_dir()); + for profile in service_profile_dirs() { + dirs.push(profile); + } + if let Some(appdir) = user_appdata_config_dir() { + dirs.push(appdir); + } + dirs +} + +fn service_profile_dirs() -> Vec { + vec![ + PathBuf::from(LOCAL_SERVICE_CONFIG), + PathBuf::from(LOCAL_SYSTEM_CONFIG), + ] +} + +fn propagation_destinations() -> Vec { + let mut dirs = Vec::new(); + dirs.push(program_data_config_dir()); + dirs.extend(service_profile_dirs()); + dirs +} + +fn ensure_password_files(secret: &str) -> Result<(), String> { + let mut errors = Vec::new(); + + for dir in remote_id_directories() { + let password_path = dir.join("RustDesk.toml"); + if let Err(error) = write_toml_kv(&password_path, "password", secret) { + errors.push(format!("{} -> {}", password_path.display(), error)); + } else { + log_event(format!( + "Senha escrita via fallback em {}", + password_path.display() + )); + } + + let local_path = dir.join("RustDesk_local.toml"); + if let Err(error) = write_toml_kv(&local_path, "verification-method", SECURITY_VERIFICATION_VALUE) { + log_event(format!( + "Falha ao ajustar verification-method em {}: {error}", + local_path.display() + )); + } else { + log_event(format!( + "verification-method atualizado para {} em {}", + SECURITY_VERIFICATION_VALUE, + local_path.display() + )); + } + + let rustdesk2_path = dir.join("RustDesk2.toml"); + if let Err(error) = enforce_security_in_rustdesk2(&rustdesk2_path) { + log_event(format!( + "Falha ao ajustar flags no RustDesk2.toml em {}: {error}", + rustdesk2_path.display() + )); + } + + if let Err(error) = write_toml_kv(&local_path, "approve-mode", SECURITY_APPROVE_MODE_VALUE) { + log_event(format!( + "Falha ao ajustar approve-mode em {}: {error}", + local_path.display() + )); + } else { + log_event(format!( + "approve-mode atualizado para {} em {}", + SECURITY_APPROVE_MODE_VALUE, + local_path.display() + )); + } + } + + if errors.is_empty() { + Ok(()) + } else { + Err(errors.join(" | ")) + } +} + +fn enforce_security_flags() -> Result<(), String> { + let mut errors = Vec::new(); + for dir in remote_id_directories() { + let local_path = dir.join("RustDesk_local.toml"); + if let Err(error) = write_toml_kv(&local_path, "verification-method", SECURITY_VERIFICATION_VALUE) { + errors.push(format!("{} -> {}", local_path.display(), error)); + } else { + log_event(format!( + "verification-method atualizado para {} em {}", + SECURITY_VERIFICATION_VALUE, + local_path.display() + )); + } + + if let Err(error) = write_toml_kv(&local_path, "approve-mode", SECURITY_APPROVE_MODE_VALUE) { + errors.push(format!("{} -> {}", local_path.display(), error)); + } else { + log_event(format!( + "approve-mode atualizado para {} em {}", + SECURITY_APPROVE_MODE_VALUE, + local_path.display() + )); + } + } + + if errors.is_empty() { + Ok(()) + } else { + Err(errors.join(" | ")) + } +} + +fn enforce_security_in_rustdesk2(path: &Path) -> io::Result<()> { + write_toml_kv(path, "verification-method", SECURITY_VERIFICATION_VALUE)?; + write_toml_kv(path, "approve-mode", SECURITY_APPROVE_MODE_VALUE)?; + Ok(()) +} + +fn propagate_password_profile() -> io::Result { + let Some(src_dir) = user_appdata_config_dir() else { + log_event("AppData do usuário não disponível para copiar RustDesk.toml (propagação ignorada)"); + return Ok(false); + }; + + let mut propagated = false; + + for filename in PROPAGATION_FILES { + let src_path = src_dir.join(filename); + if !src_path.exists() { + continue; + } + log_event(format!( + "Copiando {} para ProgramData/serviços", + src_path.display() + )); + + for dest_root in propagation_destinations() { + let target_path = dest_root.join(filename); + copy_overwrite(&src_path, &target_path)?; + log_event(format!( + "{} propagado para {}", + filename, + target_path.display() + )); + propagated = true; + } + } + + if !propagated { + log_event("Nenhum arquivo de perfil encontrado para propagação; aplicando fallback"); + } + + Ok(propagated) +} + +fn replicate_password_artifacts() -> io::Result<()> { + let Some(src) = user_appdata_config_dir() else { + return Ok(()); + }; + let destinations = propagation_destinations(); + let candidates = ["password", "passwd", "passwd.txt"]; + + for dest in destinations { + fs::create_dir_all(&dest)?; + for name in candidates { + let source_path = src.join(name); + if !source_path.exists() { + continue; + } + let metadata = match fs::metadata(&source_path) { + Ok(data) => data, + Err(_) => continue, + }; + if !metadata.is_file() || metadata.len() == 0 { + continue; + } + + let target_path = dest.join(name); + copy_overwrite(&source_path, &target_path)?; + log_event(format!( + "Artefato de senha {name} replicado para {}", + target_path.display() + )); + } + } + + Ok(()) +} + +fn purge_existing_rustdesk_profiles() -> Result<(), String> { + let mut errors = Vec::new(); + + for dir in remote_id_directories() { + match purge_config_dir(&dir) { + Ok(true) => { + log_event(format!( + "Perfis antigos removidos em {}", + dir.display() + )); + } + Ok(false) => {} + Err(error) => errors.push(format!("{} -> {error}", dir.display())), + } + } + + if errors.is_empty() { + Ok(()) + } else { + Err(errors.join(" | ")) + } +} + +fn purge_config_dir(dir: &Path) -> Result { + if !dir.exists() { + return Ok(false); + } + + let mut removed = false; + fs::create_dir_all(dir)?; + + for name in RUSTDESK_CONFIG_FILES { + let path = dir.join(name); + if path.is_dir() { + fs::remove_dir_all(&path)?; + removed = true; + continue; + } + if path.exists() { + fs::remove_file(&path)?; + removed = true; + } + } + + Ok(removed) +} + +#[allow(dead_code)] +fn run_powershell_elevated(script: &str) -> Result<(), String> { + let temp_dir = env::temp_dir(); + let payload = temp_dir.join("raven_payload.ps1"); + fs::write(&payload, script).map_err(|error| format!("write payload: {error}"))?; + + let launcher = temp_dir.join("raven_launcher.ps1"); + let launcher_body = format!( + r#" +$ErrorActionPreference='Stop' +$psi = New-Object System.Diagnostics.ProcessStartInfo +$psi.FileName = 'powershell.exe' +$psi.Arguments = '-NoProfile -ExecutionPolicy Bypass -File "{payload}"' +$psi.Verb = 'runas' +$psi.WindowStyle = 'Hidden' +$process = [System.Diagnostics.Process]::Start($psi) +$process.WaitForExit() +exit $process.ExitCode +"#, + payload = payload.display() + ); + fs::write(&launcher, launcher_body).map_err(|error| format!("write launcher: {error}"))?; + + let status = Command::new("powershell") + .creation_flags(CREATE_NO_WINDOW) + .args([ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + &launcher.to_string_lossy(), + ]) + .status() + .map_err(|error| format!("spawn ps: {error}"))?; + + let _ = fs::remove_file(&launcher); + let _ = fs::remove_file(&payload); + + if let Some(code) = status.code() { + if code == 0 || code == 1 { + return Ok(()); + } + } else if status.success() { + return Ok(()); + } + Err(format!("elevated ps exit {:?}", status.code())) +} + +#[allow(dead_code)] +fn fix_profile_acl(target: &Path) -> Result<(), String> { + let target_str = target.display().to_string(); + let transcript = env::temp_dir().join("raven_acl_ps.log"); + let log_str = transcript.display().to_string(); + let script = format!( + r#" +$ErrorActionPreference='Stop' +Start-Transcript -Path '{log}' -Force +try {{ + if (-not (Test-Path '{target}')) {{ New-Item -ItemType Directory -Force -Path '{target}' | Out-Null }} + + & takeown /F '{target}' /R /D Y + $takeCode = $LASTEXITCODE + + & icacls '{target}' /grant '*S-1-5-32-544:(OI)(CI)F' '*S-1-5-19:(OI)(CI)F' '*S-1-5-32-545:(OI)(CI)M' /T /C /Q + $icaCode = $LASTEXITCODE + + if (($takeCode -eq 0) -and ($icaCode -in 0,1)) {{ exit 0 }} + if ($icaCode -ne 0) {{ exit $icaCode }} + exit $takeCode +}} catch {{ + Write-Host ("exception: " + ($_.Exception.Message)) + exit 1 +}} finally {{ + try {{ Stop-Transcript | Out-Null }} catch {{ }} +}} +"#, + target = target_str, + log = log_str + ); + + let result = run_powershell_elevated(&script); + if result.is_err() { + if let Ok(content) = fs::read_to_string(&transcript) { + log_event(format!( + "ACL transcript para {}:\n{}", + target.display(), content + )); + } + } + let _ = fs::remove_file(&transcript); + result +} + +fn ensure_service_profiles_writable_preflight() -> Result<(), String> { + // Verificamos se os diretorios de perfil sao graváveis + // Se nao forem, apenas logamos aviso - o Raven Service deve lidar com isso + // Nao usamos elevacao para evitar UAC adicional + let mut blocked_dirs = Vec::new(); + for dir in service_profile_dirs() { + if !can_write_dir(&dir) { + blocked_dirs.push(dir); + } + } + + if blocked_dirs.is_empty() { + return Ok(()); + } + + // Apenas logamos aviso - o serviço RavenService deve lidar com permissões + log_event(format!( + "Aviso: alguns perfis de serviço não são graváveis: {:?}. O Raven Service deve configurar permissões.", + blocked_dirs.iter().map(|d| d.display().to_string()).collect::>() + )); + + // Retornamos Ok para não bloquear o fluxo + // O Raven Service, rodando como LocalSystem, pode gravar nesses diretórios + Ok(()) +} + +fn stop_service_elevated() -> Result<(), String> { + // Tentamos parar o serviço RustDesk sem elevação + // Se falhar, apenas logamos aviso - o Raven Service pode lidar com isso + // Não usamos elevação para evitar UAC adicional + let output = Command::new("sc") + .args(["stop", "RustDesk"]) + .output(); + + match output { + Ok(result) => { + if result.status.success() { + // Aguarda um pouco para o serviço parar + std::thread::sleep(std::time::Duration::from_secs(2)); + Ok(()) + } else { + let stderr = String::from_utf8_lossy(&result.stderr); + log_event(format!( + "Aviso: não foi possível parar o serviço RustDesk sem elevação: {}", + stderr.trim() + )); + // Retornamos Ok para não bloquear - o serviço pode estar já parado + Ok(()) + } + } + Err(e) => { + log_event(format!("Aviso: falha ao executar sc stop RustDesk: {e}")); + Ok(()) + } + } +} + +fn can_write_dir(dir: &Path) -> bool { + if fs::create_dir_all(dir).is_err() { + return false; + } + let probe = dir.join(".raven_acl_probe"); + match OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&probe) + { + Ok(mut file) => { + if file.write_all(b"ok").is_err() { + let _ = fs::remove_file(&probe); + return false; + } + let _ = fs::remove_file(&probe); + true + } + Err(_) => false, + } +} + +fn write_remote_id_value(path: &Path, id: &str) -> io::Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + let replacement = format!("remote_id = '{}'\n", id); + if let Ok(existing) = fs::read_to_string(path) { + let mut replaced = false; + let mut buffer = String::with_capacity(existing.len() + replacement.len()); + for line in existing.lines() { + if line.trim_start().starts_with("remote_id") { + buffer.push_str(&replacement); + replaced = true; + } else { + buffer.push_str(line); + buffer.push('\n'); + } + } + if !replaced { + buffer.push_str(&replacement); + } + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path)?; + file.write_all(buffer.as_bytes()) + } else { + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path)?; + file.write_all(replacement.as_bytes()) + } +} + +fn write_toml_kv(path: &Path, key: &str, value: &str) -> io::Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + if path.is_dir() { + fs::remove_dir_all(path)?; + } + let sanitized = value.replace('\\', "\\\\").replace('"', "\\\""); + let replacement = format!("{key} = \"{sanitized}\"\n"); + let existing = fs::read_to_string(path).unwrap_or_default(); + let mut replaced = false; + let mut buffer = String::with_capacity(existing.len() + replacement.len()); + for line in existing.lines() { + let trimmed = line.trim_start(); + if trimmed.starts_with(&format!("{key} ")) || trimmed.starts_with(&format!("{key}=")) { + buffer.push_str(&replacement); + replaced = true; + } else { + buffer.push_str(line); + buffer.push('\n'); + } + } + if !replaced { + buffer.push_str(&replacement); + } + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path)?; + file.write_all(buffer.as_bytes()) +} + +fn read_remote_id_from_profiles() -> Option { + for dir in remote_id_directories() { + for candidate in [dir.join("RustDesk_local.toml"), dir.join("RustDesk.toml")] { + if let Some(id) = read_remote_id_file(&candidate) { + if !id.is_empty() { + return Some(id); + } + } + } + } + None +} + +fn read_remote_id_file(path: &Path) -> Option { + let content = fs::read_to_string(path).ok()?; + for line in content.lines() { + if let Some(value) = parse_assignment(line, "remote_id") { + return Some(value); + } + } + None +} + +fn parse_assignment(line: &str, key: &str) -> Option { + let trimmed = line.trim(); + if !trimmed.starts_with(key) { + return None; + } + let (_, rhs) = trimmed.split_once('=')?; + let value = rhs.trim().trim_matches(|c| c == '\'' || c == '"'); + if value.is_empty() { + None + } else { + Some(value.to_string()) + } +} + +fn run_with_args(exe_path: &Path, args: &[&str]) -> Result<(), RustdeskError> { + let status = hidden_command(exe_path) + .args(args) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status()?; + if !status.success() { + return Err(RustdeskError::CommandFailed { + command: format!("{} {}", exe_path.display(), args.join(" ")), + status: status.code(), + }); + } + Ok(()) +} + +fn log_password_replication(secret: &str) { + for dir in remote_id_directories() { + let primary = dir.join("RustDesk.toml"); + log_password_match(&primary, secret); + + let local_path = dir.join("RustDesk_local.toml"); + log_password_match(&local_path, secret); + } +} + +fn log_password_match(path: &Path, secret: &str) { + match read_password_from_file(path) { + Some(value) if value == secret => { + log_event(format!( + "Senha confirmada em {} ({})", + path.display(), + mask_secret(&value) + )); + } + Some(value) => { + log_event(format!( + "Aviso: senha divergente ({}) em {}", + mask_secret(&value), + path.display() + )); + } + None => { + log_event(format!( + "Aviso: chave 'password' não encontrada em {}", + path.display() + )); + } + } +} + +fn read_password_from_file(path: &Path) -> Option { + let content = fs::read_to_string(path).ok()?; + for line in content.lines() { + if let Some(value) = parse_assignment(line, "password") { + return Some(value); + } + } + None +} + +fn mask_secret(secret: &str) -> String { + if secret.is_empty() { + return "".to_string(); + } + let chars: Vec = secret.chars().collect(); + if chars.len() <= 4 { + return "*".repeat(chars.len()); + } + let prefix: String = chars.iter().take(2).copied().collect(); + let suffix: String = chars + .iter() + .rev() + .take(2) + .copied() + .collect::>() + .into_iter() + .rev() + .collect(); + format!("{}***{}", prefix, suffix) +} + +fn hidden_command(program: impl AsRef) -> Command { + let mut cmd = Command::new(program); + cmd.creation_flags(CREATE_NO_WINDOW); + cmd +} + +fn copy_overwrite(src: &Path, dst: &Path) -> io::Result<()> { + if let Some(parent) = dst.parent() { + fs::create_dir_all(parent)?; + } + if dst.is_dir() { + fs::remove_dir_all(dst)?; + } else if dst.exists() { + fs::remove_file(dst)?; + } + fs::copy(src, dst)?; + Ok(()) +} + +fn log_event(message: impl AsRef) { + if let Some(dir) = logs_directory() { + if let Err(error) = append_log(dir, message.as_ref()) { + eprintln!("[rustdesk][log] Falha ao registrar log: {error}"); + } + } +} + +fn logs_directory() -> Option { + let base = env::var("LOCALAPPDATA").ok()?; + Some( + Path::new(&base) + .join("br.com.esdrasrenan.sistemadechamados") + .join("logs"), + ) +} + +fn append_log(dir: PathBuf, message: &str) -> io::Result<()> { + fs::create_dir_all(&dir)?; + let log_path = dir.join("rustdesk.log"); + let mut file = OpenOptions::new() + .create(true) + .append(true) + .open(log_path)?; + let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S"); + writeln!(file, "[{timestamp}] {message}")?; + Ok(()) +} + +fn raven_appdata_root() -> Option { + env::var("LOCALAPPDATA") + .ok() + .map(|value| Path::new(&value).join(APP_IDENTIFIER)) +} + +fn machine_store_path() -> Option { + raven_appdata_root().map(|dir| dir.join(MACHINE_STORE_FILENAME)) +} + +fn read_machine_store_object() -> Option> { + let path = machine_store_path()?; + let contents = fs::read_to_string(path).ok()?; + let value: JsonValue = serde_json::from_str(&contents).ok()?; + value.as_object().cloned() +} + +fn write_machine_store_object(map: JsonMap) -> Result<(), String> { + let path = machine_store_path().ok_or_else(|| "LOCALAPPDATA não disponível".to_string())?; + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).map_err(|error| format!("mkdir AppData: {error}"))?; + } + let serialized = serde_json::to_vec_pretty(&JsonValue::Object(map)) + .map_err(|error| format!("serialize machine-agent: {error}"))?; + fs::write(&path, serialized).map_err(|error| format!("write machine-agent: {error}"))?; + Ok(()) +} + +fn upsert_machine_store_value(key: &str, value: JsonValue) -> Result<(), String> { + let mut map = read_machine_store_object().unwrap_or_default(); + map.insert(key.to_string(), value); + write_machine_store_object(map) +} + +#[allow(dead_code)] +fn machine_store_key_exists(key: &str) -> bool { + read_machine_store_object() + .map(|map| map.contains_key(key)) + .unwrap_or(false) +} + +#[allow(dead_code)] +fn acl_flag_file_path() -> Option { + raven_appdata_root().map(|dir| dir.join(ACL_FLAG_FILENAME)) +} + +#[allow(dead_code)] +fn has_acl_unlock_flag() -> bool { + if let Some(flag) = acl_flag_file_path() { + if flag.exists() { + return true; + } + } + machine_store_key_exists(RUSTDESK_ACL_STORE_KEY) +} + +#[allow(dead_code)] +fn mark_acl_unlock_flag() { + let timestamp = Utc::now().timestamp_millis(); + if let Some(flag_path) = acl_flag_file_path() { + if let Some(parent) = flag_path.parent() { + let _ = fs::create_dir_all(parent); + } + if let Err(error) = fs::write(&flag_path, timestamp.to_string()) { + log_event(format!( + "Falha ao gravar flag de ACL em {}: {error}", + flag_path.display() + )); + } + } + + if let Err(error) = upsert_machine_store_value(RUSTDESK_ACL_STORE_KEY, JsonValue::from(timestamp)) { + log_event(format!( + "Falha ao registrar flag de ACL no machine-agent: {error}" + )); + } +} + +fn get_machine_store_path() -> Result { + let base = env::var("LOCALAPPDATA") + .map_err(|_| RustdeskError::MissingId)?; + Ok(Path::new(&base) + .join(APP_IDENTIFIER) + .join(MACHINE_STORE_FILENAME)) +} + +fn sync_remote_access_with_backend(result: &crate::RustdeskProvisioningResult) -> Result<(), RustdeskError> { + log_event("Iniciando sincronizacao com backend..."); + + // Le token e config do store + let store_path = get_machine_store_path()?; + let store_content = fs::read_to_string(&store_path) + .map_err(RustdeskError::Io)?; + let store: serde_json::Value = serde_json::from_str(&store_content) + .map_err(|_| RustdeskError::MissingId)?; + + let token = store.get("token") + .and_then(|v| v.as_str()) + .ok_or(RustdeskError::MissingId)?; + + let config = store.get("config") + .ok_or(RustdeskError::MissingId)?; + + let machine_id = config.get("machineId") + .and_then(|v| v.as_str()) + .ok_or(RustdeskError::MissingId)?; + + let api_base_url = config.get("apiBaseUrl") + .and_then(|v| v.as_str()) + .unwrap_or("https://tickets.esdrasrenan.com.br"); + + log_event(format!("Sincronizando com backend: {} (machineId: {})", api_base_url, machine_id)); + + // Monta payload conforme schema esperado pelo backend + // Schema: { machineToken, provider, identifier, password?, url?, username?, notes? } + let payload = serde_json::json!({ + "machineToken": token, + "provider": "RustDesk", + "identifier": result.id, + "password": result.password, + "notes": format!("Versao: {}. Provisionado em: {}", + result.installed_version.as_deref().unwrap_or("desconhecida"), + result.last_provisioned_at) + }); + + // Faz POST para /api/machines/remote-access + let client = Client::builder() + .user_agent(USER_AGENT) + .timeout(Duration::from_secs(30)) + .build()?; + + let url = format!("{}/api/machines/remote-access", api_base_url); + let response = client.post(&url) + .header("Content-Type", "application/json") + .header("Idempotency-Key", format!("{}:RustDesk:{}", machine_id, result.id)) + .body(payload.to_string()) + .send()?; + + if response.status().is_success() { + log_event(format!("Sync com backend OK: status {}", response.status())); + Ok(()) + } else { + let status = response.status(); + let body = response.text().unwrap_or_default(); + let body_preview = if body.len() > 200 { &body[..200] } else { &body }; + log_event(format!("Sync com backend falhou: {} - {}", status, body_preview)); + Err(RustdeskError::CommandFailed { + command: "sync_remote_access".to_string(), + status: Some(status.as_u16() as i32) + }) + } +} diff --git a/apps/desktop/src-tauri/src/service_client.rs b/apps/desktop/src-tauri/src/service_client.rs new file mode 100644 index 0000000..f2af2ed --- /dev/null +++ b/apps/desktop/src-tauri/src/service_client.rs @@ -0,0 +1,244 @@ +//! Cliente IPC para comunicacao com o Raven Service +//! +//! Este modulo permite que o app Tauri se comunique com o Raven Service +//! via Named Pipes para executar operacoes privilegiadas. + +#![allow(dead_code)] + +use serde::{Deserialize, Serialize}; +use std::io::{BufRead, BufReader, Write}; +use std::time::Duration; +use thiserror::Error; + +const PIPE_NAME: &str = r"\\.\pipe\RavenService"; + +#[derive(Debug, Error)] +pub enum ServiceClientError { + #[error("Servico nao disponivel: {0}")] + ServiceUnavailable(String), + + #[error("Erro de comunicacao: {0}")] + CommunicationError(String), + + #[error("Erro de serializacao: {0}")] + SerializationError(#[from] serde_json::Error), + + #[error("Erro do servico: {message} (code: {code})")] + ServiceError { code: i32, message: String }, + + #[error("Timeout aguardando resposta")] + Timeout, +} + +#[derive(Debug, Serialize)] +struct Request { + id: String, + method: String, + params: serde_json::Value, +} + +#[derive(Debug, Deserialize)] +struct Response { + id: String, + result: Option, + error: Option, +} + +#[derive(Debug, Deserialize)] +struct ErrorResponse { + code: i32, + message: String, +} + +// ============================================================================= +// Tipos de Resultado +// ============================================================================= + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UsbPolicyResult { + pub success: bool, + pub policy: String, + pub error: Option, + pub applied_at: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RustdeskResult { + pub id: String, + pub password: String, + pub installed_version: Option, + pub updated: bool, + pub last_provisioned_at: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RustdeskStatus { + pub installed: bool, + pub running: bool, + pub id: Option, + pub version: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct HealthCheckResult { + pub status: String, + pub service: String, + pub version: String, + pub timestamp: i64, +} + +// ============================================================================= +// Cliente +// ============================================================================= + +/// Verifica se o servico esta disponivel +pub fn is_service_available() -> bool { + health_check().is_ok() +} + +/// Verifica saude do servico +pub fn health_check() -> Result { + let response = call_service("health_check", serde_json::json!({}))?; + serde_json::from_value(response).map_err(|e| e.into()) +} + +/// Aplica politica de USB +pub fn apply_usb_policy(policy: &str) -> Result { + let response = call_service( + "apply_usb_policy", + serde_json::json!({ "policy": policy }), + )?; + serde_json::from_value(response).map_err(|e| e.into()) +} + +/// Obtem politica de USB atual +pub fn get_usb_policy() -> Result { + let response = call_service("get_usb_policy", serde_json::json!({}))?; + response + .get("policy") + .and_then(|p| p.as_str()) + .map(String::from) + .ok_or_else(|| ServiceClientError::CommunicationError("Resposta invalida".into())) +} + +/// Provisiona RustDesk +pub fn provision_rustdesk( + config: Option<&str>, + password: Option<&str>, + machine_id: Option<&str>, +) -> Result { + let params = serde_json::json!({ + "config": config, + "password": password, + "machineId": machine_id, + }); + let response = call_service("provision_rustdesk", params)?; + serde_json::from_value(response).map_err(|e| e.into()) +} + +/// Obtem status do RustDesk +pub fn get_rustdesk_status() -> Result { + let response = call_service("get_rustdesk_status", serde_json::json!({}))?; + serde_json::from_value(response).map_err(|e| e.into()) +} + +// ============================================================================= +// Comunicacao IPC +// ============================================================================= + +fn call_service( + method: &str, + params: serde_json::Value, +) -> Result { + // Gera ID unico para a requisicao + let id = uuid::Uuid::new_v4().to_string(); + + let request = Request { + id: id.clone(), + method: method.to_string(), + params, + }; + + // Serializa requisicao + let request_json = serde_json::to_string(&request)?; + + // Conecta ao pipe + let mut pipe = connect_to_pipe()?; + + // Envia requisicao + writeln!(pipe, "{}", request_json).map_err(|e| { + ServiceClientError::CommunicationError(format!("Erro ao enviar requisicao: {}", e)) + })?; + pipe.flush().map_err(|e| { + ServiceClientError::CommunicationError(format!("Erro ao flush: {}", e)) + })?; + + // Le resposta + let mut reader = BufReader::new(pipe); + let mut response_line = String::new(); + + reader.read_line(&mut response_line).map_err(|e| { + ServiceClientError::CommunicationError(format!("Erro ao ler resposta: {}", e)) + })?; + + // Parse da resposta + let response: Response = serde_json::from_str(&response_line)?; + + // Verifica se o ID bate + if response.id != id { + return Err(ServiceClientError::CommunicationError( + "ID de resposta nao corresponde".into(), + )); + } + + // Verifica erro + if let Some(error) = response.error { + return Err(ServiceClientError::ServiceError { + code: error.code, + message: error.message, + }); + } + + // Retorna resultado + response + .result + .ok_or_else(|| ServiceClientError::CommunicationError("Resposta sem resultado".into())) +} + +#[cfg(target_os = "windows")] +fn connect_to_pipe() -> Result { + // Tenta conectar ao pipe com retry + let mut attempts = 0; + let max_attempts = 3; + + loop { + match std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(PIPE_NAME) + { + Ok(file) => return Ok(file), + Err(e) => { + attempts += 1; + if attempts >= max_attempts { + return Err(ServiceClientError::ServiceUnavailable(format!( + "Nao foi possivel conectar ao servico apos {} tentativas: {}", + max_attempts, e + ))); + } + std::thread::sleep(Duration::from_millis(500)); + } + } + } +} + +#[cfg(not(target_os = "windows"))] +fn connect_to_pipe() -> Result { + Err(ServiceClientError::ServiceUnavailable( + "Named Pipes so estao disponiveis no Windows".into(), + )) +} diff --git a/apps/desktop/src-tauri/src/usb_control.rs b/apps/desktop/src-tauri/src/usb_control.rs new file mode 100644 index 0000000..a95e0a5 --- /dev/null +++ b/apps/desktop/src-tauri/src/usb_control.rs @@ -0,0 +1,408 @@ +//! USB Storage Control Module +//! +//! Este modulo implementa o controle de dispositivos de armazenamento USB no Windows. +//! Utiliza duas abordagens complementares: +//! 1. Removable Storage Access Policy (via registro do Windows) +//! 2. USBSTOR driver control (como fallback/reforco) +//! +//! IMPORTANTE: Requer privilegios de administrador para funcionar. + +use serde::{Deserialize, Serialize}; +use std::io; +use thiserror::Error; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum UsbPolicy { + Allow, + BlockAll, + Readonly, +} + +impl UsbPolicy { + pub fn from_str(s: &str) -> Option { + match s.to_uppercase().as_str() { + "ALLOW" => Some(Self::Allow), + "BLOCK_ALL" => Some(Self::BlockAll), + "READONLY" => Some(Self::Readonly), + _ => None, + } + } + + pub fn as_str(&self) -> &'static str { + match self { + Self::Allow => "ALLOW", + Self::BlockAll => "BLOCK_ALL", + Self::Readonly => "READONLY", + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UsbPolicyResult { + pub success: bool, + pub policy: String, + pub error: Option, + pub applied_at: Option, +} + +#[derive(Error, Debug)] +#[allow(dead_code)] +pub enum UsbControlError { + #[error("Politica USB invalida: {0}")] + InvalidPolicy(String), + #[error("Erro de registro do Windows: {0}")] + RegistryError(String), + #[error("Permissao negada - requer privilegios de administrador")] + PermissionDenied, + #[error("Sistema operacional nao suportado")] + UnsupportedOs, + #[error("Erro de I/O: {0}")] + Io(#[from] io::Error), +} + +#[cfg(target_os = "windows")] +mod windows_impl { + use super::*; + use std::fs; + use std::path::PathBuf; + use std::process::Command; + use winreg::enums::*; + use winreg::RegKey; + + // GUID para Removable Storage Devices (Disk) + const REMOVABLE_STORAGE_GUID: &str = "{53f56307-b6bf-11d0-94f2-00a0c91efb8b}"; + + // Chaves de registro + const REMOVABLE_STORAGE_PATH: &str = + r"Software\Policies\Microsoft\Windows\RemovableStorageDevices"; + const USBSTOR_PATH: &str = r"SYSTEM\CurrentControlSet\Services\USBSTOR"; + const STORAGE_POLICY_PATH: &str = r"SYSTEM\CurrentControlSet\Control\StorageDevicePolicies"; + + pub fn apply_usb_policy(policy: UsbPolicy) -> Result { + let now = chrono::Utc::now().timestamp_millis(); + + let direct_result = try_apply_policy_direct(policy); + + match direct_result { + Ok(()) => Ok(UsbPolicyResult { + success: true, + policy: policy.as_str().to_string(), + error: None, + applied_at: Some(now), + }), + Err(err) => { + // Se faltou permissão, retorna erro - o serviço deve ser usado + // Não fazemos elevação aqui para evitar UAC adicional + if is_permission_error(&err) { + return Err(UsbControlError::PermissionDenied); + } + Err(err) + } + } + } + + fn try_apply_policy_direct(policy: UsbPolicy) -> Result<(), UsbControlError> { + // 1. Aplicar Removable Storage Access Policy + apply_removable_storage_policy(policy)?; + + // 2. Aplicar USBSTOR como reforco + apply_usbstor_policy(policy)?; + + // 3. Aplicar WriteProtect se necessario + if policy == UsbPolicy::Readonly { + apply_write_protect(true)?; + } else { + apply_write_protect(false)?; + } + + Ok(()) + } + + fn apply_removable_storage_policy(policy: UsbPolicy) -> Result<(), UsbControlError> { + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + + let full_path = format!(r"{}\{}", REMOVABLE_STORAGE_PATH, REMOVABLE_STORAGE_GUID); + + match policy { + UsbPolicy::Allow => { + // Tenta remover as restricoes, se existirem + if let Ok(key) = hklm.open_subkey_with_flags(&full_path, KEY_ALL_ACCESS) { + let _ = key.delete_value("Deny_Read"); + let _ = key.delete_value("Deny_Write"); + let _ = key.delete_value("Deny_Execute"); + } + // Tenta remover a chave inteira se estiver vazia + let _ = hklm.delete_subkey(&full_path); + } + UsbPolicy::BlockAll => { + let (key, _) = hklm + .create_subkey(&full_path) + .map_err(map_winreg_error)?; + + key.set_value("Deny_Read", &1u32) + .map_err(map_winreg_error)?; + key.set_value("Deny_Write", &1u32) + .map_err(map_winreg_error)?; + key.set_value("Deny_Execute", &1u32) + .map_err(map_winreg_error)?; + } + UsbPolicy::Readonly => { + let (key, _) = hklm + .create_subkey(&full_path) + .map_err(map_winreg_error)?; + + // Permite leitura, bloqueia escrita + key.set_value("Deny_Read", &0u32) + .map_err(map_winreg_error)?; + key.set_value("Deny_Write", &1u32) + .map_err(map_winreg_error)?; + key.set_value("Deny_Execute", &0u32) + .map_err(map_winreg_error)?; + } + } + + Ok(()) + } + + fn apply_usbstor_policy(policy: UsbPolicy) -> Result<(), UsbControlError> { + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + + let key = hklm + .open_subkey_with_flags(USBSTOR_PATH, KEY_ALL_ACCESS) + .map_err(map_winreg_error)?; + + match policy { + UsbPolicy::Allow => { + // Start = 3 habilita o driver + key.set_value("Start", &3u32) + .map_err(map_winreg_error)?; + } + UsbPolicy::BlockAll | UsbPolicy::Readonly => { + // Start = 4 desabilita o driver + // Nota: Para Readonly, mantemos o driver ativo mas com WriteProtect + // Porem, como fallback de seguranca, desabilitamos para BlockAll + if policy == UsbPolicy::BlockAll { + key.set_value("Start", &4u32) + .map_err(map_winreg_error)?; + } else { + // Readonly mantem driver ativo + key.set_value("Start", &3u32) + .map_err(map_winreg_error)?; + } + } + } + + Ok(()) + } + + fn apply_write_protect(enable: bool) -> Result<(), UsbControlError> { + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + + if enable { + let (key, _) = hklm + .create_subkey(STORAGE_POLICY_PATH) + .map_err(map_winreg_error)?; + + key.set_value("WriteProtect", &1u32) + .map_err(map_winreg_error)?; + } else if let Ok(key) = hklm.open_subkey_with_flags(STORAGE_POLICY_PATH, KEY_ALL_ACCESS) { + let _ = key.set_value("WriteProtect", &0u32); + } + + Ok(()) + } + + pub fn get_current_policy() -> Result { + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + + // Verifica Removable Storage Policy primeiro + let full_path = format!(r"{}\{}", REMOVABLE_STORAGE_PATH, REMOVABLE_STORAGE_GUID); + + if let Ok(key) = hklm.open_subkey_with_flags(&full_path, KEY_READ) { + let deny_read: u32 = key.get_value("Deny_Read").unwrap_or(0); + let deny_write: u32 = key.get_value("Deny_Write").unwrap_or(0); + + if deny_read == 1 && deny_write == 1 { + return Ok(UsbPolicy::BlockAll); + } + + if deny_read == 0 && deny_write == 1 { + return Ok(UsbPolicy::Readonly); + } + } + + // Verifica USBSTOR como fallback + if let Ok(key) = hklm.open_subkey_with_flags(USBSTOR_PATH, KEY_READ) { + let start: u32 = key.get_value("Start").unwrap_or(3); + if start == 4 { + return Ok(UsbPolicy::BlockAll); + } + } + + Ok(UsbPolicy::Allow) + } + + fn is_permission_error(error: &UsbControlError) -> bool { + match error { + UsbControlError::PermissionDenied => true, + UsbControlError::RegistryError(msg) => { + let lower = msg.to_lowercase(); + lower.contains("access is denied") || lower.contains("acesso negado") || lower.contains("5") + } + _ => false, + } + } + + #[allow(dead_code)] + fn apply_policy_with_elevation(policy: UsbPolicy) -> Result<(), UsbControlError> { + // Cria script temporário para aplicar as chaves via PowerShell elevado + let temp_dir = std::env::temp_dir(); + let script_path: PathBuf = temp_dir.join("raven_usb_policy.ps1"); + + let policy_str = policy.as_str(); + let script = format!( + r#"$ErrorActionPreference = 'Stop' +$guid = '{guid}' +$policy = '{policy}' + +function Set-Allow {{ + reg delete 'HKLM\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{guid}' /f 2>$null + reg delete 'HKLM\SYSTEM\CurrentControlSet\Control\StorageDevicePolicies' /f 2>$null + reg add 'HKLM\SYSTEM\CurrentControlSet\Services\USBSTOR' /v Start /t REG_DWORD /d 3 /f | Out-Null +}} + +function Set-BlockAll {{ + reg add 'HKLM\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{guid}' /f | Out-Null + reg add 'HKLM\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{guid}' /v Deny_Read /t REG_DWORD /d 1 /f | Out-Null + reg add 'HKLM\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{guid}' /v Deny_Write /t REG_DWORD /d 1 /f | Out-Null + reg add 'HKLM\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{guid}' /v Deny_Execute /t REG_DWORD /d 1 /f | Out-Null + reg add 'HKLM\SYSTEM\CurrentControlSet\Services\USBSTOR' /v Start /t REG_DWORD /d 4 /f | Out-Null + reg add 'HKLM\SYSTEM\CurrentControlSet\Control\StorageDevicePolicies' /f | Out-Null + reg add 'HKLM\SYSTEM\CurrentControlSet\Control\StorageDevicePolicies' /v WriteProtect /t REG_DWORD /d 0 /f | Out-Null +}} + +function Set-Readonly {{ + reg add 'HKLM\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{guid}' /f | Out-Null + reg add 'HKLM\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{guid}' /v Deny_Read /t REG_DWORD /d 0 /f | Out-Null + reg add 'HKLM\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{guid}' /v Deny_Write /t REG_DWORD /d 1 /f | Out-Null + reg add 'HKLM\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{guid}' /v Deny_Execute /t REG_DWORD /d 0 /f | Out-Null + reg add 'HKLM\SYSTEM\CurrentControlSet\Services\USBSTOR' /v Start /t REG_DWORD /d 3 /f | Out-Null + reg add 'HKLM\SYSTEM\CurrentControlSet\Control\StorageDevicePolicies' /f | Out-Null + reg add 'HKLM\SYSTEM\CurrentControlSet\Control\StorageDevicePolicies' /v WriteProtect /t REG_DWORD /d 1 /f | Out-Null +}} + +switch ($policy) {{ + 'ALLOW' {{ Set-Allow }} + 'BLOCK_ALL' {{ Set-BlockAll }} + 'READONLY' {{ Set-Readonly }} + default {{ throw 'Politica invalida' }} +}} + +try {{ + gpupdate /target:computer /force | Out-Null +}} catch {{}} +"#, + guid = REMOVABLE_STORAGE_GUID, + policy = policy_str + ); + + fs::write(&script_path, script).map_err(UsbControlError::Io)?; + + // Start-Process com RunAs para acionar UAC + let arg = format!( + "Start-Process -WindowStyle Hidden -FilePath powershell -Verb RunAs -Wait -ArgumentList '-ExecutionPolicy Bypass -File \"{}\"'", + script_path.display() + ); + + let status = Command::new("powershell") + .arg("-Command") + .arg(arg) + .status() + .map_err(UsbControlError::Io)?; + + if !status.success() { + return Err(UsbControlError::PermissionDenied); + } + + Ok(()) + } + + fn map_winreg_error(error: io::Error) -> UsbControlError { + if let Some(code) = error.raw_os_error() { + if code == 5 { + return UsbControlError::PermissionDenied; + } + } + UsbControlError::RegistryError(error.to_string()) + } + + pub fn refresh_group_policy() -> Result<(), UsbControlError> { + use std::os::windows::process::CommandExt; + use std::process::Command; + + const CREATE_NO_WINDOW: u32 = 0x08000000; + + // Executa gpupdate para forcar atualizacao das politicas + let output = Command::new("gpupdate") + .args(["/target:computer", "/force"]) + .creation_flags(CREATE_NO_WINDOW) + .output() + .map_err(UsbControlError::Io)?; + + if !output.status.success() { + // Nao e critico se falhar, apenas log + eprintln!( + "[usb_control] gpupdate retornou erro: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + + Ok(()) + } +} + +#[cfg(not(target_os = "windows"))] +mod fallback_impl { + use super::*; + + pub fn apply_usb_policy(_policy: UsbPolicy) -> Result { + Err(UsbControlError::UnsupportedOs) + } + + pub fn get_current_policy() -> Result { + Err(UsbControlError::UnsupportedOs) + } + + pub fn refresh_group_policy() -> Result<(), UsbControlError> { + Err(UsbControlError::UnsupportedOs) + } +} + +#[cfg(target_os = "windows")] +pub use windows_impl::*; + +#[cfg(not(target_os = "windows"))] +pub use fallback_impl::*; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_policy_from_str() { + assert_eq!(UsbPolicy::from_str("ALLOW"), Some(UsbPolicy::Allow)); + assert_eq!(UsbPolicy::from_str("BLOCK_ALL"), Some(UsbPolicy::BlockAll)); + assert_eq!(UsbPolicy::from_str("READONLY"), Some(UsbPolicy::Readonly)); + assert_eq!(UsbPolicy::from_str("allow"), Some(UsbPolicy::Allow)); + assert_eq!(UsbPolicy::from_str("invalid"), None); + } + + #[test] + fn test_policy_as_str() { + assert_eq!(UsbPolicy::Allow.as_str(), "ALLOW"); + assert_eq!(UsbPolicy::BlockAll.as_str(), "BLOCK_ALL"); + assert_eq!(UsbPolicy::Readonly.as_str(), "READONLY"); + } +} diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json new file mode 100644 index 0000000..b9a94d1 --- /dev/null +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -0,0 +1,71 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "Raven", + "version": "0.2.0", + "identifier": "br.com.esdrasrenan.sistemadechamados", + "build": { + "beforeDevCommand": "bun run dev", + "devUrl": "http://localhost:1420", + "beforeBuildCommand": "bun run build", + "frontendDist": "../dist" + }, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "title": "Raven", + "width": 1100, + "height": 720, + "resizable": true, + "fullscreen": false, + "maximized": true + } + ], + "security": { + "csp": null + } + }, + "plugins": { + "updater": { + "endpoints": [ + "https://raw.githubusercontent.com/esdrasrenan/sistema-de-chamados/main/apps/desktop/public/latest.json" + ], + "dialog": true, + "active": true, + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDZDRTBFNkY1NUQ3QzU0QkEKUldTNlZIeGQ5ZWJnYk5mY0J4aWRlb0dRdVZ4TGpBSUZXMnRVUFhmdmlLT0tlY084UjJQUHFWWUkK" + }, + "deep-link": { + "desktop": { + "schemes": ["raven"] + } + } + }, + "bundle": { + "active": true, + "createUpdaterArtifacts": true, + "targets": ["nsis", "deb", "rpm"], + "icon": [ + "icons/icon.ico", + "icons/icon.icns", + "icons/icon.png", + "icons/Raven.png" + ], + "resources": { + "../service/target/release/raven-service.exe": "raven-service.exe" + }, + "windows": { + "webviewInstallMode": { + "type": "skip" + }, + "nsis": { + "displayLanguageSelector": true, + "installerIcon": "icons/icon.ico", + "headerImage": "icons/nsis-header.bmp", + "sidebarImage": "icons/nsis-sidebar.bmp", + "installMode": "perMachine", + "installerHooks": "installer-hooks.nsh", + "languages": ["PortugueseBR"] + } + } + } +} diff --git a/apps/desktop/src/assets/tauri.svg b/apps/desktop/src/assets/tauri.svg new file mode 100644 index 0000000..31b62c9 --- /dev/null +++ b/apps/desktop/src/assets/tauri.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/desktop/src/assets/typescript.svg b/apps/desktop/src/assets/typescript.svg new file mode 100644 index 0000000..30a5edd --- /dev/null +++ b/apps/desktop/src/assets/typescript.svg @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/apps/desktop/src/assets/vite.svg b/apps/desktop/src/assets/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/apps/desktop/src/assets/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/desktop/src/chat/ChatHubWidget.tsx b/apps/desktop/src/chat/ChatHubWidget.tsx new file mode 100644 index 0000000..04358d9 --- /dev/null +++ b/apps/desktop/src/chat/ChatHubWidget.tsx @@ -0,0 +1,256 @@ +/** + * ChatHubWidget - Lista de sessoes de chat ativas usando Convex subscriptions + * + * Arquitetura: + * - Usa useQuery do Convex React para subscription reativa (tempo real verdadeiro) + * - Sem polling - todas as atualizacoes sao push-based via WebSocket + * - Tauri usado apenas para gerenciamento de janelas + */ + +import { useEffect, useState } from "react" +import { invoke } from "@tauri-apps/api/core" +import { Loader2, MessageCircle, ChevronUp, X, Minimize2 } from "lucide-react" +import { useMachineSessions, type MachineSession } from "./useConvexMachineQueries" + +/** + * Hub Widget - Lista todas as sessoes de chat ativas + * Ao clicar em uma sessao, abre/foca a janela de chat daquele ticket + */ +export function ChatHubWidget() { + // Inicializa baseado na altura real da janela (< 100px = minimizado) + const [isMinimized, setIsMinimized] = useState(() => window.innerHeight < 100) + + // Convex subscription reativa + const { sessions = [], isLoading, hasToken } = useMachineSessions() + + // Sincronizar estado minimizado com tamanho da janela + useEffect(() => { + const mountTime = Date.now() + const STABILIZATION_DELAY = 500 + + const handler = () => { + if (Date.now() - mountTime < STABILIZATION_DELAY) { + return + } + const h = window.innerHeight + setIsMinimized(h < 100) + } + window.addEventListener("resize", handler) + return () => window.removeEventListener("resize", handler) + }, []) + + const handleSelectSession = async (ticketId: string, ticketRef: number) => { + try { + // Tauri 2.x auto-converts snake_case (Rust) to camelCase (JS) + await invoke("open_chat_window", { ticketId, ticketRef }) + await invoke("close_hub_window") + } catch (err) { + console.error("open_chat_window FAILED:", err) + } + } + + const handleMinimize = async () => { + setIsMinimized(true) + try { + await invoke("set_hub_minimized", { minimized: true }) + } catch (err) { + console.error("Erro ao minimizar hub:", err) + } + } + + const handleExpand = async () => { + try { + await invoke("set_hub_minimized", { minimized: false }) + setTimeout(() => setIsMinimized(false), 100) + } catch (err) { + console.error("set_hub_minimized FAILED:", err) + setIsMinimized(false) + } + } + + const handleClose = () => { + invoke("close_hub_window").catch((err) => { + console.error("Erro ao fechar janela do hub:", err) + }) + } + + const totalUnread = sessions.reduce((sum, s) => sum + s.unreadCount, 0) + + // Sem token + if (!hasToken) { + return ( +
+
+ Token nao configurado +
+
+ ) + } + + // Loading + if (isLoading) { + return ( +
+
+ + Carregando... +
+
+ ) + } + + // Sem sessoes ativas + if (sessions.length === 0) { + return ( +
+
+ + Sem chats +
+
+ ) + } + + // Minimizado + if (isMinimized) { + return ( +
+ +
+ ) + } + + // Expandido + return ( +
+ {/* Header */} +
+
+
+ +
+
+

Chats Ativos

+

+ {sessions.length} conversa{sessions.length !== 1 ? "s" : ""} +

+
+
+
+ + +
+
+ + {/* Lista de sessoes */} +
+
+ {sessions.map((session) => ( + handleSelectSession(session.ticketId, session.ticketRef)} + /> + ))} +
+
+
+ ) +} + +function SessionItem({ + session, + onClick, +}: { + session: MachineSession + onClick: () => void +}) { + const handleClick = (e: React.MouseEvent) => { + e.stopPropagation() + onClick() + } + + return ( + + ) +} + +function formatRelativeTime(timestamp: number): string { + const now = Date.now() + const diff = now - timestamp + + const minutes = Math.floor(diff / 60000) + if (minutes < 1) return "agora" + if (minutes < 60) return `${minutes}m` + + const hours = Math.floor(minutes / 60) + if (hours < 24) return `${hours}h` + + const days = Math.floor(hours / 24) + return `${days}d` +} diff --git a/apps/desktop/src/chat/ChatWidget.tsx b/apps/desktop/src/chat/ChatWidget.tsx new file mode 100644 index 0000000..3c5e9c5 --- /dev/null +++ b/apps/desktop/src/chat/ChatWidget.tsx @@ -0,0 +1,896 @@ +/** + * ChatWidget - Componente de chat em tempo real usando Convex subscriptions + * + * Arquitetura: + * - Usa useQuery do Convex React para subscriptions reativas (tempo real verdadeiro) + * - Usa useMutation do Convex React para enviar mensagens + * - Mantém Tauri apenas para: upload de arquivos, gerenciamento de janela + * - Sem polling - todas as atualizacoes sao push-based via WebSocket + */ + +import { useCallback, useEffect, useMemo, useRef, useState } from "react" +import { open as openDialog } from "@tauri-apps/plugin-dialog" +import { openUrl as openExternal } from "@tauri-apps/plugin-opener" +import { invoke } from "@tauri-apps/api/core" +import { Send, X, Loader2, MessageCircle, Paperclip, FileText, Image as ImageIcon, File, User, ChevronUp, Minimize2, Eye, Download, Check, MessagesSquare } from "lucide-react" +import type { Id } from "@convex/_generated/dataModel" +import { useMachineMessages, useMachineSessions, usePostMachineMessage, useMarkMachineMessagesRead, type MachineMessage } from "./useConvexMachineQueries" +import { useConvexMachine } from "./ConvexMachineProvider" + +const MAX_MESSAGES_IN_MEMORY = 200 +const MARK_READ_BATCH_SIZE = 50 +const SCROLL_BOTTOM_THRESHOLD_PX = 120 + +const ALLOWED_EXTENSIONS = [ + "jpg", "jpeg", "png", "gif", "webp", + "pdf", "txt", "doc", "docx", "xls", "xlsx", +] + +interface UploadedAttachment { + storageId: string + name: string + size?: number + type?: string +} + +interface ChatAttachment { + storageId: string + name: string + size?: number + type?: string +} + +function getFileIcon(fileName: string) { + const ext = fileName.toLowerCase().split(".").pop() ?? "" + if (["jpg", "jpeg", "png", "gif", "webp"].includes(ext)) { + return + } + if (["pdf", "doc", "docx", "txt"].includes(ext)) { + return + } + return +} + +function isImageAttachment(attachment: ChatAttachment) { + if (attachment.type?.startsWith("image/")) return true + const ext = attachment.name.toLowerCase().split(".").pop() ?? "" + return ["jpg", "jpeg", "png", "gif", "webp"].includes(ext) +} + +function formatAttachmentSize(size?: number) { + if (!size) return null + if (size < 1024) return `${size}B` + const kb = size / 1024 + if (kb < 1024) return `${Math.round(kb)}KB` + return `${(kb / 1024).toFixed(1)}MB` +} + +function getUnreadAgentMessageIds(messages: MachineMessage[], unreadCount: number): string[] { + if (unreadCount <= 0 || messages.length === 0) return [] + const ids: string[] = [] + for (let i = messages.length - 1; i >= 0 && ids.length < unreadCount; i--) { + const msg = messages[i] + if (!msg.isFromMachine) { + ids.push(msg.id) + } + } + return ids.reverse() +} + +function chunkArray(items: T[], size: number): T[][] { + if (size <= 0) return [items] + const result: T[][] = [] + for (let i = 0; i < items.length; i += size) { + result.push(items.slice(i, i + size)) + } + return result +} + +function MessageAttachment({ + attachment, + isAgent, + loadUrl, +}: { + attachment: ChatAttachment + isAgent: boolean + loadUrl: (storageId: string) => Promise +}) { + const [url, setUrl] = useState(null) + const [loading, setLoading] = useState(true) + const [downloading, setDownloading] = useState(false) + const [downloaded, setDownloaded] = useState(false) + + useEffect(() => { + let cancelled = false + setLoading(true) + loadUrl(attachment.storageId) + .then((resolved) => { + if (!cancelled) setUrl(resolved) + }) + .catch((err) => { + console.error("Falha ao carregar URL do anexo:", err) + }) + .finally(() => { + if (!cancelled) setLoading(false) + }) + + return () => { + cancelled = true + } + }, [attachment.storageId, loadUrl]) + + const handleView = async () => { + if (!url) return + try { + await openExternal(url) + } catch (err) { + console.error("Falha ao abrir anexo:", err) + } + } + + const handleDownload = async () => { + if (!url || downloading) return + setDownloading(true) + try { + const response = await fetch(url) + const blob = await response.blob() + const downloadUrl = URL.createObjectURL(blob) + const a = document.createElement("a") + a.href = downloadUrl + a.download = attachment.name + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(downloadUrl) + setDownloaded(true) + setTimeout(() => setDownloaded(false), 2000) + } catch (err) { + console.error("Falha ao baixar anexo:", err) + await handleView() + } finally { + setDownloading(false) + } + } + + const sizeLabel = formatAttachmentSize(attachment.size) + const isImage = isImageAttachment(attachment) + + if (loading) { + return ( +
+ + Carregando anexo... +
+ ) + } + + if (isImage && url) { + return ( +
+ {/* eslint-disable-next-line @next/next/no-img-element -- Tauri desktop app, not Next.js */} + {attachment.name} +
+ + +
+
+ ) + } + + return ( +
+ {getFileIcon(attachment.name)} + + {sizeLabel && ({sizeLabel})} +
+ + +
+
+ ) +} + +interface ChatWidgetProps { + ticketId: string + ticketRef?: number +} + +export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) { + const [inputValue, setInputValue] = useState("") + const [isSending, setIsSending] = useState(false) + const [isUploading, setIsUploading] = useState(false) + const [pendingAttachments, setPendingAttachments] = useState([]) + // Inicializa baseado na altura real da janela (< 100px = minimizado) + const [isMinimized, setIsMinimized] = useState(() => window.innerHeight < 100) + + // Convex hooks + const { apiBaseUrl, machineToken } = useConvexMachine() + const { sessions: machineSessions = [] } = useMachineSessions() + const { messages: convexMessages, hasSession, unreadCount, isLoading } = useMachineMessages( + ticketId as Id<"tickets">, + { limit: MAX_MESSAGES_IN_MEMORY } + ) + const postMessage = usePostMachineMessage() + const markMessagesRead = useMarkMachineMessagesRead() + + // Limitar mensagens em memoria + const messages = useMemo(() => convexMessages.slice(-MAX_MESSAGES_IN_MEMORY), [convexMessages]) + + const messagesEndRef = useRef(null) + const messagesContainerRef = useRef(null) + const messageElementsRef = useRef>(new Map()) + const prevHasSessionRef = useRef(false) + + const [isAtBottom, setIsAtBottom] = useState(true) + const isAtBottomRef = useRef(true) + const pendingScrollActionRef = useRef< + | { type: "bottom"; behavior: ScrollBehavior; markRead: boolean } + | { type: "message"; messageId: string; behavior: ScrollBehavior; markRead: boolean } + | null + >(null) + const autoReadInFlightRef = useRef(false) + const lastAutoReadCountRef = useRef(null) + + const unreadAgentMessageIds = useMemo(() => getUnreadAgentMessageIds(messages, unreadCount), [messages, unreadCount]) + const firstUnreadAgentMessageId = unreadAgentMessageIds[0] ?? null + + const otherUnreadCount = useMemo(() => { + if (machineSessions.length <= 1) return 0 + return machineSessions.reduce((sum, session) => { + return sum + (session.ticketId === ticketId ? 0 : session.unreadCount) + }, 0) + }, [machineSessions, ticketId]) + + const handleOpenHub = useCallback(async () => { + try { + await invoke("open_hub_window") + await invoke("set_hub_minimized", { minimized: false }) + } catch (err) { + console.error("Erro ao abrir hub:", err) + } + }, []) + + const updateIsAtBottom = useCallback(() => { + const el = messagesContainerRef.current + if (!el) return + const distance = el.scrollHeight - el.scrollTop - el.clientHeight + const atBottom = distance <= SCROLL_BOTTOM_THRESHOLD_PX + if (isAtBottomRef.current !== atBottom) { + isAtBottomRef.current = atBottom + setIsAtBottom(atBottom) + } + }, []) + + const scrollToBottom = useCallback((behavior: ScrollBehavior) => { + messagesEndRef.current?.scrollIntoView({ behavior }) + requestAnimationFrame(() => updateIsAtBottom()) + }, [updateIsAtBottom]) + + const scrollToMessage = useCallback((messageId: string, behavior: ScrollBehavior) => { + const el = messageElementsRef.current.get(messageId) + if (!el) return false + el.scrollIntoView({ behavior, block: "center" }) + requestAnimationFrame(() => updateIsAtBottom()) + return true + }, [updateIsAtBottom]) + + // Fechar janela quando sessao termina + useEffect(() => { + const prevHasSession = prevHasSessionRef.current + if (prevHasSession && !hasSession) { + invoke("close_chat_window", { ticketId }).catch((err) => { + console.error("Erro ao fechar janela ao encerrar sessao:", err) + }) + } + prevHasSessionRef.current = hasSession + }, [hasSession, ticketId]) + + // Ref para acessar isMinimized dentro de callbacks + const isMinimizedRef = useRef(isMinimized) + useEffect(() => { + isMinimizedRef.current = isMinimized + }, [isMinimized]) + + // Cache de URLs de anexos + const attachmentUrlCacheRef = useRef>(new Map()) + + const loadAttachmentUrl = useCallback(async (storageId: string) => { + const cached = attachmentUrlCacheRef.current.get(storageId) + if (cached) return cached + + if (!apiBaseUrl || !machineToken) { + throw new Error("Configuracao nao disponivel") + } + + const response = await fetch(`${apiBaseUrl}/api/machines/chat/attachments/url`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + machineToken, + ticketId, + storageId, + }), + }) + + if (!response.ok) { + const text = await response.text().catch(() => "") + throw new Error(text || `Falha ao obter URL do anexo (${response.status})`) + } + + const data = (await response.json()) as { url?: string } + if (!data.url) { + throw new Error("Resposta invalida ao obter URL do anexo") + } + + attachmentUrlCacheRef.current.set(storageId, data.url) + return data.url + }, [apiBaseUrl, machineToken, ticketId]) + + const markUnreadMessagesRead = useCallback(async () => { + if (unreadCount <= 0) return false + const ids = getUnreadAgentMessageIds(messages, unreadCount) + if (ids.length === 0) return false + + const chunks = chunkArray(ids, MARK_READ_BATCH_SIZE) + for (const chunk of chunks) { + await markMessagesRead({ + ticketId: ticketId as Id<"tickets">, + messageIds: chunk as Id<"ticketChatMessages">[], + }) + } + return true + }, [messages, ticketId, unreadCount, markMessagesRead]) + + const maybeAutoMarkRead = useCallback(async () => { + if (autoReadInFlightRef.current) return + if (!hasSession || unreadCount <= 0) return + if (isMinimizedRef.current || !isAtBottomRef.current) return + if (lastAutoReadCountRef.current === unreadCount) return + + autoReadInFlightRef.current = true + try { + const didMark = await markUnreadMessagesRead() + if (didMark) { + lastAutoReadCountRef.current = unreadCount + } + } finally { + autoReadInFlightRef.current = false + } + }, [hasSession, unreadCount, markUnreadMessagesRead]) + + // Auto-scroll quando novas mensagens chegam (se ja estava no bottom) + const prevMessagesLengthRef = useRef(messages.length) + useEffect(() => { + if (messages.length > prevMessagesLengthRef.current && isAtBottomRef.current && !isMinimizedRef.current) { + pendingScrollActionRef.current = { type: "bottom", behavior: "smooth", markRead: true } + } + prevMessagesLengthRef.current = messages.length + }, [messages.length]) + + // Executar scroll pendente + useEffect(() => { + if (isMinimized) return + + const action = pendingScrollActionRef.current + if (!action) return + + if (action.type === "bottom") { + if (!messagesEndRef.current) return + pendingScrollActionRef.current = null + scrollToBottom(action.behavior) + if (action.markRead) { + markUnreadMessagesRead().catch((err) => console.error("Falha ao marcar mensagens como lidas:", err)) + } + return + } + + const ok = scrollToMessage(action.messageId, action.behavior) + if (!ok) { + if (!messagesEndRef.current) return + pendingScrollActionRef.current = null + scrollToBottom(action.behavior) + if (action.markRead) { + markUnreadMessagesRead().catch((err) => console.error("Falha ao marcar mensagens como lidas:", err)) + } + return + } + + pendingScrollActionRef.current = null + if (action.markRead) { + markUnreadMessagesRead().catch((err) => console.error("Falha ao marcar mensagens como lidas:", err)) + } + }, [isMinimized, messages, markUnreadMessagesRead, scrollToBottom, scrollToMessage]) + + useEffect(() => { + if (unreadCount === 0) { + lastAutoReadCountRef.current = null + return + } + maybeAutoMarkRead().catch((err) => console.error("Falha ao auto-marcar mensagens:", err)) + }, [isMinimized, isAtBottom, unreadCount, maybeAutoMarkRead]) + + // Sincronizar estado minimizado com tamanho da janela + useEffect(() => { + const mountTime = Date.now() + const STABILIZATION_DELAY = 500 + + const handler = () => { + if (Date.now() - mountTime < STABILIZATION_DELAY) { + return + } + const h = window.innerHeight + setIsMinimized(h < 100) + } + window.addEventListener("resize", handler) + return () => window.removeEventListener("resize", handler) + }, []) + + // Selecionar arquivo para anexar + const handleAttach = async () => { + if (isUploading || isSending) return + + try { + const selected = await openDialog({ + multiple: false, + filters: [{ + name: "Arquivos permitidos", + extensions: ALLOWED_EXTENSIONS, + }], + }) + + if (!selected) return + + const filePath = typeof selected === "string" ? selected : (selected as { path: string }).path + + setIsUploading(true) + + if (!apiBaseUrl || !machineToken) { + throw new Error("Configuracao nao disponivel") + } + + const attachment = await invoke("upload_chat_file", { + baseUrl: apiBaseUrl, + token: machineToken, + filePath, + }) + + setPendingAttachments(prev => [...prev, attachment]) + } catch (err) { + console.error("Erro ao anexar arquivo:", err) + alert(typeof err === "string" ? err : "Erro ao anexar arquivo") + } finally { + setIsUploading(false) + } + } + + // Remover anexo pendente + const handleRemoveAttachment = (storageId: string) => { + setPendingAttachments(prev => prev.filter(a => a.storageId !== storageId)) + } + + // Enviar mensagem + const handleSend = async () => { + if ((!inputValue.trim() && pendingAttachments.length === 0) || isSending) return + + const messageText = inputValue.trim() + const attachmentsToSend = [...pendingAttachments] + setInputValue("") + setPendingAttachments([]) + setIsSending(true) + + try { + await postMessage({ + ticketId: ticketId as Id<"tickets">, + body: messageText, + attachments: attachmentsToSend.length > 0 ? attachmentsToSend.map(a => ({ + storageId: a.storageId as Id<"_storage">, + name: a.name, + size: a.size, + type: a.type, + })) : undefined, + }) + pendingScrollActionRef.current = { type: "bottom", behavior: "smooth", markRead: false } + } catch (err) { + console.error("Erro ao enviar mensagem:", err) + setInputValue(messageText) + setPendingAttachments(attachmentsToSend) + } finally { + setIsSending(false) + } + } + + const handleMinimize = async () => { + setIsMinimized(true) + try { + if (machineSessions.length > 1) { + await invoke("open_hub_window") + await invoke("close_chat_window", { ticketId }) + return + } + await invoke("set_chat_minimized", { ticketId, minimized: true }) + } catch (err) { + console.error("Erro ao minimizar janela:", err) + } + } + + const handleExpand = async () => { + if (firstUnreadAgentMessageId) { + pendingScrollActionRef.current = { type: "message", messageId: firstUnreadAgentMessageId, behavior: "auto", markRead: unreadCount > 0 } + } else { + pendingScrollActionRef.current = { type: "bottom", behavior: "auto", markRead: false } + } + + setIsMinimized(false) + try { + await invoke("open_chat_window", { ticketId, ticketRef: ticketRef ?? 0 }) + } catch (err) { + console.error("Erro ao expandir janela:", err) + } + } + + const handleClose = () => { + invoke("close_chat_window", { ticketId }).catch((err) => { + console.error("Erro ao fechar janela de chat:", err) + }) + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault() + handleSend() + } + } + + // Loading + if (isLoading) { + return ( +
+
+ + Carregando... +
+
+ ) + } + + // Sem sessao ativa + if (!hasSession) { + return ( +
+
+ + + {ticketRef ? `Ticket #${ticketRef}` : "Chat"} + + + Offline + +
+
+ ) + } + + // Minimizado + if (isMinimized) { + return ( +
+ +
+ ) + } + + // Expandido + return ( +
+ {/* Header */} +
+
+
+ +
+
+
+

Chat

+ + + Online + +
+

+ Ticket #{ticketRef} - Suporte +

+
+
+
+ {machineSessions.length > 1 && ( + + )} + + +
+
+ + {/* Mensagens */} +
+ {messages.length === 0 ? ( +
+

+ Nenhuma mensagem ainda +

+

+ O agente iniciara a conversa em breve +

+
+ ) : ( +
+ {messages.map((msg) => { + const isAgent = !msg.isFromMachine + const bodyText = msg.body.trim() + const shouldShowBody = + bodyText.length > 0 && !(bodyText === "[Anexo]" && (msg.attachments?.length ?? 0) > 0) + return ( +
+ {firstUnreadAgentMessageId === msg.id && unreadCount > 0 && !isAtBottom && ( +
+
+ Novas mensagens +
+
+ )} + +
{ + if (el) { + messageElementsRef.current.set(msg.id, el) + } else { + messageElementsRef.current.delete(msg.id) + } + }} + className={`flex gap-2 ${isAgent ? "flex-row-reverse" : "flex-row"}`} + > + {/* Avatar */} +
+ {isAgent ? : } +
+ + {/* Bubble */} +
+ {!isAgent && ( +

+ {msg.authorName} +

+ )} + {shouldShowBody &&

{msg.body}

} + {/* Anexos */} + {msg.attachments && msg.attachments.length > 0 && ( +
+ {msg.attachments.map((att) => ( + + ))} +
+ )} +

+ {formatTime(msg.createdAt)} +

+
+
+
+ ) + })} +
+
+ )} +
+ + {/* Input */} +
+ {unreadCount > 0 && !isAtBottom && ( +
+ +
+ )} + {/* Anexos pendentes */} + {pendingAttachments.length > 0 && ( +
+ {pendingAttachments.map((att) => ( +
+ {getFileIcon(att.name)} + {att.name} + +
+ ))} +
+ )} +
+